15 releases (7 breaking)
0.8.0 | Aug 28, 2024 |
---|---|
0.7.4 | Jun 17, 2024 |
0.7.3 | May 3, 2024 |
0.6.0 | Feb 20, 2024 |
0.2.0 | Jan 14, 2023 |
#1519 in HTTP server
83 downloads per month
Used in ohkami
32KB
384 lines
Ohkami
Ohkami - [狼] wolf in Japanese - is intuitive and declarative web framework.- macro-less and type-safe APIs for intuitive and declarative code
- multiple runtimes are supported:
tokio
,async-std
,smol
,glommio
,worker
(Cloudflare Workers)
Benchmark Results
Quick Start
- Add to
dependencies
:
[dependencies]
ohkami = { version = "0.20", features = ["rt_tokio"] }
tokio = { version = "1", features = ["full"] }
- Write your first code with Ohkami : examples/quick_start
use ohkami::prelude::*;
use ohkami::typed::status;
async fn health_check() -> status::NoContent {
status::NoContent
}
async fn hello(name: &str) -> String {
format!("Hello, {name}!")
}
#[tokio::main]
async fn main() {
Ohkami::new((
"/healthz"
.GET(health_check),
"/hello/:name"
.GET(hello),
)).howl("localhost:3000").await
}
- Run and check the behavior :
$ cargo run
$ curl http://localhost:3000/healthz
$ curl http://localhost:3000/hello/your_name
Hello, your_name!
Feature flags
"rt_tokio"
, "rt_async-std"
, "rt_smol"
, "rt_glommio"
:async runtime
"rt_worker"
:Cloudflare Workers
npm create cloudflare ./path/to/project -- --template https://github.com/ohkami-rs/ohkami-templates/worker
then your project directory has wrangler.toml
, package.json
and a Rust library crate. Local dev by npm run dev
and deploy by npm run deploy
!
See README of the template for details.
"graceful"
:Graceful Shutdown
Automatically catch Ctrl-C ( SIGINT ) and perform graceful shutdown.
Currently, only supported on rt_tokio
.
"sse"
:Server-Sent Events
Ohkami responds with HTTP/1.1 Transfer-Encoding: chunked
.
Use some reverse proxy to do with HTTP/2,3.
use ohkami::prelude::*;
use ohkami::typed::DataStream;
use ohkami::utils::stream;
use {tokio::time::sleep, std::time::Duration};
async fn sse() -> DataStream<String> {
DataStream::from_stream(stream::queue(|mut q| async move {
for i in 1..=5 {
sleep(Duration::from_secs(1)).await;
q.add(format!("Hi, I'm message #{i} !"))
}
}))
}
#[tokio::main]
async fn main() {
Ohkami::new((
"/sse".GET(sse),
)).howl("localhost:5050").await
}
"ws"
:WebSocket
Ohkami only handles ws://
.
Use some reverse proxy to do with wss://
.
Currently, WebSocket on rt_worker
is not supported.
use ohkami::prelude::*;
use ohkami::ws::{WebSocketContext, WebSocket, Message};
async fn echo_text(c: WebSocketContext<'_>) -> WebSocket {
c.connect(|mut ws| async move {
while let Ok(Some(Message::Text(text))) = ws.recv().await {
ws.send(Message::Text(text)).await.expect("Failed to send text");
}
})
}
#[tokio::main]
async fn main() {
Ohkami::new((
"/ws".GET(echo_text),
)).howl("localhost:3030").await
}
"ip"
:remote IP address
Get and hold remote peer's IP address
"nightly"
:enable nightly-only functionalities
- try response
Snippets
Middlewares
Ohkami's request handling system is called "fangs", and middlewares are implemented on this.
builtin fang : CORS
, JWT
, BasicAuth
, Timeout
use ohkami::prelude::*;
#[derive(Clone)]
struct GreetingFang;
/* utility trait; automatically impl `Fang` trait */
impl FangAction for GreetingFang {
async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> {
println!("Welcomm request!: {req:?}");
Ok(())
}
async fn back<'a>(&'a self, res: &'a mut Response) {
println!("Go, response!: {res:?}");
}
}
#[tokio::main]
async fn main() {
Ohkami::with(GreetingFang, (
"/".GET(|| async {"Hello, fangs!"})
)).howl("localhost:3000").await
}
Typed payload
builtin payload : JSON
, Text
, HTML
, URLEncoded
, Multipart
use ohkami::prelude::*;
use ohkami::typed::{status};
use ohkami::format::JSON;
/* `serde = 〜` is not needed in your [dependencies] */
use ohkami::serde::{Serialize, Deserialize};
/* Deserialize for request */
#[derive(Deserialize)]
struct CreateUserRequest<'req> {
name: &'req str,
password: &'req str,
}
/* Serialize for response */
#[derive(Serialize)]
struct User {
name: String,
}
async fn create_user(
JSON(req): JSON<CreateUserRequest<'_>>
) -> status::Created<JSON<User>> {
status::Created(JSON(
User {
name: String::from(req.name)
}
))
}
Typed params
use ohkami::prelude::*;
use ohkami::format::{Query, JSON};
use ohkami::serde::{Serialize, Deserialize};
#[tokio::main]
async fn main() {
Ohkami::new((
"/hello/:name"
.GET(hello),
"/hello/:name/:n"
.GET(hello_n),
"/search"
.GET(search),
)).howl("localhost:5000").await
}
async fn hello(name: &str) -> String {
format!("Hello, {name}!")
}
async fn hello_n((name, n): (&str, usize)) -> String {
vec![format!("Hello, {name}!"); n].join(" ")
}
#[derive(Deserialize)]
struct SearchQuery<'q> {
#[serde(rename = "q")]
keyword: &'q str,
lang: &'q str,
}
#[derive(Serialize)]
struct SearchResult {
title: String,
}
async fn search(
Query(query): Query<SearchQuery<'_>>
) -> JSON<Vec<SearchResult>> {
JSON(vec![
SearchResult { title: String::from("ohkami") },
])
}
Static directory serving
use ohkami::prelude::*;
#[tokio::main]
async fn main() {
Ohkami::new((
"/".Dir("./dist"),
)).howl("0.0.0.0:3030").await
}
File upload
use ohkami::prelude::*;
use ohkami::typed::status;
use ohkami::format::{Multipart, File};
use ohkami::serde::Deserialize;
#[derive(Deserialize)]
struct FormData<'req> {
#[serde(rename = "account-name")]
account_name: Option<&'req str>,
pics: Vec<File<'req>>,
}
async fn post_submit(
Multipart(data): Multipart<FormData<'_>>
) -> status::NoContent {
println!("\n\
===== submit =====\n\
[account name] {:?}\n\
[ pictures ] {} files (mime: [{}])\n\
==================",
data.account_name,
data.pics.len(),
data.pics.iter().map(|f| f.mimetype).collect::<Vec<_>>().join(", "),
);
status::NoContent
}
Pack of Ohkamis
use ohkami::prelude::*;
use ohkami::typed::status;
use ohkami::format::JSON;
use ohkami::serde::Serialize;
#[derive(Serialize)]
struct User {
name: String
}
async fn list_users() -> JSON<Vec<User>> {
JSON(vec![
User { name: String::from("actix") },
User { name: String::from("axum") },
User { name: String::from("ohkami") },
])
}
async fn create_user() -> status::Created<JSON<User>> {
status::Created(JSON(User {
name: String::from("ohkami web framework")
}))
}
async fn health_check() -> status::NoContent {
status::NoContent
}
#[tokio::main]
async fn main() {
// ...
let users_ohkami = Ohkami::new((
"/"
.GET(list_users)
.POST(create_user),
));
Ohkami::new((
"/healthz"
.GET(health_check),
"/api/users"
.By(users_ohkami), // nest by `By`
)).howl("localhost:5000").await
}
Testing
use ohkami::prelude::*;
use ohkami::testing::*; // <--
fn hello_ohkami() -> Ohkami {
Ohkami::new((
"/hello".GET(|| async {"Hello, world!"}),
))
}
#[cfg(test)]
#[tokio::test]
async fn test_my_ohkami() {
let t = hello_ohkami().test();
let req = TestRequest::GET("/");
let res = t.oneshot(req).await;
assert_eq!(res.status(), Status::NotFound);
let req = TestRequest::GET("/hello");
let res = t.oneshot(req).await;
assert_eq!(res.status(), Status::OK);
assert_eq!(res.text(), Some("Hello, world!"));
}
Supported protocols
- HTTP/1.1
- HTTP/2
- HTTP/3
- HTTPS
- Server-Sent Events
- WebSocket
MSRV ( Minimum Supported Rust Version )
Latest stable
License
ohkami is licensed under MIT LICENSE ( LICENSE or https://opensource.org/licenses/MIT ).
Dependencies
~1.2–1.9MB
~44K SLoC