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)
License build check status of ohkami crates.io

## Benchmark Results - [Web Frameworks Benchmark](https://web-frameworks-benchmark.netlify.app/result?l=rust)
## Quick Start 1. Add to `dependencies` : ```toml [dependencies] ohkami = { version = "0.20", features = ["rt_tokio"] } tokio = { version = "1", features = ["full"] } ``` 2. Write your first code with Ohkami : [examples/quick_start](https://github.com/ohkami-rs/ohkami/blob/main/examples/quick_start/src/main.rs) ```rust,no_run 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 } ``` 3. Run and check the behavior : ```sh $ cargo run ``` ```sh $ 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 - [tokio](https://github.com/tokio-rs/tokio) - [async-std](https://github.com/async-rs/async-std) - [smol](https://github.com/smol-rs/smol) - [glommio](https://github.com/DataDog/glommio) ### `"rt_worker"`:Cloudflare Workers ```sh 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](https://github.com/ohkami-rs/ohkami-templates/tree/main/worker) 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. ```rust,no_run use ohkami::prelude::*; use ohkami::typed::DataStream; use ohkami::utils::stream; use {tokio::time::sleep, std::time::Duration}; async fn sse() -> DataStream { 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. ```rust,no_run 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 "**fang**s", and middlewares are implemented on this. *builtin fang* : `CORS`, `JWT`, `BasicAuth`, `Timeout` ```rust,no_run 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` ```rust 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> ) -> status::Created> { status::Created(JSON( User { name: String::from(req.name) } )) } ``` ### Typed params ```rust,no_run 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> ) -> JSON> { JSON(vec![ SearchResult { title: String::from("ohkami") }, ]) } ``` ### Static directory serving ```rust,no_run use ohkami::prelude::*; #[tokio::main] async fn main() { Ohkami::new(( "/".Dir("./dist"), )).howl("0.0.0.0:3030").await } ``` ### File upload ```rust,no_run 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>, } async fn post_submit( Multipart(data): Multipart> ) -> 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::>().join(", "), ); status::NoContent } ``` ### Pack of Ohkamis ```rust,no_run 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> { JSON(vec![ User { name: String::from("actix") }, User { name: String::from("axum") }, User { name: String::from("ohkami") }, ]) } async fn create_user() -> status::Created> { 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 ```rust 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 - [x] HTTP/1.1 - [ ] HTTP/2 - [ ] HTTP/3 - [ ] HTTPS - [x] Server-Sent Events - [x] WebSocket ## MSRV ( Minimum Supported Rust Version ) Latest stable ## License ohkami is licensed under MIT LICENSE ( [LICENSE](https://github.com/ohkami-rs/ohkami/blob/main/LICENSE) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) ).