├── examples ├── jwt │ ├── .gitignore │ ├── .env.sample │ └── Cargo.toml ├── websocket │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── template │ │ ├── main.js │ │ └── index.html ├── static_files │ ├── public │ │ ├── index.js │ │ ├── .env.sample │ │ ├── sub.js.br │ │ ├── sub.js.gz │ │ ├── blog │ │ │ ├── second.html.gz │ │ │ ├── second.html │ │ │ ├── first.html │ │ │ └── index.html │ │ ├── about.html │ │ └── index.html │ └── Cargo.toml ├── quick_start │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── json_response │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── basic_auth │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── uibeam │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── derive_from_request │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── sse │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── chatgpt │ ├── Cargo.toml │ └── src │ │ ├── error.rs │ │ ├── fangs.rs │ │ ├── models.rs │ │ └── main.rs ├── multiple-single-threads │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── hello │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── form │ ├── Cargo.toml │ ├── form.html │ └── src │ │ └── main.rs ├── Cargo.toml └── test.sh ├── .github ├── CODEOWNERS └── workflows │ ├── AutoApprove.yml │ ├── Publish.yml │ └── CI.yml ├── samples ├── openapi-tags │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── streaming │ ├── .gitignore │ ├── Cargo.toml │ ├── src │ │ └── main.rs │ └── openapi.json.sample ├── readme-openapi │ ├── .gitignore │ ├── Cargo.toml │ ├── src │ │ └── main.rs │ └── openapi.json.sample ├── openapi-schema-enums │ ├── .gitignore │ └── Cargo.toml ├── openapi-schema-from-into │ ├── .gitignore │ ├── Cargo.toml │ ├── openapi.json.sample │ └── src │ │ └── main.rs ├── worker-with-global-bindings │ ├── .gitignore │ ├── migrations │ │ └── 0001_schema.sql │ ├── package.json │ ├── wrangler.toml │ └── Cargo.toml ├── worker-bindings │ ├── .cargo │ │ └── config.toml │ ├── dummy_env_test.js │ ├── Cargo.toml │ └── wrangler.toml ├── worker-bindings-jsonc │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ └── wrangler.jsonc ├── worker-with-openapi │ ├── .cargo │ │ └── config.toml │ ├── .gitignore │ ├── package.json │ ├── wrangler.toml.sample │ ├── migrations │ │ └── 0001_schema.sql │ ├── Cargo.toml │ └── src │ │ ├── error.rs │ │ ├── model.rs │ │ └── fang.rs ├── worker-durable-websocket │ ├── .cargo │ │ └── config.toml │ ├── package.json │ ├── Cargo.toml │ ├── wrangler.toml │ └── src │ │ ├── lib.rs │ │ └── room.rs ├── petstore │ ├── .gitignore │ ├── Cargo.toml │ ├── client │ │ ├── package.json │ │ └── src │ │ │ └── main.ts │ └── README.md ├── realworld │ ├── README.md │ ├── .env │ ├── migrations │ │ ├── 20240108165911_add_password_of_users.up.sql │ │ ├── 20240114131044_add_unique_on_users_name_email.up.sql │ │ └── 20240108163944_create_tables.up.sql │ ├── Cargo.toml │ ├── src │ │ ├── handlers.rs │ │ ├── handlers │ │ │ ├── tags.rs │ │ │ ├── users.rs │ │ │ └── user.rs │ │ ├── main.rs │ │ ├── config.rs │ │ ├── models │ │ │ └── response.rs │ │ ├── fangs.rs │ │ ├── errors.rs │ │ └── models.rs │ └── docker-compose.yml ├── issue_550_glommio │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── issue_459 │ ├── src │ │ └── main.rs │ ├── Cargo.toml │ └── README.md └── test.sh ├── .gitignore ├── benches ├── .gitignore ├── src │ ├── request_headers.rs │ ├── response_headers.rs │ ├── lib.rs │ ├── request_headers │ │ ├── fxmap.rs │ │ └── headerhashmap.rs │ └── response_headers │ │ └── fxmap.rs ├── .vscode │ └── settings.json ├── benches │ └── imf_fixdate.rs └── Cargo.toml ├── benches_rt ├── .gitignore ├── Cargo.toml ├── vs_actix-web │ ├── Cargo.toml │ └── src │ │ └── bin │ │ └── param.rs ├── nio │ ├── src │ │ └── bin │ │ │ └── param.rs │ └── Cargo.toml ├── smol │ ├── src │ │ └── bin │ │ │ └── param.rs │ └── Cargo.toml ├── compio │ ├── src │ │ └── bin │ │ │ └── param.rs │ └── Cargo.toml ├── tokio │ ├── src │ │ └── bin │ │ │ ├── sleep.rs │ │ │ ├── param.rs │ │ │ ├── hello.rs │ │ │ └── headers.rs │ └── Cargo.toml ├── glommio │ ├── Cargo.toml │ └── src │ │ └── bin │ │ └── param.rs └── monoio │ ├── Cargo.toml │ └── src │ └── bin │ └── param.rs ├── ohkami └── src │ ├── router │ ├── mod.rs │ └── util.rs │ ├── header │ ├── mod.rs │ ├── append.rs │ └── qvalue.rs │ ├── fang │ ├── builtin.rs │ ├── bound.rs │ └── builtin │ │ └── context.rs │ ├── request │ ├── _test_headers.rs │ ├── from_request.rs │ └── context.rs │ ├── claw │ ├── content │ │ ├── html.rs │ │ ├── multipart.rs │ │ ├── json.rs │ │ ├── text.rs │ │ └── urlencoded.rs │ └── mod.rs │ ├── ws │ └── mod.rs │ └── response │ ├── into_response.rs │ └── content.rs ├── ohkami_macros ├── src │ ├── openapi │ │ ├── attributes │ │ │ ├── serde.rs │ │ │ └── serde │ │ │ │ └── case.rs │ │ └── attributes.rs │ ├── serde.rs │ ├── worker │ │ └── wrangler.rs │ └── from_request.rs └── Cargo.toml ├── ohkami_lib ├── src │ ├── lib.rs │ ├── percent_encoding.rs │ ├── serde_cookie.rs │ ├── serde_cookie │ │ └── _test.rs │ ├── serde_urlencoded.rs │ ├── num.rs │ ├── serde_utf8 │ │ └── _test.rs │ ├── serde_multipart │ │ └── de.rs │ └── serde_multipart.rs └── Cargo.toml ├── ohkami_openapi ├── Cargo.toml └── src │ └── _test.rs ├── Cargo.toml └── LICENSE /examples/jwt/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kanarus 2 | -------------------------------------------------------------------------------- /examples/websocket/.gitignore: -------------------------------------------------------------------------------- 1 | /*.pem 2 | -------------------------------------------------------------------------------- /samples/openapi-tags/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json -------------------------------------------------------------------------------- /samples/streaming/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json -------------------------------------------------------------------------------- /samples/readme-openapi/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/Cargo.lock 3 | /memo.md -------------------------------------------------------------------------------- /samples/openapi-schema-enums/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json -------------------------------------------------------------------------------- /samples/openapi-schema-from-into/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json -------------------------------------------------------------------------------- /examples/jwt/.env.sample: -------------------------------------------------------------------------------- 1 | JWT_SECRET=your-jwt-secret-key 2 | -------------------------------------------------------------------------------- /benches/.gitignore: -------------------------------------------------------------------------------- 1 | *flamegraph.svg 2 | perf.data 3 | perf.data.old -------------------------------------------------------------------------------- /benches_rt/.gitignore: -------------------------------------------------------------------------------- 1 | *flamegraph.svg 2 | perf.data 3 | perf.data.old -------------------------------------------------------------------------------- /samples/worker-with-global-bindings/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock -------------------------------------------------------------------------------- /examples/static_files/public/index.js: -------------------------------------------------------------------------------- 1 | window.alert('Hello, Ohkami!'); 2 | -------------------------------------------------------------------------------- /benches/src/request_headers.rs: -------------------------------------------------------------------------------- 1 | pub mod fxmap; 2 | pub mod headerhashmap; 3 | -------------------------------------------------------------------------------- /examples/static_files/public/.env.sample: -------------------------------------------------------------------------------- 1 | WHATS_THIS='A sample .env file' 2 | -------------------------------------------------------------------------------- /samples/worker-bindings/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /benches/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.showUnlinkedFileNotification": false 3 | } -------------------------------------------------------------------------------- /samples/worker-bindings-jsonc/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /samples/worker-durable-websocket/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /benches/src/response_headers.rs: -------------------------------------------------------------------------------- 1 | pub mod fxmap; 2 | pub mod heap_ohkami_headers; 3 | pub mod heap_ohkami_headers_nosize; -------------------------------------------------------------------------------- /samples/petstore/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json 2 | client/package-lock.json 3 | client/node_modules 4 | client/openapi.d.ts -------------------------------------------------------------------------------- /samples/realworld/README.md: -------------------------------------------------------------------------------- 1 | An implementation of [RealWorld example apps](https://github.com/gothinkster/realworld) 2 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .wrangler 3 | package-lock.json 4 | wrangler.toml 5 | openapi.json -------------------------------------------------------------------------------- /examples/static_files/public/sub.js.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohkami-rs/ohkami/HEAD/examples/static_files/public/sub.js.br -------------------------------------------------------------------------------- /examples/static_files/public/sub.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohkami-rs/ohkami/HEAD/examples/static_files/public/sub.js.gz -------------------------------------------------------------------------------- /examples/static_files/public/blog/second.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohkami-rs/ohkami/HEAD/examples/static_files/public/blog/second.html.gz -------------------------------------------------------------------------------- /benches/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod light_ohkami; 2 | pub mod header_map; 3 | pub mod header_hashbrown; 4 | pub mod request_headers; 5 | pub mod response_headers; 6 | -------------------------------------------------------------------------------- /ohkami/src/router/mod.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "__rt__")] 2 | 3 | pub(crate) mod base; 4 | pub(crate) mod r#final; 5 | pub(crate) mod segments; 6 | mod util; 7 | -------------------------------------------------------------------------------- /samples/worker-with-global-bindings/migrations/0001_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id INTEGER NOT NULL PRIMARY KEY, 3 | name TEXT NOT NULL UNIQUE, 4 | age INTEGER 5 | ); 6 | -------------------------------------------------------------------------------- /examples/quick_start/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quick_start" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | -------------------------------------------------------------------------------- /benches_rt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["*"] 4 | exclude = ["target"] 5 | 6 | [profile.release] 7 | lto = true 8 | panic = "abort" 9 | codegen-units = 1 10 | -------------------------------------------------------------------------------- /examples/jwt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jwt" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | dotenvy = "0.15" 10 | -------------------------------------------------------------------------------- /samples/realworld/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL='postgres://ohkami:password@localhost:5432/realworld?sslmode=disable' 2 | JWT_SECRET_KEY='ohkami-realworld-jwt-authorization-secret-key' 3 | PEPPER='ohkami-realworld-password-secret-guard-pepper' -------------------------------------------------------------------------------- /samples/realworld/migrations/20240108165911_add_password_of_users.up.sql: -------------------------------------------------------------------------------- 1 | -- Add up migration script here 2 | ALTER TABLE users ADD COLUMN password varchar(128) NOT NULL; 3 | ALTER TABLE users ADD COLUMN salt varchar(128) NOT NULL; -------------------------------------------------------------------------------- /benches_rt/vs_actix-web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches_vs_actix-web" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | actix-web = { version = "4" } 9 | -------------------------------------------------------------------------------- /examples/json_response/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "json_response" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | ohkami = { workspace = true } 9 | tokio = { workspace = true } -------------------------------------------------------------------------------- /benches_rt/nio/src/bin/param.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | #[nio::main] 4 | async fn main() { 5 | Ohkami::new(( 6 | "/user/:id" 7 | .GET(|Path(id): Path| async {id}), 8 | )).howl("0.0.0.0:3000").await 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic_auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic_auth" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | 10 | [features] 11 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /samples/realworld/migrations/20240114131044_add_unique_on_users_name_email.up.sql: -------------------------------------------------------------------------------- 1 | -- Add up migration script here 2 | ALTER TABLE users ADD CONSTRAINT users_name_is_unique UNIQUE (name); 3 | ALTER TABLE users ADD CONSTRAINT users_email_is_unique UNIQUE (email); -------------------------------------------------------------------------------- /examples/uibeam/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uibeam" 3 | version = "0.0.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | uibeam = { version = "0.4", default-features = false } 10 | -------------------------------------------------------------------------------- /ohkami_macros/src/openapi/attributes/serde.rs: -------------------------------------------------------------------------------- 1 | mod attributes; 2 | mod case; 3 | mod value; 4 | 5 | pub(super) use attributes::{ContainerAttributes, FieldAttributes, VariantAttributes}; 6 | pub(super) use case::Case; 7 | pub(super) use value::{EqValue, Separatable}; 8 | -------------------------------------------------------------------------------- /examples/derive_from_request/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "derive_from_request" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } -------------------------------------------------------------------------------- /samples/issue_550_glommio/README.md: -------------------------------------------------------------------------------- 1 | # A regression sample to reproduce issue #550 2 | 3 | To test [issue #550](https://github.com/ohkami-rs/ohkami/issues/550), run `cargo run`. 4 | 5 | As for v0.24.0, this fails to compile as described in the issue. 6 | v0.24.1 fixes it. 7 | -------------------------------------------------------------------------------- /examples/sse/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sse" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | futures-util = { version = "0.3" } 10 | 11 | [features] 12 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /examples/chatgpt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chatgpt" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | reqwest = { version = "0.12", features = ["json", "stream"] } 10 | 11 | [features] 12 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /examples/multiple-single-threads/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiple-single-threads" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | ohkami = { workspace = true } 9 | tokio = { workspace = true } 10 | 11 | [features] 12 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /samples/openapi-tags/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openapi-tags" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio", "openapi"] } 9 | -------------------------------------------------------------------------------- /samples/openapi-schema-enums/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openapi-schema-enums" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio", "openapi"] } 9 | -------------------------------------------------------------------------------- /samples/openapi-schema-from-into/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openapi-schema-from-into" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio", "openapi"] } 9 | -------------------------------------------------------------------------------- /benches_rt/smol/src/bin/param.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | 4 | #[inline(always)] 5 | async fn echo_id(Path(id): Path) -> String { 6 | id 7 | } 8 | 9 | fn main() { 10 | smol::block_on({ 11 | Ohkami::new(( 12 | "/user/:id" 13 | .GET(echo_id), 14 | )).howl("0.0.0.0:3000") 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /benches/benches/imf_fixdate.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use ohkami_lib::time::UTCDateTime; 5 | use ohkami::util::unix_timestamp; 6 | 7 | 8 | #[bench] fn format_imf_fixdate(b: &mut test::Bencher) { 9 | b.iter(|| { 10 | UTCDateTime::from_unix_timestamp(unix_timestamp()) 11 | .into_imf_fixdate(); 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /examples/static_files/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static_files" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [features] 7 | DEBUG = ["ohkami/DEBUG"] 8 | 9 | [dependencies] 10 | ohkami = { workspace = true } 11 | tokio = { workspace = true } 12 | tracing = { workspace = true } 13 | tracing-subscriber = { workspace = true } -------------------------------------------------------------------------------- /examples/static_files/public/blog/second.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Second article 7 | 8 | 9 |

Second article

10 |

Hi everyone, This is my second blog post.

11 | 12 | -------------------------------------------------------------------------------- /samples/issue_459/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::{Ohkami, Route}; 2 | 3 | async fn large_response() -> String { 4 | (1..=100000) 5 | .map(|i| format!("This is line #{i}\n")) 6 | .collect::() 7 | } 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | Ohkami::new(( 12 | "/".GET(large_response), 13 | )).howl("localhost:3000").await 14 | } 15 | -------------------------------------------------------------------------------- /samples/readme-openapi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme-openapi" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio", "openapi"] } 9 | tokio = { version = "1", features = ["full"] } -------------------------------------------------------------------------------- /samples/worker-bindings/dummy_env_test.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { join } from 'node:path'; 4 | import { cwd, exit } from 'node:process'; 5 | 6 | const wasmpack_js = await import(join(cwd(), `pkg`, `worker_bindings_test.js`)); 7 | if (!wasmpack_js) { 8 | exit("wasmpack_js is not found") 9 | } 10 | 11 | wasmpack_js.handle_dummy_env(); 12 | 13 | console.log("ok"); 14 | -------------------------------------------------------------------------------- /benches_rt/compio/src/bin/param.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | 4 | #[inline(always)] 5 | async fn echo_id(Path(id): Path) -> String { 6 | id 7 | } 8 | 9 | fn main() { 10 | compio::runtime::Runtime::new().unwrap().block_on({ 11 | Ohkami::new(( 12 | "/user/:id" 13 | .GET(echo_id), 14 | )).howl("0.0.0.0:3000") 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /examples/static_files/public/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | About 7 | 8 | 9 |

About this site

10 |

This is a demo site of ohkami web framework's static directory serving.

11 | 12 | -------------------------------------------------------------------------------- /benches_rt/tokio/src/bin/sleep.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | async fn sleeping_hello(Path(secs): Path) -> &'static str { 4 | tokio::time::sleep(std::time::Duration::from_secs(secs)).await; 5 | "Hello, sleep!" 6 | } 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | Ohkami::new(( 11 | "/sleep/:secs".GET(sleeping_hello), 12 | )).howl("localhost:8888").await 13 | } 14 | -------------------------------------------------------------------------------- /examples/static_files/public/blog/first.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My first article 7 | 8 | 9 |

My first article

10 |

Hi, this is my first article, served by ohkami web framework.

11 | 12 | -------------------------------------------------------------------------------- /examples/hello/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | tracing = { workspace = true } 10 | tracing-subscriber = { workspace = true } 11 | 12 | [features] 13 | nightly = ["ohkami/nightly"] 14 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /examples/form/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "form" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [features] 7 | DEBUG = ["ohkami/DEBUG"] 8 | 9 | [dependencies] 10 | ohkami = { workspace = true } 11 | tokio = { workspace = true } 12 | tracing = { workspace = true } 13 | tracing-subscriber = { workspace = true } 14 | -------------------------------------------------------------------------------- /samples/issue_550_glommio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "issue_550_glommio" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_glommio"] } 9 | glommio = "0.9" 10 | 11 | [features] 12 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /samples/worker-with-openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ohkami-worker-with-openapi", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "export OHKAMI_WORKER_DEV='' && wrangler deploy", 7 | "dev": "export OHKAMI_WORKER_DEV=1 && wrangler dev", 8 | "openapi": "node ../../scripts/workers_openapi.js" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "^3.50" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/static_files/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | This is index page ! 7 | 8 | 9 |

ohkami

10 |

This page is served by Ohkami web framework.

11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/worker-durable-websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ohkami-worker-durable-websocket", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "export OHKAMI_WORKER_DEV='' && wrangler deploy", 7 | "dev": "export OHKAMI_WORKER_DEV=1 && wrangler dev", 8 | "openapi": "node ../../scripts/workers_openapi.js" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "^3.50" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/worker-with-global-bindings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ohkami-with-global-bindings", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "export OHKAMI_WORKER_DEV='' && wrangler deploy", 7 | "dev": "export OHKAMI_WORKER_DEV=1 && wrangler dev", 8 | "openapi": "node ../../scripts/workers_openapi.js" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "^3.50" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/issue_459/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "issue_459" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio"] } 9 | tokio = { version = "1", features = ["full"] } 10 | 11 | [features] 12 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /benches_rt/vs_actix-web/src/bin/param.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, App, HttpServer}; 2 | 3 | #[get("/user/{id}")] 4 | async fn echo(id: web::Path) -> String { 5 | id.into_inner() 6 | } 7 | 8 | #[actix_web::main] 9 | async fn main() -> std::io::Result<()> { 10 | HttpServer::new(|| { 11 | App::new() 12 | .service(echo) 13 | }) 14 | .bind("0.0.0.0:3000")? 15 | .run().await 16 | } 17 | -------------------------------------------------------------------------------- /samples/issue_459/README.md: -------------------------------------------------------------------------------- 1 | # A minimal sample to reproduce issue #459 2 | 3 | To test [issue #459](https://github.com/ohkami-rs/ohkami/issues/459), run `cargo run`, and in another terminal: 4 | 5 | ```sh 6 | timeout -sKILL 0.01 curl localhost:5000 7 | ``` 8 | 9 | As for v0.23.3, this will cause server panic, and may lead to `process didn't exit successfully`. 10 | 11 | v0.23.4 fixes the behavior to safely print warnings. 12 | -------------------------------------------------------------------------------- /samples/petstore/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petstore" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio", "openapi"] } 9 | tokio = { version = "1", features = ["full"] } 10 | 11 | [features] 12 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /samples/streaming/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petstore" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio", "sse", "openapi"] } 9 | tokio = { version = "1", features = ["full"] } 10 | 11 | [features] 12 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /benches/src/request_headers/fxmap.rs: -------------------------------------------------------------------------------- 1 | use ohkami_lib::{CowSlice, Slice}; 2 | use rustc_hash::FxHashMap; 3 | 4 | 5 | pub struct FxMap(FxHashMap< 6 | Slice, CowSlice 7 | >); 8 | 9 | impl FxMap { 10 | pub fn new() -> Self { 11 | Self(FxHashMap::default()) 12 | } 13 | 14 | #[inline(always)] 15 | pub fn insert(&mut self, key: Slice, value: CowSlice) { 16 | self.0.insert(key, value); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ohkami/src/header/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | mod append; 4 | pub(crate) use append::Append; 5 | pub use append::append; 6 | 7 | mod etag; 8 | pub use etag::ETag; 9 | 10 | mod encoding; 11 | pub use encoding::{AcceptEncoding, CompressionEncoding, Encoding}; 12 | 13 | mod qvalue; 14 | pub use qvalue::QValue; 15 | 16 | mod setcookie; 17 | pub(crate) use setcookie::*; 18 | 19 | mod map; 20 | pub(crate) use map::ByteArrayMap; 21 | -------------------------------------------------------------------------------- /samples/worker-durable-websocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "worker-with-openapi" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 11 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_worker", "ws"] } 12 | worker = { version = "0.7" } 13 | -------------------------------------------------------------------------------- /ohkami/src/fang/builtin.rs: -------------------------------------------------------------------------------- 1 | mod basicauth; 2 | pub use basicauth::BasicAuth; 3 | 4 | mod cors; 5 | pub use cors::Cors; 6 | 7 | mod csrf; 8 | pub use csrf::Csrf; 9 | 10 | mod jwt; 11 | pub use jwt::{Jwt, JwtToken}; 12 | 13 | mod context; 14 | pub use context::Context; 15 | 16 | pub mod enamel; 17 | pub use enamel::Enamel; 18 | 19 | #[cfg(feature = "__rt_native__")] 20 | mod timeout; 21 | #[cfg(feature = "__rt_native__")] 22 | pub use timeout::Timeout; 23 | -------------------------------------------------------------------------------- /benches_rt/nio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches-with-nio" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 9 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_nio"] } 10 | nio = { version = "0.0" } 11 | 12 | [features] 13 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /benches_rt/smol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches-with-smol" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 9 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_smol"] } 10 | smol = { version = "2" } 11 | 12 | [features] 13 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /examples/websocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "websocket" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { workspace = true } 8 | tokio = { workspace = true } 9 | rustls = { optional = true, version = "0.23", features = ["ring"] } 10 | rustls-pemfile = { optional = true, version = "2.2" } 11 | 12 | [features] 13 | tls = ["ohkami/tls", "dep:rustls", "dep:rustls-pemfile"] 14 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /benches_rt/glommio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches-with-glommio" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 9 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_glommio"] } 10 | glommio = { version = "0.9" } 11 | 12 | [features] 13 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /benches_rt/tokio/src/bin/param.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | fn main() { 4 | tokio::runtime::Builder::new_multi_thread() 5 | .enable_all() 6 | .event_interval(11) 7 | .global_queue_interval(31) 8 | .build() 9 | .expect("Failed building the Runtime") 10 | .block_on(Ohkami::new(( 11 | "/user/:id" 12 | .GET(|Path(id): Path| async {id}), 13 | )).howl("0.0.0.0:3000")) 14 | } 15 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["*"] 4 | exclude = ["target"] 5 | 6 | [workspace.dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../ohkami", default-features = false, features = ["rt_tokio", "sse", "ws"] } 9 | tokio = { version = "1", features = ["full"] } 10 | tracing = "0.1" 11 | tracing-subscriber = "0.3" 12 | -------------------------------------------------------------------------------- /benches_rt/compio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches-with-compio" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 9 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_compio"] } 10 | compio = { version = "0.15" } 11 | 12 | [features] 13 | DEBUG = ["ohkami/DEBUG"] 14 | -------------------------------------------------------------------------------- /benches_rt/monoio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches-with-monoio" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 9 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_monoio"] } 10 | monoio = { version = "0.2" } 11 | 12 | [features] 13 | DEBUG = ["ohkami/DEBUG"] 14 | -------------------------------------------------------------------------------- /benches_rt/tokio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches-with-tokio" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [dependencies] 8 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 9 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio"] } 10 | tokio = { version = "1", features = ["full"] } 11 | 12 | [features] 13 | DEBUG = ["ohkami/DEBUG"] -------------------------------------------------------------------------------- /benches_rt/tokio/src/bin/hello.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::claw::{Path, Json}; 3 | 4 | #[derive(Serialize)] 5 | struct Message { 6 | message: String 7 | } 8 | 9 | async fn hello(Path(name): Path<&str>) -> Json { 10 | Json(Message { 11 | message: format!("Hello, {name}!") 12 | }) 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | Ohkami::new(( 18 | "/hello/:name".GET(hello), 19 | )).howl("localhost:3000").await 20 | } 21 | -------------------------------------------------------------------------------- /samples/worker-bindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "worker-bindings-test" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 11 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_worker"] } 12 | worker = { version = "0.7", features = ["queue", "d1"] } 13 | console_error_panic_hook = "0.1" 14 | -------------------------------------------------------------------------------- /samples/worker-bindings-jsonc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "worker-bindings-test-jsonc" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 11 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_worker"] } 12 | worker = { version = "0.7", features = ["queue", "d1"] } 13 | console_error_panic_hook = "0.1" 14 | -------------------------------------------------------------------------------- /examples/static_files/public/blog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Blog home 7 | 8 | 9 |

Well come to my blog!

10 |

articles

11 | 15 | 16 | -------------------------------------------------------------------------------- /examples/quick_start/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::{Ohkami, Route}; 2 | use ohkami::claw::{status, Path}; 3 | 4 | async fn health_check() -> status::NoContent { 5 | status::NoContent 6 | } 7 | 8 | async fn hello(Path(name): Path<&str>) -> String { 9 | format!("Hello, {name}!") 10 | } 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | Ohkami::new(( 15 | "/healthz" 16 | .GET(health_check), 17 | "/hello/:name" 18 | .GET(hello), 19 | )).howl("localhost:3000").await 20 | } 21 | -------------------------------------------------------------------------------- /examples/basic_auth/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::fang::BasicAuth; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let private_ohkami = Ohkami::new(( 7 | BasicAuth { 8 | username: "master of hello", 9 | password: "world" 10 | }, 11 | "/hello".GET(|| async {"Hello, private :)"}) 12 | )); 13 | 14 | Ohkami::new(( 15 | "/hello".GET(|| async {"Hello, public!"}), 16 | "/private".By(private_ohkami) 17 | )).howl("localhost:8888").await 18 | } 19 | -------------------------------------------------------------------------------- /examples/websocket/README.md: -------------------------------------------------------------------------------- 1 | # WebSocket Example 2 | 3 | ## Feature flags description 4 | 5 | - `DEBUG`: Enables Ohkami's debug logging. 6 | - `tls`: Enables TLS support ( https://, wss:// ). 7 | 8 | ## Prerequisites 9 | 10 | If you want to run this example with TLS support, you need to have 11 | [`mkcert`](https://github.com/FiloSottile/mkcert) and run: 12 | 13 | ```sh 14 | # assuming you have mkcert installed and `mkcert -install` has already executed: 15 | mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 16 | ``` 17 | -------------------------------------------------------------------------------- /ohkami_lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod map; 2 | pub use map::TupleMap; 3 | 4 | pub mod num; 5 | 6 | pub mod time; 7 | pub use time::imf_fixdate; 8 | 9 | mod slice; 10 | pub use slice::{CowSlice, Slice}; 11 | 12 | mod percent_encoding; 13 | pub use percent_encoding::{percent_decode, percent_decode_utf8, percent_encode}; 14 | 15 | pub mod serde_cookie; 16 | pub mod serde_multipart; 17 | pub mod serde_urlencoded; 18 | pub mod serde_utf8; 19 | 20 | #[cfg(feature = "stream")] 21 | pub mod stream; 22 | #[cfg(feature = "stream")] 23 | pub use stream::{Stream, StreamExt}; 24 | -------------------------------------------------------------------------------- /samples/worker-durable-websocket/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "ohkami-worker-durable-websocket" 2 | main = "build/worker/shim.mjs" 3 | compatibility_date = "2024-04-19" 4 | 5 | # `worker-build` and `wasm-pack` is required 6 | # (run `cargo install wasm-pack worker-build` to install) 7 | 8 | [build] 9 | command = "test $OHKAMI_WORKER_DEV && worker-build --dev || worker-build -- --no-default-features" 10 | 11 | [[durable_objects.bindings]] 12 | name = "ROOMS" 13 | class_name = "Room" 14 | 15 | [[migrations]] 16 | tag = "v1" 17 | new_classes = ["Room"] 18 | -------------------------------------------------------------------------------- /examples/form/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Form example 7 | 8 | 9 |

Form example

10 |
11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/wrangler.toml.sample: -------------------------------------------------------------------------------- 1 | name = "ohkami-worker-with-openapi" 2 | main = "build/worker/shim.mjs" 3 | compatibility_date = "2024-04-19" 4 | 5 | # `worker-build` and `wasm-pack` is required 6 | # (run `cargo install wasm-pack worker-build` to install) 7 | 8 | [build] 9 | command = "test $OHKAMI_WORKER_DEV && worker-build --dev || worker-build -- --no-default-features" 10 | 11 | [vars] 12 | OPENAPI_DOC_PASSWORD = "openapi" 13 | 14 | [[d1_databases]] 15 | binding = "DB" 16 | database_name = "db" 17 | database_id = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 18 | -------------------------------------------------------------------------------- /samples/worker-with-global-bindings/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "worker-bindings-test" 2 | main = "build/worker/shim.mjs" 3 | compatibility_date = "2025-02-26" 4 | 5 | # `worker-build` and `wasm-pack` is required 6 | # (run `cargo install wasm-pack worker-build` to install) 7 | 8 | [build] 9 | command = "test $OHKAMI_WORKER_DEV && worker-build --dev || worker-build -- --no-default-features" 10 | 11 | [[d1_databases]] 12 | binding = "DB" 13 | database_name = "db" 14 | database_id = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 15 | 16 | [[kv_namespaces]] 17 | binding = "MY_KV" 18 | id = "" 19 | -------------------------------------------------------------------------------- /samples/petstore/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "license": "ISC", 6 | "author": "", 7 | "type": "module", 8 | "scripts": { 9 | "main": "node --experimental-strip-types --no-warnings=ExperimentalWarning src/main.ts", 10 | "gen": "npx openapi-typescript ../openapi.json -o ./openapi.d.ts" 11 | }, 12 | "dependencies": { 13 | "openapi-fetch": "^0.13.4" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^22.10.5", 17 | "openapi-typescript": "^7.5.2", 18 | "typescript": "^5.7.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ohkami_lib/src/percent_encoding.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, str::Utf8Error}; 2 | 3 | #[inline(always)] 4 | pub fn percent_decode_utf8(input: &[u8]) -> Result, Utf8Error> { 5 | ::percent_encoding::percent_decode(input).decode_utf8() 6 | } 7 | #[inline(always)] 8 | pub fn percent_decode(input: &[u8]) -> Cow<'_, [u8]> { 9 | ::percent_encoding::percent_decode(input).into() 10 | } 11 | 12 | #[inline(always)] 13 | pub fn percent_encode(input: &str) -> Cow<'_, str> { 14 | ::percent_encoding::percent_encode(input.as_bytes(), ::percent_encoding::NON_ALPHANUMERIC) 15 | .into() 16 | } 17 | -------------------------------------------------------------------------------- /examples/sse/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::sse::DataStream; 3 | use tokio::time::{sleep, Duration}; 4 | 5 | async fn handler() -> DataStream { 6 | DataStream::new(|mut s| async move { 7 | s.send("starting streaming..."); 8 | for i in 1..=5 { 9 | sleep(Duration::from_secs(1)).await; 10 | s.send(format!("MESSAGE #{i}")); 11 | } 12 | s.send("streaming finished!"); 13 | }) 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | Ohkami::new(( 19 | "/sse".GET(handler), 20 | )).howl("localhost:3020").await 21 | } 22 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/migrations/0001_schema.sql: -------------------------------------------------------------------------------- 1 | -- Migration number: 0001 2025-01-15T23:14:42.890Z 2 | 3 | CREATE TABLE IF NOT EXISTS users ( 4 | id integer NOT NULL PRIMARY KEY, 5 | token text NOT NULL, -- just for demo 6 | name text NOT NULL, 7 | location text, 8 | age integer, 9 | 10 | UNIQUE (name, token) 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS tweets ( 14 | id integer NOT NULL PRIMARY KEY, 15 | user_id integer NOT NULL, 16 | content text NOT NULL, 17 | posted_at text NOT NULL -- unix timestamp as text 18 | ); 19 | -------------------------------------------------------------------------------- /benches_rt/glommio/src/bin/param.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use glommio::{LocalExecutorPoolBuilder, PoolPlacement, CpuSet}; 3 | 4 | 5 | #[inline(always)] 6 | async fn echo_id(Path(id): Path) -> String { 7 | id 8 | } 9 | 10 | fn main() { 11 | LocalExecutorPoolBuilder::new(PoolPlacement::MaxSpread( 12 | dbg!(std::thread::available_parallelism().map_or(1, |n| n.get())), 13 | dbg!(CpuSet::online().ok()) 14 | )).on_all_shards(|| { 15 | Ohkami::new(( 16 | "/user/:id" 17 | .GET(echo_id), 18 | )).howl("0.0.0.0:3000") 19 | }).unwrap().join_all(); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/AutoApprove.yml: -------------------------------------------------------------------------------- 1 | # This will be removed when ohkami has more than one maintainers 2 | 3 | name: AutoApprove 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | - ready_for_review 12 | 13 | jobs: 14 | approve: 15 | if: | 16 | github.event.pull_request.user.login == 'kanarus' && 17 | !github.event.pull_request.draft 18 | permissions: 19 | pull-requests: write 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: hmarr/auto-approve-action@v4 23 | with: 24 | github-token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /samples/worker-durable-websocket/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod room; 2 | 3 | use ohkami::prelude::*; 4 | use ohkami::ws::{WebSocketContext, WebSocket}; 5 | 6 | #[ohkami::bindings] 7 | struct Bindings; 8 | 9 | #[ohkami::worker] 10 | async fn main() -> Ohkami { 11 | Ohkami::new(( 12 | "/ws/:room_name".GET(ws_chatroom), 13 | )) 14 | } 15 | 16 | async fn ws_chatroom( 17 | Path(room_name): Path<&str>, 18 | ctx: WebSocketContext<'_>, 19 | Bindings { ROOMS, .. }: Bindings, 20 | ) -> Result { 21 | let room = ROOMS 22 | .id_from_name(room_name)? 23 | .get_stub()?; 24 | ctx.upgrade_durable(room).await 25 | } 26 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "worker-with-openapi" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_worker"] } 9 | worker = { version = "0.7", features = ["d1"] } 10 | thiserror = "1.0" 11 | console_error_panic_hook = "0.1" 12 | 13 | [lib] 14 | crate-type = ["cdylib", "rlib"] 15 | 16 | [profile.release] 17 | opt-level = "s" 18 | 19 | [features] 20 | default = ["openapi"] 21 | openapi = ["ohkami/openapi"] 22 | DEBUG = ["ohkami/DEBUG"] 23 | -------------------------------------------------------------------------------- /samples/petstore/README.md: -------------------------------------------------------------------------------- 1 | # Example Project for Ohkami's `openapi` feature 2 | 3 | ## Setup 4 | 5 | - Recent Rust toolchain 6 | - Node.js >= 22.6.0 ( for `--experimental-strip-types` ) 7 | 8 | ## How to play 9 | 10 | First, run the Ohkami app: 11 | 12 | ```sh 13 | cargo run 14 | ``` 15 | 16 | Then you'll see `openapi.json` generated at the project root! 17 | 18 | Now you can fetch the Ohkami in a type-safe way: 19 | 20 | ```sh 21 | # (another terminal window) 22 | 23 | cd client 24 | npm install 25 | 26 | # generate type definitions from openapi.json 27 | npm run gen 28 | 29 | # run client app to perform type-safe interaction with Ohkami 30 | npm run main 31 | ``` 32 | -------------------------------------------------------------------------------- /samples/issue_550_glommio/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use glommio::{LocalExecutorPoolBuilder, PoolPlacement, CpuSet, executor}; 3 | 4 | async fn echo_id(Path(id): Path) -> String { 5 | let executor = executor(); 6 | executor.spawn_blocking(move || id).await 7 | } 8 | 9 | fn main() { 10 | LocalExecutorPoolBuilder::new(PoolPlacement::MaxSpread( 11 | dbg!(std::thread::available_parallelism().map_or(1, |n| n.get())), 12 | dbg!(CpuSet::online().ok()) 13 | )).on_all_shards(|| { 14 | Ohkami::new(( 15 | "/user/:id" 16 | .GET(echo_id), 17 | )).howl("0.0.0.0:3000") 18 | }).unwrap().join_all(); 19 | } 20 | -------------------------------------------------------------------------------- /samples/realworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "realworld" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_tokio"] } 8 | tokio = { version = "1", features = ["full"] } 9 | tracing = "0.1" 10 | tracing-subscriber = "0.3" 11 | sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "macros", "chrono", "uuid"] } 12 | chrono = "0.4" 13 | dotenvy = "0.15" 14 | argon2 = "0.5" 15 | uuid = { version = "1.18", features = ["serde", "v4"] } 16 | 17 | [features] 18 | openapi = ["ohkami/openapi"] -------------------------------------------------------------------------------- /samples/worker-with-global-bindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "worker-with-global-bindings" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | # set `default-features = false` to assure "DEBUG" feature be off even when DEBUGing `../ohkami` 8 | ohkami = { path = "../../ohkami", default-features = false, features = ["rt_worker"] } 9 | worker = { version = "0.7", features = ["d1"] } 10 | thiserror = "1.0" 11 | console_error_panic_hook = "0.1" 12 | 13 | [lib] 14 | crate-type = ["cdylib", "rlib"] 15 | 16 | [profile.release] 17 | opt-level = "s" 18 | 19 | [features] 20 | openapi = ["ohkami/openapi"] 21 | 22 | # `--no-default-features` in release profile 23 | default = ["openapi"] -------------------------------------------------------------------------------- /samples/realworld/src/handlers.rs: -------------------------------------------------------------------------------- 1 | mod users; 2 | mod user; 3 | mod profiles; 4 | mod articles; 5 | mod tags; 6 | 7 | use sqlx::PgPool; 8 | use ohkami::{Ohkami, Route, fang::Context}; 9 | use crate::fangs::Logger; 10 | 11 | pub fn realworld_ohkami( 12 | pool: PgPool, 13 | ) -> Ohkami { 14 | Ohkami::new( 15 | "/api".By(Ohkami::new(( 16 | Logger, 17 | Context::new(pool), 18 | "/users".By(users::users_ohkami()), 19 | "/user".By(user::user_ohkami()), 20 | "/profiles".By(profiles::profiles_ohkami()), 21 | "/articles".By(articles::articles_ohkami()), 22 | "/tags".By(tags::tags_ohkami()), 23 | )) 24 | )) 25 | } 26 | -------------------------------------------------------------------------------- /samples/realworld/src/handlers/tags.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use sqlx::PgPool; 3 | use crate::errors::RealWorldError; 4 | use crate::models::{Tag, response::ListOfTagsResponse}; 5 | 6 | 7 | pub fn tags_ohkami() -> Ohkami { 8 | Ohkami::new(( 9 | "/".GET(get), 10 | )) 11 | } 12 | 13 | async fn get( 14 | Context(pool): Context<'_, PgPool> 15 | ) -> Result>, RealWorldError> { 16 | let tags = sqlx::query!(r#" 17 | SELECT name 18 | FROM tags 19 | "#).fetch_all(pool).await 20 | .map_err(RealWorldError::DB)?.into_iter() 21 | .map(|n| Tag::new(n.name)).collect(); 22 | 23 | Ok(Json(ListOfTagsResponse { tags })) 24 | } 25 | -------------------------------------------------------------------------------- /samples/realworld/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:15-alpine 4 | container_name: realworld-postgres 5 | tty: true 6 | # command: postgres -c log_destination=stderr -c log_statement=all -c log_connections=on -c log_disconnections=on 7 | # logging: 8 | # options: 9 | # max-size: '10k' 10 | # max-file: '5' 11 | environment: 12 | POSTGRES_USER: ohkami 13 | POSTGRES_PASSWORD: password 14 | POSTGRES_PORT: 5432 15 | POSTGRES_DB: realworld 16 | PGSSLMODE: disable 17 | ports: 18 | - 5432:5432 19 | volumes: 20 | - realworld-data:/var/lib/postgresql/data 21 | 22 | volumes: 23 | realworld-data: 24 | name: realworld-data 25 | driver: local -------------------------------------------------------------------------------- /ohkami_macros/src/serde.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::Result; 4 | 5 | #[allow(non_snake_case)] 6 | pub(super) fn Serialize(data: TokenStream) -> Result { 7 | Ok(quote! { 8 | #[derive(::ohkami::__internal__::serde::Serialize)] 9 | #[serde(crate = "::ohkami::__internal__::serde")] 10 | #[::ohkami::__internal__::consume_struct] 11 | #data 12 | }) 13 | } 14 | 15 | #[allow(non_snake_case)] 16 | pub(super) fn Deserialize(data: TokenStream) -> Result { 17 | Ok(quote! { 18 | #[derive(::ohkami::__internal__::serde::Deserialize)] 19 | #[serde(crate = "::ohkami::__internal__::serde")] 20 | #[::ohkami::__internal__::consume_struct] 21 | #data 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /examples/chatgpt/src/error.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | #[derive(Debug)] 4 | pub enum Error { 5 | Fetch(reqwest::Error), 6 | } 7 | 8 | impl IntoResponse for Error { 9 | fn into_response(self) -> Response { 10 | println!("{self}"); 11 | match self { 12 | Self::Fetch(_) => Response::InternalServerError(), 13 | } 14 | } 15 | } 16 | 17 | const _: () = { 18 | impl std::error::Error for Error {} 19 | impl std::fmt::Display for Error { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | std::fmt::Debug::fmt(self, f) 22 | } 23 | } 24 | 25 | impl From for Error { 26 | fn from(e: reqwest::Error) -> Self { 27 | Self::Fetch(e) 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /samples/worker-bindings/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "worker-bindings-test" 2 | 3 | [vars] 4 | VARIABLE_1 = "hoge" 5 | VARIABLE_2 = "super fun" 6 | 7 | [ai] 8 | binding = "INTELIGENT" 9 | 10 | [[d1_databases]] 11 | binding = "DB" 12 | database_name = "db" 13 | database_id = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 14 | 15 | [[kv_namespaces]] 16 | binding = "MY_KVSTORE" 17 | id = "" 18 | 19 | [[r2_buckets]] 20 | binding = 'MY_BUCKET' 21 | bucket_name = '' 22 | 23 | [[services]] 24 | binding = "S" 25 | service = "" 26 | 27 | [[queues.producers]] 28 | queue = "my-queue" 29 | binding = "MY_QUEUE" 30 | 31 | [[durable_objects.bindings]] 32 | name = "RATE_LIMITER" 33 | class_name = "RateLimiter" 34 | 35 | [[hyperdrive]] 36 | binding = "HYPERDRIVE" 37 | id = "" 38 | -------------------------------------------------------------------------------- /benches/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_benches" 3 | version = "0.0.0" 4 | edition = "2024" 5 | authors = ["kanarus "] 6 | 7 | [features] 8 | DEBUG = ["ohkami/DEBUG"] 9 | # `--no-default-features` when running `bin/` 10 | default = ["DEBUG"] 11 | 12 | [dependencies] 13 | ohkami = { path = "../ohkami", default-features = false, features = ["rt_tokio"] } 14 | ohkami_lib = { path = "../ohkami_lib" } 15 | http = "1.0.0" 16 | rustc-hash = "1.1" 17 | bytes = "1.5.0" 18 | byte_reader = "3.0.0" 19 | 20 | tokio = { version = "1.37.0", features = ["full"] } 21 | tracing = "0.1.4" 22 | tracing-subscriber = "0.3.18" 23 | hashbrown = { version = "0.14.5", features = ["raw", "inline-more"] } 24 | 25 | [dev-dependencies] 26 | rand = "0.8.5" 27 | rand_chacha = "0.3.1" -------------------------------------------------------------------------------- /ohkami_openapi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_openapi" 3 | description = "OpenAPI types for Ohkami - A performant, declarative, and runtime-flexible web framework for Rust" 4 | documentation = "https://docs.rs/ohkami_openapi" 5 | version = { workspace = true } 6 | edition = { workspace = true } 7 | authors = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | readme = { workspace = true } 11 | keywords = { workspace = true } 12 | categories = { workspace = true } 13 | license = { workspace = true } 14 | 15 | [lints] 16 | workspace = true 17 | 18 | [dependencies] 19 | serde = { workspace = true } 20 | serde_json = { workspace = true } 21 | 22 | # for built-in utility impl of `Schema` trait 23 | uuid = { version = "1.18" } 24 | 25 | [features] 26 | no-release-serialize = [] -------------------------------------------------------------------------------- /examples/json_response/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::{Ohkami, Route}; 2 | use ohkami::claw::Json; 3 | use ohkami::serde::Serialize; 4 | 5 | 6 | #[derive(Serialize)] 7 | struct User { 8 | id: u64, 9 | name: String, 10 | } 11 | 12 | async fn single_user() -> Json { 13 | Json(User { 14 | id: 42, 15 | name: String::from("ohkami"), 16 | }) 17 | } 18 | 19 | async fn multiple_users() -> Json> { 20 | Json(vec![ 21 | User { 22 | id: 42, 23 | name: String::from("ohkami"), 24 | }, 25 | User { 26 | id: 1024, 27 | name: String::from("bynari"), 28 | } 29 | ]) 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() { 34 | Ohkami::new(( 35 | "/single" .GET(single_user), 36 | "/multiple".GET(multiple_users), 37 | )).howl("localhost:5000").await 38 | } 39 | -------------------------------------------------------------------------------- /samples/realworld/src/main.rs: -------------------------------------------------------------------------------- 1 | mod db; 2 | mod config; 3 | mod errors; 4 | mod models; 5 | mod fangs; 6 | mod handlers; 7 | 8 | #[cfg(test)] 9 | mod _test; 10 | 11 | use errors::RealWorldError; 12 | 13 | use sqlx::postgres::PgPoolOptions; 14 | 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<(), errors::RealWorldError> { 18 | dotenvy::dotenv() 19 | .map_err(|e| RealWorldError::Config(format!("Failed to load .env: {e}")))?; 20 | tracing_subscriber::fmt() 21 | .with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG) 22 | .init(); 23 | 24 | let pool = PgPoolOptions::new() 25 | .max_connections(42) 26 | .min_connections(42) 27 | .connect(config::DATABASE_URL()?).await 28 | .map_err(|e| RealWorldError::DB(e))?; 29 | 30 | handlers::realworld_ohkami(pool) 31 | .howl("localhost:8080").await; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /samples/streaming/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::sse::DataStream; 3 | use ohkami::openapi::{OpenAPI, Server}; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let o = Ohkami::new(( 8 | "/once".GET(hello_once), 9 | "/".GET(intervally_hello) 10 | )); 11 | 12 | o.generate(OpenAPI { 13 | title: "Streaming Sample API", 14 | version: "0.1.0", 15 | servers: &[Server::at("http://localhost:8080")] 16 | }); 17 | 18 | o.howl("localhost:8080").await 19 | } 20 | 21 | async fn hello_once() -> &'static str { 22 | "Hello!" 23 | } 24 | 25 | async fn intervally_hello() -> DataStream<&'static str> { 26 | DataStream::new(|mut s| async move { 27 | for _ in 0..5 { 28 | s.send("Hello!"); 29 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 30 | } 31 | s.send("Bye!"); 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /examples/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -Cue 4 | 5 | EXAMPLES=$(pwd) 6 | 7 | # First, check the buildability of all examples 8 | for directory in ./*/; do 9 | if [ "$(basename $directory)" != "target" ]; then 10 | cd $directory 11 | cargo check 12 | cd .. 13 | fi 14 | done 15 | 16 | # Now, additionally run tests for each example if needed 17 | 18 | cd $EXAMPLES/static_files && \ 19 | cargo test 20 | 21 | cd $EXAMPLES/jwt && \ 22 | cp .env.sample .env && \ 23 | cargo test 24 | 25 | cd $EXAMPLES/uibeam && \ 26 | cargo build && \ 27 | (timeout -sKILL 5 cargo run &) && \ 28 | sleep 1 && \ 29 | CONTENT_TYPE_COUNT=$(curl -i 'http://localhost:5555' 2>&1 | grep -i 'content-type' | wc -l) && \ 30 | if [ $CONTENT_TYPE_COUNT -eq 1 ]; then 31 | echo '---> ok' 32 | else 33 | echo '---> multiple content-type headers found (or something else went wrong)' 34 | exit 1 35 | fi 36 | -------------------------------------------------------------------------------- /ohkami/src/fang/bound.rs: -------------------------------------------------------------------------------- 1 | use super::FangProcCaller; 2 | use std::future::Future; 3 | 4 | pub use dispatch::*; 5 | 6 | #[cfg(feature = "__rt_threaded__")] 7 | mod dispatch { 8 | pub trait SendSyncOnThreaded: Send + Sync {} 9 | impl SendSyncOnThreaded for T {} 10 | 11 | #[allow(unused)] 12 | pub trait SendOnThreaded: Send {} 13 | impl SendOnThreaded for T {} 14 | } 15 | #[cfg(not(feature = "__rt_threaded__"))] 16 | mod dispatch { 17 | pub trait SendSyncOnThreaded {} 18 | impl SendSyncOnThreaded for T {} 19 | 20 | pub trait SendOnThreaded {} 21 | impl SendOnThreaded for T {} 22 | } 23 | 24 | #[allow(unused)] 25 | pub trait SendOnThreadedFuture: Future + SendOnThreaded {} 26 | impl + SendOnThreaded> SendOnThreadedFuture for F {} 27 | 28 | pub(crate) trait FPCBound: FangProcCaller + SendSyncOnThreaded {} 29 | impl FPCBound for T {} 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = [ 4 | "ohkami", 5 | "ohkami_lib", 6 | "ohkami_macros", 7 | "ohkami_openapi", 8 | ] 9 | exclude = [ 10 | "samples", 11 | "benches", 12 | "benches_rt", 13 | ] 14 | 15 | [workspace.package] 16 | version = "0.24.3" 17 | edition = "2024" 18 | authors = ["kanarus "] 19 | homepage = "https://crates.io/crates/ohkami" 20 | repository = "https://github.com/ohkami-rs/ohkami" 21 | readme = "README.md" 22 | keywords = ["async", "http", "web", "server", "framework"] 23 | categories = ["asynchronous", "web-programming::http-server", "network-programming", "wasm"] 24 | license = "MIT" 25 | 26 | [workspace.dependencies] 27 | byte_reader = { version = "3.1", features = ["text"] } 28 | serde = { version = "1.0", features = ["derive"] } 29 | serde_json = { version = "1.0" } 30 | 31 | [workspace.lints.clippy] 32 | useless_format = "allow" 33 | duplicated_attributes = "allow" 34 | -------------------------------------------------------------------------------- /benches_rt/monoio/src/bin/param.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | fn main() { 4 | let ncpus = std::thread::available_parallelism().map_or(1, |x| x.get()); 5 | 6 | let runtime = || monoio::RuntimeBuilder::::new() 7 | .enable_all() 8 | .build() 9 | .unwrap(); 10 | 11 | for core in 1..dbg!(ncpus) { 12 | std::thread::spawn(move || { 13 | monoio::utils::bind_to_cpu_set([core]).unwrap(); 14 | runtime().block_on({ 15 | Ohkami::new(( 16 | "/user/:id" 17 | .GET(async |Path(id): Path| id), 18 | )).howl("0.0.0.0:3000") 19 | }); 20 | }); 21 | } 22 | 23 | monoio::utils::bind_to_cpu_set([0]).unwrap(); 24 | runtime().block_on({ 25 | Ohkami::new(( 26 | "/user/:id" 27 | .GET(async |Path(id): Path| id), 28 | )).howl("0.0.0.0:3000") 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /ohkami_lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ohkami_lib" 3 | description = "internal library for Ohkami - A performant, declarative, and runtime-flexible web framework for Rust" 4 | documentation = "https://docs.rs/ohkami_lib" 5 | version = { workspace = true } 6 | edition = { workspace = true } 7 | authors = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | readme = { workspace = true } 11 | keywords = { workspace = true } 12 | categories = { workspace = true } 13 | license = { workspace = true } 14 | 15 | [package.metadata.docs.rs] 16 | features = ["stream"] 17 | 18 | [lints] 19 | workspace = true 20 | 21 | [dependencies] 22 | serde = { workspace = true } 23 | byte_reader = { workspace = true } 24 | percent-encoding = { version = "2.3" } 25 | futures-core = { optional = true, version = "0.3" } 26 | 27 | [features] 28 | stream = ["dep:futures-core"] 29 | 30 | ### DEBUG ### 31 | #default = ["stream"] -------------------------------------------------------------------------------- /ohkami/src/request/_test_headers.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(debug_assertions, feature = "DEBUG"))] 2 | #![cfg(all(test, feature = "__rt__"))] 3 | 4 | use ohkami_lib::CowSlice; 5 | 6 | use super::{RequestHeader, RequestHeaders}; 7 | use crate::header::append; 8 | 9 | #[test] 10 | fn append_header() { 11 | let mut h = RequestHeaders::new(); 12 | 13 | h.append(RequestHeader::Origin, CowSlice::from("A".as_bytes())); 14 | assert_eq!(h.origin(), Some("A")); 15 | h.append(RequestHeader::Origin, CowSlice::from("B".as_bytes())); 16 | assert_eq!(h.origin(), Some("A, B")); 17 | 18 | h.set().accept(append("X")); 19 | assert_eq!(h.accept(), Some("X")); 20 | h.set().accept(append("Y")); 21 | assert_eq!(h.accept(), Some("X, Y")); 22 | } 23 | 24 | #[test] 25 | fn append_custom_header() { 26 | let mut h = RequestHeaders::new(); 27 | 28 | h.set().x("Custom-Header", append("A")); 29 | assert_eq!(h.get("Custom-Header"), Some("A")); 30 | h.set().x("Custom-Header", append("B")); 31 | assert_eq!(h.get("Custom-Header"), Some("A, B")); 32 | } 33 | -------------------------------------------------------------------------------- /ohkami/src/header/append.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | pub struct Append(pub(crate) Cow<'static, str>); 4 | 5 | /// Passed to `{Request/Response}.headers.set().{name}( 〜 )` and 6 | /// append `value` to the header. 7 | /// 8 | /// Here appended values are combined by `,`. 9 | /// 10 | /// --- 11 | /// *example.rs* 12 | /// ```no_run 13 | /// use ohkami::prelude::*; 14 | /// use ohkami::header::append; 15 | /// 16 | /// #[derive(Clone)] 17 | /// struct AppendServer(&'static str); 18 | /// impl FangAction for AppendServer { 19 | /// async fn back<'b>(&'b self, res: &'b mut Response) { 20 | /// res.headers.set().server(append(self.0)); 21 | /// } 22 | /// } 23 | /// 24 | /// #[tokio::main] 25 | /// async fn main() { 26 | /// Ohkami::new(( 27 | /// AppendServer("ohkami"), 28 | /// 29 | /// "/".GET(|| async {"Hello, append!"}) 30 | /// 31 | /// )).howl("localhost:3000").await 32 | /// } 33 | /// ``` 34 | #[inline] 35 | pub fn append(value: impl Into>) -> Append { 36 | Append(value.into()) 37 | } 38 | -------------------------------------------------------------------------------- /benches_rt/tokio/src/bin/headers.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | #[derive(Clone)] 4 | struct SetHeaders; 5 | impl FangAction for SetHeaders { 6 | async fn back(&self, res: &mut Response) { 7 | res.headers.set() 8 | .server("Ohkami") 9 | .cross_origin_embedder_policy("require-corp") 10 | .cross_origin_resource_policy("same-origin") 11 | .referrer_policy("no-referrer") 12 | .strict_transport_security("max-age=15552000; includeSubDomains") 13 | .x_content_type_options("nosniff") 14 | .x_frame_options("SAMEORIGIN") 15 | ; 16 | } 17 | } 18 | 19 | fn main() { 20 | tokio::runtime::Builder::new_multi_thread() 21 | .enable_all() 22 | .event_interval(11) 23 | .global_queue_interval(31) 24 | .build() 25 | .expect("Failed building the Runtime") 26 | .block_on(Ohkami::new(( 27 | SetHeaders, 28 | "/user/:id" 29 | .GET(|Path(id): Path| async {id}), 30 | )).howl("0.0.0.0:3000")) 31 | } 32 | -------------------------------------------------------------------------------- /samples/streaming/openapi.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Streaming Sample API", 5 | "version": "0.1.0" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "http://localhost:8080" 10 | } 11 | ], 12 | "paths": { 13 | "/": { 14 | "get": { 15 | "operationId": "intervally_hello", 16 | "responses": { 17 | "200": { 18 | "description": "Streaming", 19 | "content": { 20 | "text/event-stream": { 21 | "schema": { 22 | "type": "string" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/once": { 31 | "get": { 32 | "operationId": "hello_once", 33 | "responses": { 34 | "200": { 35 | "description": "OK", 36 | "content": { 37 | "text/plain": { 38 | "schema": { 39 | "type": "string" 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 kanarus 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/chatgpt/src/fangs.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::OnceLock; 3 | use ohkami::prelude::*; 4 | 5 | #[derive(Clone)] 6 | pub struct APIKey(pub &'static str); 7 | 8 | impl APIKey { 9 | pub fn from_env() -> Self { 10 | static API_KEY: OnceLock> = OnceLock::new(); 11 | 12 | let api_key = API_KEY.get_or_init(|| { 13 | match env::args().nth(1).as_deref() { 14 | Some("--api-key") => env::args().nth(2), 15 | _ => env::var("OPENAI_API_KEY").ok() 16 | } 17 | }).as_deref().expect("\ 18 | OpenAI API key is not found\n\ 19 | \n\ 20 | [USAGE]\n\ 21 | Run `cargo run` with one of \n\ 22 | a. Set an environment variable `OPENAI_API_KEY` to your API key\n\ 23 | b. Pass your API key by command line arguments `-- --api-key <here>`\n\ 24 | "); 25 | 26 | Self(api_key) 27 | } 28 | } 29 | 30 | impl FangAction for APIKey { 31 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 32 | req.context.set(self.clone()); 33 | Ok(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/chatgpt/src/models.rs: -------------------------------------------------------------------------------- 1 | use ohkami::serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize)] 4 | pub struct ChatCompletions { 5 | pub model: &'static str, 6 | pub messages: Vec, 7 | pub stream: bool, 8 | } 9 | #[derive(Serialize)] 10 | pub struct ChatMessage { 11 | pub role: Role, 12 | pub content: String, 13 | } 14 | 15 | #[derive(Deserialize)] 16 | pub struct ChatCompletionChunk { 17 | pub id: String, 18 | pub choices: [ChatCompletionChoice; 1], 19 | } 20 | #[derive(Deserialize)] 21 | pub struct ChatCompletionChoice { 22 | pub delta: ChatCompletionDelta, 23 | pub finish_reason: Option, 24 | } 25 | #[derive(Deserialize)] 26 | pub struct ChatCompletionDelta { 27 | pub role: Option, 28 | pub content: Option, 29 | } 30 | #[derive(Deserialize)] 31 | #[allow(non_camel_case_types)] 32 | pub enum ChatCompletionFinishReason { 33 | stop, 34 | length, 35 | content_filter, 36 | } 37 | 38 | #[derive(Deserialize, Serialize)] 39 | #[allow(non_camel_case_types)] 40 | pub enum Role { 41 | system, 42 | user, 43 | assistant, 44 | } 45 | -------------------------------------------------------------------------------- /examples/multiple-single-threads/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | 3 | fn main() { 4 | async fn serve(o: Ohkami) -> std::io::Result<()> { 5 | let socket = tokio::net::TcpSocket::new_v4()?; 6 | 7 | socket.set_reuseport(true)?; 8 | socket.set_reuseaddr(true)?; 9 | socket.set_nodelay(true)?; 10 | 11 | socket.bind("0.0.0.0:8000".parse().unwrap())?; 12 | 13 | let listener = socket.listen(1024)?; 14 | 15 | o.howl(listener).await; 16 | 17 | Ok(()) 18 | } 19 | 20 | fn runtime() -> tokio::runtime::Runtime { 21 | tokio::runtime::Builder::new_current_thread() 22 | .enable_all() 23 | .build() 24 | .unwrap() 25 | } 26 | 27 | for _ in 0..(std::thread::available_parallelism().map_or(1, |n| n.get()) - 1/*for main thread*/) { 28 | std::thread::spawn(|| { 29 | runtime().block_on(serve(ohkami())).expect("serving error") 30 | }); 31 | } 32 | runtime().block_on(serve(ohkami())).expect("serving error") 33 | } 34 | 35 | fn ohkami() -> Ohkami { 36 | Ohkami::new(( 37 | "/".GET(async || {"Hello, world!"}), 38 | )) 39 | } 40 | -------------------------------------------------------------------------------- /ohkami_lib/src/serde_cookie.rs: -------------------------------------------------------------------------------- 1 | mod de; 2 | 3 | #[cfg(test)] 4 | mod _test; 5 | 6 | #[inline(always)] 7 | pub fn from_str<'de, D: serde::Deserialize<'de>>(input: &'de str) -> Result { 8 | let mut d = de::CookieDeserializer::new(input); 9 | let t = D::deserialize(&mut d)?; 10 | d.remaining().is_empty().then_some(t).ok_or_else(|| { 11 | serde::de::Error::custom(format!( 12 | "Unexpected trailing charactors: `{}`", 13 | d.remaining().escape_ascii() 14 | )) 15 | }) 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct Error(String); 20 | const _: () = { 21 | impl std::fmt::Display for Error { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | f.write_str(&self.0) 24 | } 25 | } 26 | impl std::error::Error for Error {} 27 | 28 | impl serde::ser::Error for Error { 29 | fn custom(msg: T) -> Self 30 | where 31 | T: std::fmt::Display, 32 | { 33 | Self(msg.to_string()) 34 | } 35 | } 36 | impl serde::de::Error for Error { 37 | fn custom(msg: T) -> Self 38 | where 39 | T: std::fmt::Display, 40 | { 41 | Self(msg.to_string()) 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /samples/realworld/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | use ohkami::util::unix_timestamp; 3 | use ohkami::serde::{Serialize, Deserialize}; 4 | use ohkami::fang::{Jwt, JwtToken}; 5 | use uuid::Uuid; 6 | use crate::errors::RealWorldError; 7 | 8 | 9 | macro_rules! environment_variables { 10 | ( $( $name:ident ),* $(,)? ) => { 11 | $( 12 | #[allow(non_snake_case)] 13 | pub fn $name() -> Result<&'static str, RealWorldError> { 14 | static $name: OnceLock> = OnceLock::new(); 15 | 16 | match $name.get_or_init(|| std::env::var(stringify!($name))) { 17 | Ok(value) => Ok(&**value), 18 | Err(e) => Err(RealWorldError::Config(e.to_string())), 19 | } 20 | } 21 | )* 22 | }; 23 | } environment_variables! { 24 | DATABASE_URL, 25 | PEPPER, 26 | JWT_SECRET_KEY, 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | pub struct JwtPayload { 31 | pub iat: u64, 32 | pub user_id: Uuid, 33 | } 34 | 35 | pub fn issue_jwt_for_user_of_id(user_id: Uuid) -> Result { 36 | let secret = JWT_SECRET_KEY()?; 37 | Ok(Jwt::default(secret).clone().issue(JwtPayload { 38 | user_id, 39 | iat: unix_timestamp(), 40 | })) 41 | } 42 | -------------------------------------------------------------------------------- /ohkami_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [lib] 2 | proc-macro = true 3 | 4 | [package] 5 | name = "ohkami_macros" 6 | description = "proc macros for Ohkami - A performant, declarative, and runtime-flexible web framework for Rust" 7 | documentation = "https://docs.rs/ohkami_macros" 8 | version = { workspace = true } 9 | edition = { workspace = true } 10 | authors = { workspace = true } 11 | homepage = { workspace = true } 12 | repository = { workspace = true } 13 | readme = { workspace = true } 14 | keywords = { workspace = true } 15 | categories = { workspace = true } 16 | license = { workspace = true } 17 | 18 | [package.metadata.docs.rs] 19 | features = ["worker", "openapi"] 20 | 21 | [lints] 22 | workspace = true 23 | 24 | [dependencies] 25 | proc-macro2 = "1.0" 26 | quote = "1.0" 27 | syn = { version = "2.0", features = ["full"] } 28 | toml = { optional = true, version = "0.9", features = ["serde", "parse"], default-features = false } 29 | jsonc-parser = { optional = true, version = "0.28", features = ["serde"] } 30 | serde = { optional = true, workspace = true } 31 | serde_json = { optional = true, workspace = true } 32 | 33 | [features] 34 | worker = ["dep:toml", "dep:jsonc-parser", "dep:serde", "dep:serde_json"] 35 | openapi = [] 36 | 37 | ##### DEBUG ##### 38 | #default = ["worker", "openapi"] -------------------------------------------------------------------------------- /samples/worker-durable-websocket/src/room.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused/* just for sample of WebSocket with `ohkami::DurableObject` */)] 2 | 3 | use ohkami::serde::{Serialize, Deserialize}; 4 | use ohkami::ws::SessionMap; 5 | use ohkami::DurableObject; // <-- 6 | 7 | #[DurableObject] 8 | struct Room { 9 | name: Option, 10 | state: worker::State, 11 | sessions: SessionMap, 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | struct Session { 16 | username: String, 17 | } 18 | 19 | impl DurableObject for Room { 20 | fn new(state: worker::State, env: worker::Env) -> Self { 21 | let mut sessions = SessionMap::new(); 22 | 23 | // restore sessions if woken up from hibernation 24 | for ws in state.get_websockets() { 25 | if let Ok(Some(session)) = ws.deserialize_attachment() { 26 | sessions.insert(ws, session).unwrap(); 27 | } 28 | } 29 | 30 | Self { name: None, state, sessions } 31 | } 32 | 33 | async fn fetch( 34 | &mut self, 35 | req: worker::Request 36 | ) -> worker::Result { 37 | todo!() 38 | } 39 | 40 | async fn websocket_message( 41 | &mut self, 42 | ws: worker::WebSocket, 43 | message: worker::WebSocketIncomingMessage, 44 | ) -> worker::Result<()> { 45 | todo!() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/websocket/template/main.js: -------------------------------------------------------------------------------- 1 | export function connect_input_and_button_to(ws_url, input_id, button_id) { 2 | let ws = null; 3 | 4 | const input = document.getElementById(input_id); 5 | input.spellcheck = false; 6 | input.disabled = true; 7 | 8 | const button = document.getElementById(button_id); 9 | button.textContent = "connect"; 10 | 11 | button.addEventListener( 12 | "click", (e) => { 13 | if (button.textContent == "connect") { 14 | ws = new WebSocket(ws_url); 15 | ws.addEventListener("open", (e) => { 16 | console.log(e); 17 | ws.send("test"); 18 | }); 19 | ws.addEventListener("message", (e) => { 20 | console.log("ws got message: ", e.data); 21 | }); 22 | ws.addEventListener("close", (e) => { 23 | console.log("close:", e); 24 | 25 | input.value = ""; 26 | input.disabled = true; 27 | 28 | button.textContent = "connect"; 29 | }); 30 | 31 | input.disabled = false; 32 | 33 | button.textContent = "send"; 34 | } else { 35 | console.log("sending:", input.value); 36 | ws.send(input.value); 37 | } 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /samples/realworld/src/models/response.rs: -------------------------------------------------------------------------------- 1 | use ohkami::serde::Serialize; 2 | use super::{User, Profile, Article, Comment, Tag}; 3 | 4 | 5 | #[derive(Serialize)] 6 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 7 | pub struct UserResponse { 8 | pub user: User, 9 | } 10 | 11 | #[derive(Serialize)] 12 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 13 | pub struct ProfileResponse { 14 | pub profile: Profile, 15 | } 16 | 17 | #[derive(Serialize)] 18 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 19 | pub struct SingleArticleResponse { 20 | pub article: Article, 21 | } 22 | #[derive(Serialize)] 23 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 24 | pub struct MultipleArticlesResponse { 25 | pub articles: Vec
, 26 | #[serde(rename = "articlesCount")] 27 | pub articles_count: usize, 28 | } 29 | 30 | #[derive(Serialize)] 31 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 32 | pub struct SingleCommentResponse { 33 | pub comment: Comment, 34 | } 35 | #[derive(Serialize)] 36 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 37 | pub struct MultipleCommentsResponse { 38 | pub comments: Vec, 39 | } 40 | 41 | #[derive(Serialize)] 42 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 43 | pub struct ListOfTagsResponse<'t> { 44 | pub tags: Vec> 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/Publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: ['v*'] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | environment: 12 | name: publishing 13 | 14 | permissions: 15 | contents: write # for creating GitHub Release 16 | id-token: write # for OIDC authentication 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | with: 21 | ref: main 22 | fetch-depth: 0 23 | 24 | - name: Ensure main branch 25 | run: | 26 | BRANCHS=$(git branch --contains ${{ github.ref_name }}) 27 | set -- $BRANCHS 28 | for BRANCH in $BRANCHS; do 29 | if [[ "$BRANCH" == "main" ]]; then 30 | exit 0 31 | fi 32 | done 33 | exit 1 34 | 35 | - uses: rust-lang/crates-io-auth-action@v1 36 | id: cratesio_auth 37 | 38 | - name: Trusted Publish to crates.io 39 | env: 40 | CARGO_REGISTRY_TOKEN: ${{ steps.cratesio_auth.outputs.token }} 41 | run: | 42 | cargo publish --package ohkami_openapi 43 | cargo publish --package ohkami_macros 44 | cargo publish --package ohkami_lib 45 | cargo publish --package ohkami 46 | 47 | - name: Create GitHub Release 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | run: | 51 | gh release create ${{ github.ref_name }} --generate-notes 52 | -------------------------------------------------------------------------------- /samples/realworld/src/fangs.rs: -------------------------------------------------------------------------------- 1 | use ohkami::fang::{Jwt, FangAction}; 2 | use ohkami::{IntoResponse, Request, Response}; 3 | 4 | 5 | /// memorizes `crate::config::JwtPayload` 6 | #[derive(Clone)] 7 | pub struct Auth { 8 | /// When `true`, not reject the request when it doesn't have any credential 9 | /// and just let it go without JwtPayload 10 | optional: bool, 11 | } 12 | impl Auth { 13 | pub fn required() -> Self { 14 | Self { optional: false } 15 | } 16 | pub fn optional() -> Self { 17 | Self { optional: true } 18 | } 19 | } 20 | impl FangAction for Auth { 21 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 22 | if req.headers.authorization().is_none() && self.optional { 23 | return Ok(()); 24 | } 25 | 26 | let secret = crate::config::JWT_SECRET_KEY() 27 | .map_err(IntoResponse::into_response)?; 28 | let payload = Jwt::::default(secret) 29 | .verified(req) 30 | .map_err(IntoResponse::into_response)?; 31 | req.context.set(payload); 32 | Ok(()) 33 | } 34 | } 35 | 36 | #[derive(Clone)] 37 | pub struct Logger; 38 | impl FangAction for Logger { 39 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 40 | tracing::info!("req = {:<7} {}", req.method, req.path.str()); 41 | Ok(()) 42 | } 43 | 44 | async fn back<'a>(&'a self, res: &'a mut Response) { 45 | tracing::info!("res = {res:?}"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/derive_from_request/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | fn main() {} 3 | 4 | use ohkami::{FromRequest, Method}; 5 | 6 | 7 | struct RequestMethod(Method); 8 | impl<'req> FromRequest<'req> for RequestMethod { 9 | type Error = std::convert::Infallible; 10 | fn from_request(req: &'req ohkami::prelude::Request) -> Option> { 11 | Some(Ok(Self(req.method))) 12 | } 13 | } 14 | 15 | struct RequestPath<'req>(std::borrow::Cow<'req, str>); 16 | impl<'req> FromRequest<'req> for RequestPath<'req> { 17 | type Error = std::convert::Infallible; 18 | fn from_request(req: &'req ohkami::prelude::Request) -> Option> { 19 | Some(Ok(Self(req.path.str()))) 20 | } 21 | } 22 | 23 | struct RequestPathOwned(String); 24 | impl<'req> FromRequest<'req> for RequestPathOwned { 25 | type Error = std::convert::Infallible; 26 | fn from_request(req: &'req ohkami::prelude::Request) -> Option> { 27 | Some(Ok(Self(req.path.str().into()))) 28 | } 29 | } 30 | 31 | 32 | #[derive(FromRequest)] 33 | struct MethodAndPathA { 34 | method: RequestMethod, 35 | path: RequestPathOwned, 36 | } 37 | 38 | #[derive(FromRequest)] 39 | struct MethodAndPathB<'req> { 40 | method: RequestMethod, 41 | path: RequestPath<'req>, 42 | } 43 | 44 | #[derive(FromRequest)] 45 | struct MethodAndPathC( 46 | RequestMethod, 47 | RequestPathOwned, 48 | ); 49 | 50 | #[derive(FromRequest)] 51 | struct MethodAndPathD<'req>( 52 | RequestMethod, 53 | RequestPath<'req>, 54 | ); 55 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::model::ID; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub(crate) enum APIError { 5 | #[error("Error in worker: {0}")] 6 | Worker(#[from] ::worker::Error), 7 | 8 | #[error("Internal error: {0}")] 9 | Internal(String), 10 | 11 | #[error("User name `{0}` is already used")] 12 | UserNameAlreadyUsed(String), 13 | 14 | #[error("User (id = {id}) not found")] 15 | UserNotFound { id: ID }, 16 | 17 | #[error("User (id = {me}) requests modifying other user (id = {other})")] 18 | ModifyingOtherUser { me: ID, other: ID }, 19 | } 20 | 21 | impl ohkami::IntoResponse for APIError { 22 | fn into_response(self) -> ohkami::Response { 23 | ::worker::console_error!("{self}"); 24 | match &self { 25 | Self::Worker(_) => ohkami::Response::InternalServerError(), 26 | Self::Internal(_) => ohkami::Response::InternalServerError(), 27 | Self::UserNameAlreadyUsed(_) => ohkami::Response::BadRequest() 28 | .with_text(self.to_string()), 29 | Self::UserNotFound { .. } => ohkami::Response::NotFound(), 30 | Self::ModifyingOtherUser { .. } => ohkami::Response::Forbidden(), 31 | } 32 | } 33 | 34 | #[cfg(feature="openapi")] 35 | fn openapi_responses() -> ohkami::openapi::Responses { 36 | use ohkami::openapi::Response; 37 | 38 | ohkami::openapi::Responses::new([ 39 | (500, Response::when("Worker's internal error")), 40 | (403, Response::when("Modyfing other user")) 41 | ]) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/form/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::claw::{status::NoContent, content::{Multipart, File}}; 3 | use ohkami::serde::Deserialize; 4 | 5 | struct FormTemplate; 6 | impl ohkami::IntoResponse for FormTemplate { 7 | fn into_response(self) -> Response { 8 | Response::OK().with_html(include_str!("../form.html")) 9 | } 10 | } 11 | 12 | async fn get_form() -> FormTemplate { 13 | FormTemplate 14 | } 15 | 16 | 17 | #[derive(Deserialize)] 18 | struct FormData<'req> { 19 | #[serde(rename = "account-name")] 20 | account_name: Option<&'req str>, 21 | pics: Vec>, 22 | } 23 | 24 | async fn post_submit( 25 | Multipart(form): Multipart> 26 | ) -> NoContent { 27 | println!("\n\ 28 | ===== submit =====\n\ 29 | [account name] {:?}\n\ 30 | [ pictures ] {} files (mime: [{}])\n\ 31 | ==================", 32 | form.account_name, 33 | form.pics.len(), 34 | form.pics.iter().map(|f| f.mimetype).collect::>().join(", "), 35 | ); 36 | 37 | NoContent 38 | } 39 | 40 | #[derive(Clone)] 41 | struct Logger; 42 | impl FangAction for Logger { 43 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 44 | println!("\n[req]\n{req:#?}"); 45 | Ok(()) 46 | } 47 | async fn back<'a>(&'a self, res: &'a mut Response) { 48 | println!("\n[res]\n{res:#?}"); 49 | } 50 | } 51 | 52 | #[tokio::main] 53 | async fn main() { 54 | Ohkami::new((Logger, 55 | "/form" .GET(get_form), 56 | "/submit".POST(post_submit), 57 | )).howl("localhost:5000").await 58 | } 59 | -------------------------------------------------------------------------------- /ohkami/src/claw/content/html.rs: -------------------------------------------------------------------------------- 1 | use super::IntoContent; 2 | use std::borrow::Cow; 3 | 4 | #[cfg(feature = "openapi")] 5 | use crate::openapi; 6 | 7 | /// # HTML format 8 | /// 9 | /// ## Request 10 | /// 11 | /// not supported 12 | /// 13 | /// ## Response 14 | /// 15 | /// - content type: `text/html; charset=UTF-8` 16 | /// - schema bound: `Into>` 17 | /// 18 | /// note: This doesn't validate the content to be a valid HTML document, 19 | /// it just sets the content type to `text/html` and returns the content as is. 20 | /// 21 | /// ### example 22 | /// 23 | /// ``` 24 | /// use ohkami::claw::content::Html; 25 | /// 26 | /// async fn handler() -> Html<&'static str> { 27 | /// Html(r#" 28 | /// 29 | /// 30 | /// Sample Document 31 | /// 32 | /// 33 | ///

Sample Document

34 | /// 35 | /// 36 | /// "#) 37 | /// } 38 | /// ``` 39 | pub struct Html(pub T); 40 | 41 | impl>> IntoContent for Html { 42 | const CONTENT_TYPE: &'static str = "text/html; charset=UTF-8"; 43 | 44 | fn into_content(self) -> Result, impl std::fmt::Display> { 45 | Result::<_, std::convert::Infallible>::Ok(match self.0.into() { 46 | Cow::Owned(s) => Cow::Owned(s.into_bytes()), 47 | Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()), 48 | }) 49 | } 50 | 51 | #[cfg(feature = "openapi")] 52 | fn openapi_responsebody() -> impl Into { 53 | openapi::string() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /samples/readme-openapi/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::claw::status; 3 | use ohkami::openapi; 4 | 5 | // Derive `Schema` trait to generate the schema of this struct in OpenAPI document. 6 | #[derive(Deserialize, openapi::Schema)] 7 | struct CreateUser<'req> { 8 | name: &'req str, 9 | } 10 | 11 | #[derive(Serialize, openapi::Schema)] 12 | // `#[openapi(component)]` to define it as component in OpenAPI document. 13 | #[openapi(component)] 14 | struct User { 15 | id: usize, 16 | name: String, 17 | } 18 | 19 | async fn create_user( 20 | Json(CreateUser { name }): Json> 21 | ) -> status::Created> { 22 | status::Created(Json(User { 23 | id: 42, 24 | name: name.to_string() 25 | })) 26 | } 27 | 28 | // (optionally) Set operationId, summary, or override descriptions by `operation` attribute. 29 | #[openapi::operation({ 30 | summary: "...", 31 | 200: "List of all users", 32 | })] 33 | /// This doc comment is used for the 34 | /// `description` field of OpenAPI document 35 | async fn list_users() -> Json> { 36 | Json(vec![]) 37 | } 38 | 39 | #[tokio::main] 40 | async fn main() { 41 | let o = Ohkami::new(( 42 | "/users" 43 | .GET(list_users) 44 | .POST(create_user), 45 | )); 46 | 47 | // This make your Ohkami spit out `openapi.json` ( the file name is configurable by `.generate_to` ). 48 | o.generate(openapi::OpenAPI { 49 | title: "Users Server", 50 | version: "0.1.0", 51 | servers: &[ 52 | openapi::Server::at("localhost:5000"), 53 | ] 54 | }); 55 | 56 | o.howl("localhost:5000").await; 57 | } 58 | -------------------------------------------------------------------------------- /ohkami_macros/src/worker/wrangler.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | use std::io::{self, Read}; 3 | 4 | pub fn parse_wrangler() -> Result { 5 | fn parse_error(e: impl std::fmt::Display) -> io::Error { 6 | io::Error::new( 7 | io::ErrorKind::InvalidData, 8 | format!("failed to parse wrangler config: `{e}`"), 9 | ) 10 | } 11 | 12 | let mut buf = String::new(); 13 | 14 | match ( 15 | util::find_file_at_package_or_workspace_root("wrangler.toml")?, 16 | util::find_file_at_package_or_workspace_root("wrangler.jsonc")?, 17 | ) { 18 | (Some(_), Some(_)) => Err(io::Error::new( 19 | io::ErrorKind::InvalidData, 20 | "both `wrangler.toml` and `wrangler.jsonc` is found !", 21 | )), 22 | (None, None) => Err(io::Error::new( 23 | io::ErrorKind::NotFound, 24 | "neither `wrangler.toml` nor `wrangler.jsonc` is found at package or workspace root", 25 | )), 26 | (Some(mut wrangler_toml), None) => { 27 | wrangler_toml.read_to_string(&mut buf)?; 28 | let config = toml::from_str(&buf).map_err(parse_error)?; 29 | Ok(config) 30 | } 31 | (None, Some(mut wrangler_jsonc)) => { 32 | wrangler_jsonc.read_to_string(&mut buf)?; 33 | let config = jsonc_parser::parse_to_serde_value(&buf, &Default::default()) 34 | .map_err(parse_error)? 35 | .ok_or_else(|| parse_error("invalid `.jsonc`"))?; 36 | let config = serde_json::from_value(config).map_err(parse_error)?; 37 | Ok(config) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/src/model.rs: -------------------------------------------------------------------------------- 1 | use ohkami::serde::{Serialize, Deserialize}; 2 | 3 | #[cfg(feature="openapi")] 4 | use ohkami::openapi::Schema; 5 | 6 | 7 | pub(super) type ID = i32; 8 | 9 | pub(super) type Age = u8; 10 | 11 | pub(super) type Timestamp = String; 12 | 13 | pub(super) fn timestamp_now() -> Timestamp { 14 | ohkami::util::unix_timestamp().to_string() 15 | } 16 | 17 | #[derive(Serialize, Deserialize)] 18 | #[cfg_attr(feature="openapi", derive(Schema))] 19 | #[cfg_attr(feature="openapi", openapi(component))] 20 | pub(super) struct UserProfile { 21 | pub(super) id: ID, 22 | pub(super) name: String, 23 | pub(super) location: Option, 24 | pub(super) age: Option, 25 | } 26 | 27 | #[derive(Serialize, Deserialize)] 28 | #[cfg_attr(feature="openapi", derive(Schema))] 29 | pub(super) struct EditProfileRequest<'req> { 30 | pub(super) location: Option<&'req str>, 31 | pub(super) age: Option, 32 | } 33 | 34 | #[derive(Deserialize)] 35 | #[cfg_attr(feature="openapi", derive(Schema))] 36 | pub(super) struct SignUpRequest<'req> { 37 | pub(super) name: &'req str, 38 | pub(super) token: &'req str, 39 | } 40 | 41 | #[derive(Serialize, Deserialize, Clone)] 42 | #[cfg_attr(feature="openapi", derive(Schema))] 43 | #[cfg_attr(feature="openapi", openapi(component))] 44 | pub(super) struct Tweet { 45 | pub(super) user_id: ID, 46 | pub(super) user_name: String, 47 | pub(super) content: String, 48 | pub(super) posted_at: Timestamp, 49 | } 50 | 51 | #[derive(Deserialize)] 52 | #[cfg_attr(feature="openapi", derive(Schema))] 53 | pub(super) struct PostTweetRequest<'req> { 54 | pub(super) content: &'req str, 55 | } 56 | 57 | -------------------------------------------------------------------------------- /ohkami/src/claw/content/multipart.rs: -------------------------------------------------------------------------------- 1 | use super::super::bound::{self, Incoming}; 2 | use super::FromContent; 3 | use ohkami_lib::serde_multipart; 4 | 5 | pub use ohkami_lib::serde_multipart::File; 6 | 7 | #[cfg(feature = "openapi")] 8 | use crate::openapi; 9 | 10 | /// # multipart/form-data format 11 | /// 12 | /// When `openapi` feature is activated, schema bound additionally 13 | /// requires `openapi::Schema`. 14 | /// 15 | /// ## Request 16 | /// 17 | /// - content type: `multipart/form-data` 18 | /// - schema bound: `Deserialize<'_>` 19 | /// 20 | /// ### example 21 | /// 22 | /// ``` 23 | /// # enum MyError {} 24 | /// use ohkami::claw::content::{Multipart, File}; 25 | /// use ohkami::serde::Deserialize; 26 | /// 27 | /// #[derive(Deserialize)] 28 | /// struct SignUpForm<'req> { 29 | /// #[serde(rename = "user-name")] 30 | /// user_name: &'req str, 31 | /// 32 | /// password: &'req str, 33 | /// 34 | /// #[serde(rename = "user-icon")] 35 | /// user_icon: Option>, 36 | /// 37 | /// #[serde(rename = "pet-photos")] 38 | /// pet_photos: Vec>, 39 | /// } 40 | /// 41 | /// async fn sign_up( 42 | /// Multipart(form): Multipart>, 43 | /// ) -> Result<(), MyError> { 44 | /// todo!() 45 | /// } 46 | /// ``` 47 | /// 48 | /// ## Response 49 | /// 50 | /// not supported 51 | pub struct Multipart(pub T); 52 | 53 | impl<'req, T: Incoming<'req>> FromContent<'req> for Multipart { 54 | const MIME_TYPE: &'static str = "multipart/form-data"; 55 | 56 | fn from_content(body: &'req [u8]) -> Result { 57 | serde_multipart::from_bytes(body).map(Multipart) 58 | } 59 | 60 | #[cfg(feature = "openapi")] 61 | fn openapi_requestbody() -> impl Into { 62 | T::schema() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /samples/petstore/client/src/main.ts: -------------------------------------------------------------------------------- 1 | import newClient from "openapi-fetch"; 2 | import type { paths, components } from "../openapi"; 3 | 4 | async function main() { 5 | const newPet: components["schemas"]["CreatePetRequest"] = (() => { 6 | let [, , petName] = process.argv; 7 | if (petName) { 8 | return { 9 | petName, 10 | tag: "user" 11 | } 12 | } else { 13 | const now = new Date(); 14 | petName = `pet${now.getHours()}${now.getMinutes()}${now.getSeconds()}`; 15 | 16 | console.warn(`System generated a pet's name: "${petName}"`); 17 | console.warn(`You can specify one via a command line argument.`); 18 | 19 | return { 20 | petName, 21 | tag: "system" 22 | } 23 | } 24 | })(); 25 | 26 | const client = newClient({ baseUrl: "http://localhost:5050" }); 27 | 28 | { 29 | const { data, error } = await client.GET("/pets"); 30 | if (error) { 31 | console.log(`error from "GET /pets": %o`, error); 32 | return; 33 | } 34 | console.log(`data from "GET /pets": %o`, data); 35 | } 36 | 37 | { 38 | const { data, error } = await client.POST("/pets", { 39 | body: newPet 40 | }); 41 | if (error) { 42 | console.log(`error from "POST /pets": %o`, error); 43 | return; 44 | } 45 | console.log(`data from "POST /pets": %o`, data); 46 | } 47 | 48 | { 49 | const { data, error } = await client.GET("/pets"); 50 | if (error) { 51 | console.log(`error from "GET /pets": %o`, error); 52 | return; 53 | } 54 | console.log(`data from "GET /pets": %o`, data); 55 | } 56 | } 57 | 58 | await main(); 59 | -------------------------------------------------------------------------------- /samples/realworld/src/errors.rs: -------------------------------------------------------------------------------- 1 | use ohkami::{IntoResponse, serde::Serialize}; 2 | use std::borrow::Cow; 3 | 4 | #[derive(Debug)] 5 | pub enum RealWorldError { 6 | Config(String), 7 | DB(sqlx::Error), 8 | Validation { body: String }, 9 | NotFound(Cow<'static, str>), 10 | Unauthorized(Cow<'static, str>), 11 | FoundUnexpectedly(Cow<'static, str>), 12 | } const _: () = { 13 | impl std::fmt::Display for RealWorldError { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | f.write_fmt(format_args!("{self:?}")) 16 | } 17 | } 18 | impl std::error::Error for RealWorldError {} 19 | }; 20 | 21 | #[derive(Serialize)] 22 | struct ValidationErrorFormat { 23 | errors: ValidationError, 24 | } 25 | #[derive(Serialize, Debug)] 26 | pub struct ValidationError { 27 | body: Vec>, 28 | } 29 | 30 | impl IntoResponse for RealWorldError { 31 | fn into_response(self) -> ohkami::Response { 32 | use ohkami::claw::{status, Json}; 33 | 34 | match self { 35 | Self::Validation { body } => status::UnprocessableEntity( 36 | Json(ValidationErrorFormat { 37 | errors: ValidationError { 38 | body: vec![body.into()], 39 | }, 40 | } 41 | )).into_response(), 42 | Self::Config(err_msg) => status::InternalServerError(err_msg).into_response(), 43 | Self::DB(sqlx_err) => status::InternalServerError(sqlx_err.to_string()).into_response(), 44 | Self::NotFound(nf) => status::NotFound(nf).into_response(), 45 | Self::Unauthorized(msg) => status::Unauthorized(msg).into_response(), 46 | Self::FoundUnexpectedly(fu) => status::BadRequest(fu).into_response(), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /benches/src/response_headers/fxmap.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, hash::BuildHasherDefault}; 2 | 3 | pub struct FxMap { 4 | map: rustc_hash::FxHashMap<&'static str, Cow<'static, str>>, 5 | size: usize, 6 | } 7 | impl FxMap { 8 | pub fn new() -> Self { 9 | Self { 10 | map: rustc_hash::FxHashMap::with_capacity_and_hasher(32, BuildHasherDefault::default()), 11 | size: 2/* "\r\n".len() */ 12 | } 13 | } 14 | 15 | #[inline(always)] 16 | pub fn insert( 17 | &mut self, 18 | key: &'static str, 19 | value: impl Into>, 20 | ) -> &mut Self { 21 | let value = value.into(); 22 | 23 | self.size += value.len(); 24 | if let Some(old) = self.map.insert(key, value) { 25 | self.size -= old.len(); 26 | } else { 27 | self.size += key.len() + 2/* ": ".len() */ + 2/* "\r\n".len() */; 28 | } 29 | self 30 | } 31 | 32 | #[inline] 33 | pub fn remove(&mut self, key: &'static str) -> &mut Self { 34 | if let Some(old) = self.map.remove(&key) { 35 | self.size -= key.len() + 2/* ": ".len() */ + old.len() + 2/* "\r\n".len() */; 36 | } 37 | self 38 | } 39 | 40 | #[inline] 41 | pub fn write_to(&self, buf: &mut Vec) { 42 | macro_rules! push { 43 | ($buf:ident <- $bytes:expr) => { 44 | unsafe { 45 | let (buf_len, bytes_len) = ($buf.len(), $bytes.len()); 46 | std::ptr::copy_nonoverlapping( 47 | $bytes.as_ptr(), 48 | <[u8]>::as_mut_ptr($buf).add(buf_len), 49 | bytes_len 50 | ); 51 | $buf.set_len(buf_len + bytes_len); 52 | } 53 | }; 54 | } 55 | 56 | buf.reserve(self.size); 57 | 58 | for (k, v) in self.map.iter() { 59 | push!(buf <- k.as_bytes()); 60 | push!(buf <- b": "); 61 | push!(buf <- v.as_bytes()); 62 | push!(buf <- b"\r\n"); 63 | } 64 | push!(buf <- b"\r\n") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ohkami/src/header/qvalue.rs: -------------------------------------------------------------------------------- 1 | // The QValue struct is used to represent the quality value of an encoding. 2 | // It is a wrapper around a u16 value, which represents the quality value 3 | // as a real number with at most 3 decimal places. 4 | // For example, a QValue of 0.5 would be represented as 500. 5 | #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] 6 | pub struct QValue(pub(crate) u16); 7 | 8 | impl QValue { 9 | pub fn parse(s: &str) -> Option { 10 | let mut r = byte_reader::Reader::new(s.as_bytes()); 11 | match r.consume_oneof(["q=0", "q=1"])? { 12 | 0 => { 13 | let mut q = 0; 14 | if r.consume(".").is_some() { 15 | for factor in [100, 10, 1] { 16 | if let Some(b) = r.next_if(u8::is_ascii_digit) { 17 | q += factor * (b - b'0') as u16; 18 | } else { 19 | break; 20 | } 21 | } 22 | } 23 | Some(Self(q)) 24 | } 25 | 1 => Some(Self(1000)), 26 | _ => unreachable!(), 27 | } 28 | } 29 | 30 | pub const fn is_zero(&self) -> bool { 31 | self.0 == 0 32 | } 33 | } 34 | 35 | impl Default for QValue { 36 | fn default() -> Self { 37 | Self(1000) 38 | } 39 | } 40 | 41 | impl std::fmt::Debug for QValue { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | write!(f, "q={}", self.0 as f32 / 1000.0) 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn test_qvalue_parse() { 53 | assert_eq!(QValue::parse("q=0.5"), Some(QValue(500))); 54 | assert_eq!(QValue::parse("q=1"), Some(QValue(1000))); 55 | assert_eq!(QValue::parse("q=0"), Some(QValue(0))); 56 | assert_eq!(QValue::parse("q=0.123"), Some(QValue(123))); 57 | assert_eq!(QValue::parse("q=0.999"), Some(QValue(999))); 58 | assert_eq!(QValue::parse("q=0.000"), Some(QValue(0))); 59 | assert_eq!(QValue::parse("q=1.000"), Some(QValue(1000))); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/readme-openapi/openapi.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Users Server", 5 | "version": "0.1.0" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "localhost:5000" 10 | } 11 | ], 12 | "paths": { 13 | "/users": { 14 | "get": { 15 | "operationId": "list_users", 16 | "summary": "...", 17 | "description": "This doc comment is used for the\n`description` field of OpenAPI document", 18 | "responses": { 19 | "200": { 20 | "description": "List of all users", 21 | "content": { 22 | "application/json": { 23 | "schema": { 24 | "type": "array", 25 | "items": { 26 | "$ref": "#/components/schemas/User" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | "post": { 35 | "operationId": "create_user", 36 | "requestBody": { 37 | "required": true, 38 | "content": { 39 | "application/json": { 40 | "schema": { 41 | "type": "object", 42 | "properties": { 43 | "name": { 44 | "type": "string" 45 | } 46 | }, 47 | "required": [ 48 | "name" 49 | ] 50 | } 51 | } 52 | } 53 | }, 54 | "responses": { 55 | "201": { 56 | "description": "Created", 57 | "content": { 58 | "application/json": { 59 | "schema": { 60 | "$ref": "#/components/schemas/User" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | "components": { 70 | "schemas": { 71 | "User": { 72 | "type": "object", 73 | "properties": { 74 | "id": { 75 | "type": "integer" 76 | }, 77 | "name": { 78 | "type": "string" 79 | } 80 | }, 81 | "required": [ 82 | "id", 83 | "name" 84 | ] 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /samples/worker-with-openapi/src/fang.rs: -------------------------------------------------------------------------------- 1 | use crate::Bindings; 2 | use ohkami::{Request, Response, FromRequest, serde::json}; 3 | use ohkami::prelude::{FangAction, Deserialize}; 4 | 5 | #[cfg(feature="openapi")] 6 | use ohkami::openapi; 7 | 8 | 9 | /// memorize `TokenAuthed` 10 | #[derive(Clone)] 11 | pub(super) struct TokenAuth; 12 | 13 | pub(super) struct TokenAuthed { 14 | pub(super) user_id: i32, 15 | pub(super) user_name: String, 16 | } 17 | 18 | #[derive(Deserialize)] 19 | struct TokenSchema<'req> { 20 | user_id: i32, 21 | token: &'req str, 22 | } 23 | 24 | impl FangAction for TokenAuth { 25 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 26 | let authorization_bearer = req.headers 27 | .authorization().ok_or_else(Response::BadRequest)? 28 | .strip_prefix("Bearer ").ok_or_else(Response::BadRequest)?; 29 | 30 | let TokenSchema { user_id, token } = 31 | json::from_str(authorization_bearer) 32 | .inspect_err(|e| worker::console_error!("Failed to parse TokenSchema `{authorization_bearer}`: {e}")) 33 | .map_err(|_| Response::Unauthorized())?; 34 | 35 | let Bindings { DB, .. } = FromRequest::from_request(req).unwrap()?; 36 | let user_name = DB.prepare("SELECT name FROM users WHERE id = ? AND token = ?") 37 | .bind(&[user_id.into(), token.into()])? 38 | .first::(Some("name")).await? 39 | .ok_or_else(Response::Unauthorized)?; 40 | 41 | req.context.set(TokenAuthed { user_id, user_name }); 42 | Ok(()) 43 | } 44 | 45 | #[cfg(feature="openapi")] 46 | fn openapi_map_operation(&self, operation: openapi::Operation) -> openapi::Operation { 47 | operation.security( 48 | openapi::SecurityScheme::bearer("tokenAuth", Some("JSON (user_id, token)")), 49 | &[] 50 | ) 51 | } 52 | } 53 | 54 | 55 | #[derive(Clone)] 56 | pub(super) struct Logger; 57 | impl FangAction for Logger { 58 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 59 | worker::console_log!("{req:?}"); 60 | Ok(()) 61 | } 62 | async fn back<'a>(&'a self, res: &'a mut Response) { 63 | worker::console_log!("{res:?}"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ohkami/src/router/util.rs: -------------------------------------------------------------------------------- 1 | /// returning `(next_section, remaining )` or `(path, empty)` 2 | #[inline(always)] 3 | pub(super) fn split_next_section(path: &[u8]) -> (&[u8], &[u8]) { 4 | let ptr = path.as_ptr(); 5 | let len = path.len(); 6 | for i in 0..len { 7 | if &b'/' == unsafe { path.get_unchecked(i) } { 8 | return unsafe { 9 | ( 10 | std::slice::from_raw_parts(ptr, i), 11 | std::slice::from_raw_parts(ptr.add(i), len - i), 12 | ) 13 | }; 14 | } 15 | } 16 | (path, b"") 17 | } 18 | 19 | #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 20 | pub(crate) struct ID(usize); 21 | impl ID { 22 | pub(super) fn new() -> Self { 23 | use std::sync::atomic::{AtomicUsize, Ordering}; 24 | 25 | static ID: AtomicUsize = AtomicUsize::new(1); 26 | Self(ID.fetch_add(1, Ordering::Relaxed)) 27 | } 28 | } 29 | impl std::fmt::Debug for ID { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | write!(f, "{}", self.0) 32 | } 33 | } 34 | 35 | #[cfg(feature = "DEBUG")] 36 | pub(super) struct DebugSimpleOption<'option, T: std::fmt::Debug>(pub(super) &'option Option); 37 | #[cfg(feature = "DEBUG")] 38 | impl<'option, T: std::fmt::Debug> std::fmt::Debug for DebugSimpleOption<'option, T> { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | match self.0 { 41 | Some(t) => write!(f, "Some({t:?})"), 42 | None => f.write_str("None"), 43 | } 44 | } 45 | } 46 | 47 | #[cfg(feature = "DEBUG")] 48 | pub(super) struct DebugSimpleIterator + Clone>(pub(super) I); 49 | #[cfg(feature = "DEBUG")] 50 | impl + Clone> std::fmt::Debug for DebugSimpleIterator { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | f.write_str(&{ 53 | let mut buf = String::new(); 54 | buf.push('['); 55 | for item in self.0.clone() { 56 | buf.push_str(&format!("{item:?}")); 57 | buf.push(','); 58 | } 59 | if buf.ends_with(',') { 60 | buf.pop(); 61 | } 62 | buf.push(']'); 63 | buf 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples/worker-bindings-jsonc/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// almost the same as `worker-bindings`, but using `wrangler.jsonc` instead of toml 2 | 3 | use ohkami::bindings; 4 | 5 | #[bindings] 6 | struct AutoBindings; 7 | 8 | #[bindings] 9 | struct ManualBindings { 10 | /* automatically `#[allow(unused)]` */ 11 | VARIABLE_1: bindings::Var, 12 | 13 | #[allow(unused)] 14 | DB: bindings::D1, 15 | 16 | #[allow(unused)] 17 | MY_KVSTORE: bindings::KV, 18 | } 19 | 20 | macro_rules! static_assert_eq_str { 21 | ($left:expr, $right:literal) => { 22 | const _: [(); true as usize] = [(); 'eq: { 23 | let (left, right) = ($left.as_bytes(), $right.as_bytes()); 24 | if left.len() != right.len() { 25 | break 'eq false 26 | } 27 | let mut i = 0; while i < left.len() { 28 | if left[i] != right[i] { 29 | break 'eq false 30 | } 31 | i += 1; 32 | } 33 | true 34 | } as usize]; 35 | }; 36 | } 37 | 38 | fn __test_auto_bindings__(bindings: AutoBindings) { 39 | fn assert_send_sync() {} 40 | assert_send_sync::(); 41 | 42 | static_assert_eq_str!(AutoBindings::VARIABLE_1, "hoge"); 43 | static_assert_eq_str!(AutoBindings::VARIABLE_2, "super fun"); 44 | 45 | let _: worker::Ai = bindings.INTELIGENT; 46 | 47 | let _: worker::D1Database = bindings.DB; 48 | 49 | let _: worker::kv::KvStore = bindings.MY_KVSTORE; 50 | 51 | let _: worker::Bucket = bindings.MY_BUCKET; 52 | 53 | let _: worker::Fetcher = bindings.S; 54 | 55 | let _: worker::Queue = bindings.MY_QUEUE; 56 | 57 | let _: worker::ObjectNamespace = bindings.RATE_LIMITER; 58 | 59 | let _: worker::Hyperdrive = bindings.HYPERDRIVE; 60 | } 61 | 62 | fn __test_manual_bindings__(bindings: ManualBindings) { 63 | fn assert_send_sync() {} 64 | assert_send_sync::(); 65 | 66 | static_assert_eq_str!(ManualBindings::VARIABLE_1, "hoge"); 67 | 68 | let _: worker::D1Database = bindings.DB; 69 | 70 | let _: worker::kv::KvStore = bindings.MY_KVSTORE; 71 | } 72 | 73 | fn __test_bindings_new__(env: &worker::Env) -> Result<(), worker::Error> { 74 | let _: AutoBindings = AutoBindings::new(env)?; 75 | let _: ManualBindings = ManualBindings::new(env)?; 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /ohkami/src/claw/content/json.rs: -------------------------------------------------------------------------------- 1 | use super::super::bound::{self, Incoming, Outgoing}; 2 | use super::{FromContent, IntoContent}; 3 | use std::borrow::Cow; 4 | 5 | #[cfg(feature = "openapi")] 6 | use crate::openapi; 7 | 8 | /// # JSON format 9 | /// 10 | /// When `openapi` feature is activated, schema bound additionally 11 | /// requires `openapi::Schema`. 12 | /// 13 | /// ## Request 14 | /// 15 | /// - content type: `application/json` 16 | /// - schema bound: `Deserialize<'_>` 17 | /// 18 | /// ### example 19 | /// 20 | /// ``` 21 | /// # enum MyError {} 22 | /// use ohkami::claw::Json; 23 | /// use ohkami::serde::Deserialize; 24 | /// 25 | /// #[derive(Deserialize)] 26 | /// struct CreateUserRequest<'req> { 27 | /// name: &'req str, 28 | /// age: Option, 29 | /// } 30 | /// 31 | /// async fn create_user( 32 | /// Json(body): Json>, 33 | /// ) -> Result<(), MyError> { 34 | /// todo!() 35 | /// } 36 | /// ``` 37 | /// 38 | /// ## Response 39 | /// 40 | /// - content type: `application/json` 41 | /// - schema bound: `Serialize` 42 | /// 43 | /// ### example 44 | /// 45 | /// ``` 46 | /// # enum MyError {} 47 | /// use ohkami::claw::Json; 48 | /// use ohkami::serde::Serialize; 49 | /// 50 | /// #[derive(Serialize)] 51 | /// struct User { 52 | /// name: String, 53 | /// age: Option, 54 | /// } 55 | /// 56 | /// async fn get_user( 57 | /// id: &str, 58 | /// ) -> Result, MyError> { 59 | /// todo!() 60 | /// } 61 | /// ``` 62 | pub struct Json(pub T); 63 | 64 | impl<'req, T: Incoming<'req>> FromContent<'req> for Json { 65 | const MIME_TYPE: &'static str = "application/json"; 66 | 67 | #[inline] 68 | fn from_content(body: &'req [u8]) -> Result { 69 | serde_json::from_slice(body).map(Json) 70 | } 71 | 72 | #[cfg(feature = "openapi")] 73 | fn openapi_requestbody() -> impl Into { 74 | T::schema() 75 | } 76 | } 77 | 78 | impl IntoContent for Json { 79 | const CONTENT_TYPE: &'static str = "application/json"; 80 | 81 | #[inline] 82 | fn into_content(self) -> Result, impl std::fmt::Display> { 83 | serde_json::to_vec(&self.0).map(Cow::Owned) 84 | } 85 | 86 | #[cfg(feature = "openapi")] 87 | fn openapi_responsebody() -> impl Into { 88 | T::schema() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ohkami/src/claw/content/text.rs: -------------------------------------------------------------------------------- 1 | use super::{FromContent, IntoContent}; 2 | use std::borrow::Cow; 3 | 4 | #[cfg(feature = "openapi")] 5 | use crate::openapi; 6 | 7 | /// # plain text format 8 | /// 9 | /// ## Request 10 | /// 11 | /// - content type: `text/html; charset=UTF-8` 12 | /// - schema bound: `From<&'_ str>` 13 | /// 14 | /// ### example 15 | /// 16 | /// ``` 17 | /// use ohkami::claw::content::Text; 18 | /// 19 | /// async fn accept_text( 20 | /// Text(text): Text<&str>, 21 | /// ) { 22 | /// println!("got plain text request: {text}"); 23 | /// } 24 | /// ``` 25 | /// 26 | /// ## Response 27 | /// 28 | /// - content type: `text/html; charset=UTF-8` 29 | /// - schema bound: `Into>` 30 | /// 31 | /// ### note 32 | /// 33 | /// For `&'static str`, `String` and `Cow<'static, str>`, this is 34 | /// useless because they can be directly available as plain text 35 | /// response. 36 | /// 37 | /// ### example 38 | /// 39 | /// ``` 40 | /// use ohkami::claw::content::Text; 41 | /// 42 | /// async fn handler() -> Text<&'static str> { 43 | /// Text(r#" 44 | /// 45 | /// 46 | /// Sample Document 47 | /// 48 | /// 49 | ///

Sample Document

50 | /// 51 | /// 52 | /// "#) 53 | /// } 54 | /// ``` 55 | pub struct Text(pub T); 56 | 57 | impl<'req, T: From<&'req str>> FromContent<'req> for Text { 58 | const MIME_TYPE: &'static str = "text/plain"; 59 | 60 | fn from_content(body: &'req [u8]) -> Result { 61 | std::str::from_utf8(body).map(|s| Text(s.into())) 62 | } 63 | 64 | #[cfg(feature = "openapi")] 65 | fn openapi_requestbody() -> impl Into { 66 | openapi::string() 67 | } 68 | } 69 | 70 | impl>> IntoContent for Text { 71 | const CONTENT_TYPE: &'static str = "text/plain; charset=UTF-8"; 72 | 73 | fn into_content(self) -> Result, impl std::fmt::Display> { 74 | Result::<_, std::convert::Infallible>::Ok(match self.0.into() { 75 | Cow::Owned(s) => Cow::Owned(s.into_bytes()), 76 | Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()), 77 | }) 78 | } 79 | 80 | #[cfg(feature = "openapi")] 81 | fn openapi_responsebody() -> impl Into { 82 | openapi::string() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/websocket/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ohkami WebSocket Example 7 | 27 | 28 | 29 |

Echo Text Sample

30 | 31 |
    32 |
  • Press connect and see dev console.
  • 33 |
  • Type some text and press send.
  • 34 |
  • Finally send close to finish the connection.
  • 35 |
36 | 37 |
38 |
39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 | 48 |
49 | 50 | 51 |
52 | 53 |
54 | 55 | 56 |
57 |
58 | 59 | 69 | 70 | -------------------------------------------------------------------------------- /ohkami_macros/src/openapi/attributes.rs: -------------------------------------------------------------------------------- 1 | mod openapi; 2 | mod serde; 3 | 4 | use syn::Attribute; 5 | 6 | #[derive(Default)] 7 | pub(super) struct ContainerAttributes { 8 | pub(super) openapi: openapi::ContainerAttributes, 9 | pub(super) serde: serde::ContainerAttributes, 10 | } 11 | impl ContainerAttributes { 12 | pub(super) fn new(attrs: &[Attribute]) -> syn::Result { 13 | let mut this = ContainerAttributes::default(); 14 | for a in attrs { 15 | let Ok(a) = a.meta.require_list() else { 16 | continue; 17 | }; 18 | if a.path.get_ident().is_some_and(|i| i == "openapi") { 19 | this.openapi = a.parse_args()?; 20 | } 21 | if a.path.get_ident().is_some_and(|i| i == "serde") { 22 | this.serde = a.parse_args()?; 23 | } 24 | } 25 | Ok(this) 26 | } 27 | } 28 | 29 | #[derive(Default)] 30 | pub(super) struct FieldAttributes { 31 | pub(super) openapi: openapi::FieldAttributes, 32 | pub(super) serde: serde::FieldAttributes, 33 | } 34 | impl FieldAttributes { 35 | pub(super) fn new(attrs: &[Attribute]) -> syn::Result { 36 | let mut this = FieldAttributes::default(); 37 | for a in attrs { 38 | let Ok(a) = a.meta.require_list() else { 39 | continue; 40 | }; 41 | if a.path.get_ident().is_some_and(|i| i == "openapi") { 42 | this.openapi = a.parse_args()?; 43 | } 44 | if a.path.get_ident().is_some_and(|i| i == "serde") { 45 | this.serde = a.parse_args()?; 46 | } 47 | } 48 | Ok(this) 49 | } 50 | } 51 | 52 | #[derive(Default)] 53 | pub(super) struct VariantAttributes { 54 | pub(super) openapi: openapi::VariantAttributes, 55 | pub(super) serde: serde::VariantAttributes, 56 | } 57 | impl VariantAttributes { 58 | pub(super) fn new(attrs: &[Attribute]) -> syn::Result { 59 | let mut this = VariantAttributes::default(); 60 | for a in attrs { 61 | let Ok(a) = a.meta.require_list() else { 62 | continue; 63 | }; 64 | if a.path.get_ident().is_some_and(|i| i == "openapi") { 65 | this.openapi = a.parse_args()?; 66 | } 67 | if a.path.get_ident().is_some_and(|i| i == "serde") { 68 | this.serde = a.parse_args()?; 69 | } 70 | } 71 | Ok(this) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ohkami_lib/src/serde_cookie/_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use crate::serde_cookie; 4 | use ::serde::{Deserialize, Serialize}; 5 | use std::borrow::Cow; 6 | 7 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 8 | struct Age(u8); 9 | 10 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 11 | enum Gender { 12 | #[serde(rename = "male")] 13 | Male, 14 | #[serde(rename = "female")] 15 | Felmale, 16 | #[serde(rename = "other")] 17 | Other, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 21 | struct UserInfo<'s> { 22 | name: Cow<'s, str>, 23 | age: Option, 24 | gender: Option, 25 | } 26 | 27 | #[test] 28 | fn simple_ascii_cookies() { 29 | assert_eq!( 30 | serde_cookie::from_str::("name=ohkami; age=4").unwrap(), 31 | UserInfo { 32 | name: Cow::Borrowed("ohkami"), 33 | age: Some(Age(4)), 34 | gender: None, 35 | } 36 | ); 37 | 38 | assert_eq!( 39 | serde_cookie::from_str::("age=4; name=ohkami; gender=other").unwrap(), 40 | UserInfo { 41 | name: Cow::Borrowed("ohkami"), 42 | age: Some(Age(4)), 43 | gender: Some(Gender::Other), 44 | } 45 | ); 46 | } 47 | 48 | #[test] 49 | fn simple_ascii_cookies_with_double_quoted_values() { 50 | assert_eq!( 51 | serde_cookie::from_str::(r#"name="ohkami"; age=4"#).unwrap(), 52 | UserInfo { 53 | name: Cow::Borrowed("ohkami"), 54 | age: Some(Age(4)), 55 | gender: None, 56 | } 57 | ); 58 | 59 | assert_eq!( 60 | serde_cookie::from_str::(r#"age=4; name="ohkami"; gender="other""#).unwrap(), 61 | UserInfo { 62 | name: Cow::Borrowed("ohkami"), 63 | age: Some(Age(4)), 64 | gender: Some(Gender::Other), 65 | } 66 | ); 67 | } 68 | 69 | #[test] 70 | fn nonascii_encoded_cookies() { 71 | assert_eq!( 72 | serde_cookie::from_str::("name=%E7%8B%BC; age=4").unwrap(), 73 | UserInfo { 74 | name: Cow::Borrowed("狼"), 75 | age: Some(Age(4)), 76 | gender: None, 77 | } 78 | ); 79 | 80 | assert_eq!( 81 | serde_cookie::from_str::("age=4; name=\"%E7%8B%BC\"; gender=other").unwrap(), 82 | UserInfo { 83 | name: Cow::Borrowed("狼"), 84 | age: Some(Age(4)), 85 | gender: Some(Gender::Other), 86 | } 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /ohkami_lib/src/serde_urlencoded.rs: -------------------------------------------------------------------------------- 1 | mod de; 2 | mod ser; 3 | 4 | #[cfg(test)] 5 | mod _test; 6 | 7 | #[inline] 8 | pub fn to_string(value: &impl serde::Serialize) -> Result { 9 | let mut s = ser::URLEncodedSerializer::new(); 10 | value.serialize(&mut s)?; 11 | Ok(s.output()) 12 | } 13 | 14 | #[inline(always)] 15 | pub fn from_bytes<'de, D: serde::Deserialize<'de>>(input: &'de [u8]) -> Result { 16 | let mut d = de::URLEncodedDeserializer::new(input); 17 | let t = D::deserialize(&mut d)?; 18 | d.remaining().is_empty().then_some(t).ok_or_else(|| { 19 | serde::de::Error::custom(format!( 20 | "Unexpected trailing charactors: `{}`", 21 | d.remaining().escape_ascii() 22 | )) 23 | }) 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct Error(String); 28 | const _: () = { 29 | impl std::fmt::Display for Error { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | f.write_str(&self.0) 32 | } 33 | } 34 | impl std::error::Error for Error {} 35 | 36 | impl serde::ser::Error for Error { 37 | fn custom(msg: T) -> Self 38 | where 39 | T: std::fmt::Display, 40 | { 41 | Self(msg.to_string()) 42 | } 43 | } 44 | impl serde::de::Error for Error { 45 | fn custom(msg: T) -> Self 46 | where 47 | T: std::fmt::Display, 48 | { 49 | Self(msg.to_string()) 50 | } 51 | } 52 | }; 53 | 54 | pub(crate) enum Infallible {} 55 | const _: () = { 56 | impl serde::ser::SerializeStructVariant for Infallible { 57 | type Ok = (); 58 | type Error = Error; 59 | 60 | fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<(), Self::Error> 61 | where 62 | T: ?Sized + serde::Serialize, 63 | { 64 | match *self {} 65 | } 66 | 67 | fn end(self) -> Result { 68 | match self {} 69 | } 70 | } 71 | 72 | impl serde::ser::SerializeTupleVariant for Infallible { 73 | type Ok = (); 74 | type Error = Error; 75 | 76 | fn serialize_field(&mut self, _: &T) -> Result<(), Self::Error> 77 | where 78 | T: ?Sized + serde::Serialize, 79 | { 80 | match *self {} 81 | } 82 | 83 | fn end(self) -> Result { 84 | match self {} 85 | } 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /samples/worker-bindings-jsonc/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | /* 2 | ../worker-bindings/wrangler.toml : 3 | 4 | ```toml 5 | name = "worker-bindings-test" 6 | 7 | [vars] 8 | VARIABLE_1 = "hoge" 9 | VARIABLE_2 = "super fun" 10 | 11 | [ai] 12 | binding = "INTELIGENT" 13 | 14 | [[d1_databases]] 15 | binding = "DB" 16 | database_name = "db" 17 | database_id = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 18 | 19 | [[kv_namespaces]] 20 | binding = "MY_KVSTORE" 21 | id = "" 22 | 23 | [[r2_buckets]] 24 | binding = 'MY_BUCKET' 25 | bucket_name = '' 26 | 27 | [[services]] 28 | binding = "S" 29 | service = "" 30 | 31 | [[queues.producers]] 32 | queue = "my-queue" 33 | binding = "MY_QUEUE" 34 | 35 | [[durable_objects.bindings]] 36 | name = "RATE_LIMITER" 37 | class_name = "RateLimiter" 38 | 39 | [[hyperdrive]] 40 | binding = "HYPERDRIVE" 41 | id = "" 42 | ``` 43 | */ 44 | 45 | { 46 | "name": "worker-bindings-test-jsonc", 47 | "vars": { 48 | "VARIABLE_1": "hoge", 49 | "VARIABLE_2": "super fun" 50 | }, 51 | "ai": { 52 | "binding": "INTELIGENT" 53 | }, 54 | "d1_databases": [ 55 | { 56 | "binding": "DB", 57 | "database_name": "db", 58 | "database_id": "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 59 | } 60 | ], 61 | "kv_namespaces": [ 62 | { 63 | "binding": "MY_KVSTORE", 64 | "id": "" 65 | } 66 | ], 67 | "r2_buckets": [ 68 | { 69 | "binding": "MY_BUCKET", 70 | "bucket_name": "" 71 | } 72 | ], 73 | "services": [ 74 | { 75 | "binding": "S", 76 | "service": "" 77 | } 78 | ], 79 | "queues": { 80 | "producers": [ 81 | { 82 | "binding": "MY_QUEUE", 83 | "queue": "my-queue" 84 | } 85 | ] 86 | }, 87 | "durable_objects": { 88 | "bindings": [ 89 | { 90 | "name": "RATE_LIMITER", 91 | "class_name": "RateLimiter" 92 | } 93 | ] 94 | }, 95 | "hyperdrive": [ 96 | { 97 | "binding": "HYPERDRIVE", 98 | "id": "" 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /ohkami/src/fang/builtin/context.rs: -------------------------------------------------------------------------------- 1 | use crate::fang::{FangAction, SendSyncOnThreaded}; 2 | use crate::{FromRequest, Request, Response}; 3 | 4 | /// # Request Context 5 | /// 6 | /// Memorize and retrieve any data within a request. 7 | /// 8 | ///
9 | /// 10 | /// ```no_run 11 | /// use ohkami::prelude::*; 12 | /// use std::sync::Arc; 13 | /// 14 | /// #[tokio::main] 15 | /// async fn main() { 16 | /// let sample_data = Arc::new(String::from("ohkami")); 17 | /// 18 | /// Ohkami::new(( 19 | /// Context::new(sample_data), // <-- 20 | /// "/hello" 21 | /// .GET(hello), 22 | /// )).howl("0.0.0.0:8080").await 23 | /// } 24 | /// 25 | /// async fn hello( 26 | /// Context(name): Context<'_, Arc>, // <-- 27 | /// ) -> String { 28 | /// format!("Hello, {name}!") 29 | /// } 30 | /// ``` 31 | #[derive(Clone, Debug)] 32 | pub struct Context<'req, T: SendSyncOnThreaded + 'static>(pub &'req T); 33 | 34 | impl Context<'static, T> 35 | where 36 | T: Clone, 37 | { 38 | /// Initialize a `FangAction` that sets the context data. 39 | #[allow(clippy::new_ret_no_self)] 40 | pub fn new(data: T) -> impl FangAction { 41 | return ContextAction(data); 42 | 43 | #[derive(Clone)] 44 | struct ContextAction(T); 45 | 46 | impl FangAction for ContextAction { 47 | #[inline] 48 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 49 | req.context.set(self.0.clone()); 50 | Ok(()) 51 | } 52 | } 53 | } 54 | } 55 | 56 | impl<'req, T: SendSyncOnThreaded + 'static> FromRequest<'req> for Context<'req, T> { 57 | type Error = std::convert::Infallible; 58 | 59 | #[inline] 60 | fn from_request(req: &'req crate::Request) -> Option> { 61 | match req.context.get::() { 62 | Some(d) => Some(Ok(Self(d))), 63 | None => { 64 | #[cfg(debug_assertions)] 65 | { 66 | crate::WARNING!("Context of `{}` doesn't exist", std::any::type_name::()) 67 | } 68 | None 69 | } 70 | } 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod test { 76 | #[test] 77 | fn context_fang_bount() { 78 | use crate::fang::{BoxedFPC, Fang}; 79 | fn assert_fang>(_: T) {} 80 | 81 | assert_fang(super::Context::new(String::new())); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ohkami/src/claw/content/urlencoded.rs: -------------------------------------------------------------------------------- 1 | use super::super::bound::{self, Incoming, Outgoing}; 2 | use super::{FromContent, IntoContent}; 3 | use ohkami_lib::serde_urlencoded; 4 | use std::borrow::Cow; 5 | 6 | #[cfg(feature = "openapi")] 7 | use crate::openapi; 8 | 9 | /// # URL encoded format 10 | /// 11 | /// When `openapi` feature is activated, schema bound additionally 12 | /// requires `openapi::Schema`. 13 | /// 14 | /// ## Request 15 | /// 16 | /// - content type: `application/x-www-form-urlencoded` 17 | /// - schema bound: `Deserialize<'_>` 18 | /// 19 | /// ### example 20 | /// 21 | /// ``` 22 | /// # enum MyError {} 23 | /// use ohkami::claw::content::UrlEncoded; 24 | /// use ohkami::serde::Deserialize; 25 | /// 26 | /// #[derive(Deserialize)] 27 | /// struct CreateUserRequest<'req> { 28 | /// name: &'req str, 29 | /// age: Option, 30 | /// } 31 | /// 32 | /// async fn create_user( 33 | /// UrlEncoded(body): UrlEncoded>, 34 | /// ) -> Result<(), MyError> { 35 | /// todo!() 36 | /// } 37 | /// ``` 38 | /// 39 | /// ## Response 40 | /// 41 | /// - content type: `application/x-www-form-urlencoded` 42 | /// - schema bound: `Serialize` 43 | /// 44 | /// ### example 45 | /// 46 | /// ``` 47 | /// # enum MyError {} 48 | /// use ohkami::claw::content::UrlEncoded; 49 | /// use ohkami::serde::Serialize; 50 | /// 51 | /// #[derive(Serialize)] 52 | /// struct User { 53 | /// name: String, 54 | /// age: Option, 55 | /// } 56 | /// 57 | /// async fn get_user( 58 | /// id: &str, 59 | /// ) -> Result, MyError> { 60 | /// todo!() 61 | /// } 62 | /// ``` 63 | pub struct UrlEncoded(pub T); 64 | 65 | impl<'req, T: Incoming<'req>> FromContent<'req> for UrlEncoded { 66 | const MIME_TYPE: &'static str = "application/x-www-form-urlencoded"; 67 | 68 | fn from_content(body: &'req [u8]) -> Result { 69 | serde_urlencoded::from_bytes(body).map(UrlEncoded) 70 | } 71 | 72 | #[cfg(feature = "openapi")] 73 | fn openapi_requestbody() -> impl Into { 74 | T::schema() 75 | } 76 | } 77 | 78 | impl IntoContent for UrlEncoded { 79 | const CONTENT_TYPE: &'static str = "application/x-www-form-urlencoded"; 80 | 81 | fn into_content(self) -> Result, impl std::fmt::Display> { 82 | serde_urlencoded::to_string(&self.0).map(|s| Cow::Owned(s.into_bytes())) 83 | } 84 | 85 | #[cfg(feature = "openapi")] 86 | fn openapi_responsebody() -> impl Into { 87 | T::schema() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ohkami/src/ws/mod.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "ws")] 2 | 3 | #[cfg(feature = "__rt_native__")] 4 | mod native; 5 | #[cfg(feature = "__rt_native__")] 6 | pub use self::native::*; 7 | 8 | #[cfg(feature = "rt_worker")] 9 | mod worker; 10 | #[cfg(feature = "rt_worker")] 11 | pub use self::worker::*; 12 | 13 | /// # Context for WebSocket handshake 14 | /// 15 | /// `.upgrade(~)` performs handshake and creates a WebSocket session. 16 | /// 17 | /// ### note 18 | /// 19 | /// On native runtimes, the session is timeout in 3600 seconds ( = 1 hour ) 20 | /// by default. This is configurable by `OHKAMI_WEBSOCKET_TIMEOUT` 21 | /// environment variable. 22 | /// 23 | ///
24 | /// 25 | /// *example.rs* 26 | /// ``` 27 | /// use ohkami::ws::{WebSocketContext, WebSocket}; 28 | /// 29 | /// async fn ws(ctx: WebSocketContext<'_>) -> WebSocket { 30 | /// ctx.upgrade(|mut conn| async move { 31 | /// conn.send("Hello, WebSocket! and bye...").await 32 | /// .expect("failed to send") 33 | /// }) 34 | /// } 35 | /// ``` 36 | pub struct WebSocketContext<'req> { 37 | #[allow(unused/* on rt_worker */)] 38 | sec_websocket_key: &'req str, 39 | } 40 | 41 | impl<'req> crate::FromRequest<'req> for WebSocketContext<'req> { 42 | type Error = crate::Response; 43 | 44 | #[inline] 45 | fn from_request(req: &'req crate::Request) -> Option> { 46 | #[cold] 47 | #[inline(never)] 48 | fn reject(message: &'static str) -> crate::Response { 49 | crate::Response::BadRequest().with_text(message) 50 | } 51 | 52 | if !matches!(req.headers.connection()?, "Upgrade" | "upgrade") { 53 | return Some(Err(reject( 54 | "upgrade request must have `Connection: Upgrade`", 55 | ))); 56 | } 57 | if !(req.headers.upgrade()?.eq_ignore_ascii_case("websocket")) { 58 | return Some(Err(reject( 59 | "upgrade request must have `Upgrade: websocket`", 60 | ))); 61 | } 62 | if req.headers.sec_websocket_version()? != "13" { 63 | return Some(Err(reject( 64 | "upgrade request must have `Sec-WebSocket-Version: 13`", 65 | ))); 66 | } 67 | 68 | req.headers 69 | .sec_websocket_key() 70 | .map(|sec_websocket_key| Ok(Self { sec_websocket_key })) 71 | } 72 | } 73 | 74 | impl<'req> WebSocketContext<'req> { 75 | pub fn new(sec_websocket_key: &'req str) -> Self { 76 | Self { sec_websocket_key } 77 | } 78 | 79 | /* 80 | `.upgrade(~)` and something are implemented in 81 | `native` or `worker` submodule 82 | */ 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ['main', 'v*'] 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | CI: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | toolchain: ['stable', 'nightly'] 17 | task: ['check', 'test:core', 'test:other', 'bench:dryrun'] 18 | 19 | steps: 20 | - uses: actions/checkout@v5 21 | 22 | - name: Setup mold 23 | run: | 24 | sudo apt install mold clang 25 | echo '[target.x86_64-unknown-linux-gnu]' >> $HOME/.cargo/config.toml 26 | echo 'linker = "clang"' >> $HOME/.cargo/config.toml 27 | echo 'rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]' >> $HOME/.cargo/config.toml 28 | 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: latest 32 | 33 | - run: npm install -g wrangler 34 | 35 | - name: Setup Rust toolchain 36 | run: | 37 | rustup update 38 | rustup default ${{ matrix.toolchain }} 39 | rustup target add wasm32-unknown-unknown 40 | rustup component add rustfmt clippy 41 | 42 | - name: Cache cargo bin 43 | id: cache_cargo_bin 44 | uses: actions/cache@v4 45 | with: 46 | key: ${{ runner.os }}-cargo-bin 47 | path: ~/.cargo/bin 48 | - name: Install cargo commands 49 | if: ${{ steps.cache_cargo_bin.outputs.cache-hit != 'true' }} 50 | run: | 51 | cargo install sqlx-cli --no-default-features --features native-tls,postgres 52 | cargo install sccache --locked 53 | cargo install wasm-pack worker-build 54 | 55 | - name: Setup sccache 56 | run: | 57 | echo '[build]' >> $HOME/.cargo/config.toml 58 | echo "rustc-wrapper = \"$HOME/.cargo/bin/sccache\"" >> $HOME/.cargo/config.toml 59 | - name: Cache sccahe dir 60 | id: cahce_sccahe_dir 61 | # cache sccache directory after the most large task 62 | # in order to maximize cache hit 63 | if: ${{ matrix.task == 'test:other' }} 64 | uses: actions/cache@v4 65 | with: 66 | key: ${{ runner.os }}-sccahe-dir 67 | path: ~/.cache/sccache 68 | 69 | - name: Run tasks 70 | if: ${{ !(matrix.toolchain == 'nightly' && matrix.task == 'check') }} # WORKAROUND for https://github.com/rust-lang/rust/issues/149692 71 | run: | 72 | sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin 73 | task ${{ matrix.task }} 74 | -------------------------------------------------------------------------------- /samples/realworld/migrations/20240108163944_create_tables.up.sql: -------------------------------------------------------------------------------- 1 | -- Add up migration script here 2 | 3 | -- core tables 4 | 5 | CREATE TABLE IF NOT EXISTS users ( 6 | id uuid NOT NULL DEFAULT gen_random_uuid(), 7 | email varchar(32) NOT NULL, 8 | name varchar(32) NOT NULL, 9 | bio varchar(512), 10 | image_url varchar(64), 11 | 12 | PRIMARY KEY (id) 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS articles ( 16 | id uuid NOT NULL DEFAULT gen_random_uuid(), 17 | author_id uuid NOT NULL, 18 | slug varchar(128) NOT NULL, 19 | title varchar(128) NOT NULL, 20 | description varchar(512) NOT NULL, 21 | body text NOT NULL, 22 | created_at timestamptz NOT NULL DEFAULT now(), 23 | updated_at timestamptz NOT NULL DEFAULT now(), 24 | 25 | PRIMARY KEY (id), 26 | UNIQUE (slug), 27 | FOREIGN KEY (author_id) REFERENCES users (id) 28 | ); 29 | 30 | CREATE TABLE IF NOT EXISTS comments ( 31 | id int NOT NULL, 32 | article_id uuid NOT NULL, 33 | author_id uuid NOT NULL, 34 | content text NOT NULL, 35 | created_at timestamptz NOT NULL DEFAULT now(), 36 | updated_at timestamptz NOT NULL DEFAULT now(), 37 | 38 | PRIMARY KEY (id, article_id), 39 | FOREIGN KEY (article_id) REFERENCES articles (id), 40 | FOREIGN KEY (author_id) REFERENCES users (id) 41 | ); 42 | 43 | CREATE TABLE IF NOT EXISTS tags ( 44 | id serial NOT NULL, 45 | name varchar(32) NOT NULL, 46 | 47 | PRIMARY KEY (id), 48 | UNIQUE(name) 49 | ); 50 | 51 | -- intermediate tables 52 | 53 | CREATE TABLE IF NOT EXISTS articles_have_tags ( 54 | id uuid NOT NULL DEFAULT gen_random_uuid(), 55 | article_id uuid NOT NULL, 56 | tag_id int NOT NULL, 57 | 58 | PRIMARY KEY (id), 59 | FOREIGN KEY (article_id) REFERENCES articles (id), 60 | FOREIGN KEY (tag_id) REFERENCES tags (id) 61 | ); 62 | 63 | CREATE TABLE IF NOT EXISTS users_follow_users ( 64 | id uuid NOT NULL DEFAULT gen_random_uuid(), 65 | follower_id uuid NOT NULL, 66 | followee_id uuid NOT NULL, 67 | 68 | PRIMARY KEY (id), 69 | FOREIGN KEY (follower_id) REFERENCES users (id), 70 | FOREIGN KEY (followee_id) REFERENCES users (id) 71 | ); 72 | 73 | CREATE TABLE IF NOT EXISTS users_favorite_articles ( 74 | id uuid NOT NULL DEFAULT gen_random_uuid(), 75 | user_id uuid NOT NULL, 76 | article_id uuid NOT NULL, 77 | 78 | PRIMARY KEY (id), 79 | FOREIGN KEY (user_id) REFERENCES users (id), 80 | FOREIGN KEY (article_id) REFERENCES articles (id) 81 | ); 82 | -------------------------------------------------------------------------------- /benches/src/request_headers/headerhashmap.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, hash::{BuildHasherDefault, Hasher}, ops::BitXor}; 2 | use ohkami_lib::{CowSlice, Slice}; 3 | 4 | 5 | pub type HeaderHashMap = HashMap>; 6 | 7 | 8 | #[cfg(target_pointer_width = "32")] 9 | const K: usize = 0x9e3779b9; 10 | #[cfg(target_pointer_width = "64")] 11 | const K: usize = 0x517cc1b727220a95; 12 | 13 | #[inline(always)] 14 | fn take_first_chunk<'s, const N: usize>(slice: &mut &'s [u8]) -> Option<&'s [u8; N]> { 15 | let (first, tail) = slice.split_first_chunk()?; 16 | *slice = tail; 17 | Some(first) 18 | } 19 | 20 | #[inline(always)] 21 | const fn ignore_case(byte: u8) -> u8 { 22 | const CASE_DIFF: u8 = b'a' - b'A'; 23 | match byte { 24 | b'A'..=b'Z' => byte + CASE_DIFF, 25 | _ => byte, 26 | } 27 | } 28 | 29 | #[derive(Clone)] 30 | pub struct HeaderHasher { 31 | hash: usize, 32 | } 33 | 34 | impl Default for HeaderHasher { 35 | #[inline(always)] 36 | fn default() -> Self { 37 | Self { hash: 0 } 38 | } 39 | } 40 | 41 | impl HeaderHasher { 42 | #[inline(always)] 43 | fn add(&mut self, word: usize) { 44 | self.hash = self.hash.rotate_left(5).bitxor(word).wrapping_mul(K); 45 | } 46 | } 47 | 48 | impl Hasher for HeaderHasher { 49 | #[inline] 50 | fn write(&mut self, mut bytes: &[u8]) { 51 | let mut state = self.clone(); 52 | 53 | while let Some(&[a, b, c, d, e, f, g, h]) = take_first_chunk(&mut bytes) { 54 | state.add(usize::from_ne_bytes([ 55 | ignore_case(a), 56 | ignore_case(b), 57 | ignore_case(c), 58 | ignore_case(d), 59 | ignore_case(e), 60 | ignore_case(f), 61 | ignore_case(g), 62 | ignore_case(h), 63 | ])); 64 | } 65 | if let Some(&[a, b, c, d]) = take_first_chunk(&mut bytes) { 66 | state.add(u32::from_ne_bytes([ 67 | ignore_case(a), 68 | ignore_case(b), 69 | ignore_case(c), 70 | ignore_case(d), 71 | ]) as usize); 72 | } 73 | if let Some(&[a, b]) = take_first_chunk(&mut bytes) { 74 | state.add(u16::from_ne_bytes([ 75 | ignore_case(a), 76 | ignore_case(b), 77 | ]) as usize); 78 | } 79 | if let Some(&[a]) = take_first_chunk(&mut bytes) { 80 | state.add(ignore_case(a) as usize); 81 | } 82 | 83 | *self = state; 84 | } 85 | 86 | #[inline(always)] 87 | fn finish(&self) -> u64 { 88 | self.hash as _ 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ohkami_lib/src/num.rs: -------------------------------------------------------------------------------- 1 | #[inline] 2 | pub fn hexized(n: usize) -> String { 3 | unsafe { String::from_utf8_unchecked(hexized_bytes(n).into()) } 4 | } 5 | 6 | #[inline(always)] 7 | pub fn hexized_bytes(n: usize) -> [u8; std::mem::size_of::() * 2] { 8 | use std::mem::{size_of, transmute}; 9 | 10 | unsafe { 11 | transmute::<[[u8; 2]; size_of::()], [u8; size_of::() * 2]>( 12 | n.to_be_bytes().map(|byte| [byte >> 4, byte & 0b1111]), 13 | ) 14 | .map(|h| { 15 | h + match h { 16 | 0..=9 => b'0', 17 | 10..=15 => b'a' - 10, 18 | _ => std::hint::unreachable_unchecked(), 19 | } 20 | }) 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | #[test] 26 | fn test_hexize() { 27 | for (n, expected) in [ 28 | (1, "1"), 29 | (9, "9"), 30 | (12, "c"), 31 | (16, "10"), 32 | (42, "2a"), 33 | (314, "13a"), 34 | ] { 35 | assert_eq!(hexized(n).trim_start_matches('0'), expected) 36 | } 37 | } 38 | 39 | #[inline] 40 | pub fn itoa(mut n: usize) -> String { 41 | const MAX: usize = usize::ilog10(usize::MAX) as _; 42 | 43 | #[cfg(target_pointer_width = "64")] 44 | const _/* static assert */: [(); 19] = [(); MAX]; 45 | 46 | let mut buf = Vec::::with_capacity(1 + MAX); 47 | 48 | { 49 | let mut push_unchecked = |byte| { 50 | let len = buf.len(); 51 | unsafe { 52 | std::ptr::write(buf.as_mut_ptr().add(len), byte); 53 | buf.set_len(len + 1); 54 | } 55 | }; 56 | 57 | macro_rules! unroll { 58 | () => {}; 59 | ($digit:expr) => {unroll!($digit,)}; 60 | ($digit:expr, $($tail:tt)*) => { 61 | if $digit <= MAX && n >= 10_usize.pow($digit) { 62 | unroll!($($tail)*); 63 | let q = n / 10_usize.pow($digit); 64 | push_unchecked(b'0' + q as u8); 65 | n -= 10_usize.pow($digit) * q 66 | } 67 | }; 68 | } 69 | 70 | unroll!( 71 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 72 | ); 73 | 74 | push_unchecked(b'0' + n as u8); 75 | } 76 | 77 | unsafe { String::from_utf8_unchecked(buf) } 78 | } 79 | 80 | #[cfg(test)] 81 | #[test] 82 | fn test_itoa() { 83 | for n in [ 84 | 0, 85 | 1, 86 | 4, 87 | 10, 88 | 11, 89 | 99, 90 | 100, 91 | 109, 92 | 999, 93 | 1000, 94 | 10_usize.pow(usize::ilog10(usize::MAX)) - 1, 95 | 10_usize.pow(usize::ilog10(usize::MAX)), 96 | usize::MAX - 1, 97 | usize::MAX, 98 | ] { 99 | assert_eq!(itoa(n), n.to_string()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ohkami_macros/src/openapi/attributes/serde/case.rs: -------------------------------------------------------------------------------- 1 | //! serde case specifiers 2 | //! 3 | //! based on https://github.com/serde-rs/serde/blob/930401b0dd58a809fce34da091b8aa3d6083cb33/serde_derive/src/internals/case.rs 4 | 5 | #[derive(Clone, Copy, PartialEq)] 6 | pub(crate) enum Case { 7 | Lower, 8 | Upper, 9 | Pascal, 10 | Camel, 11 | Snake, 12 | ScreamingSnake, 13 | Kebab, 14 | ScreamingKebab, 15 | } 16 | 17 | impl From for Case { 18 | fn from(s: String) -> Self { 19 | Self::from_str(&s).expect("unexpected case specifier") 20 | } 21 | } 22 | 23 | impl Case { 24 | pub(crate) const fn from_str(s: &str) -> Option { 25 | match s.as_bytes() { 26 | b"lowercase" => Some(Self::Lower), 27 | b"UPPERCASE" => Some(Self::Upper), 28 | b"PascalCase" => Some(Self::Pascal), 29 | b"camelCase" => Some(Self::Camel), 30 | b"snake_case" => Some(Self::Snake), 31 | b"SCREAMING_SNAKE_CASE" => Some(Self::ScreamingSnake), 32 | b"kebab-case" => Some(Self::Kebab), 33 | b"SCREAMING-KEBAB-CASE" => Some(Self::ScreamingKebab), 34 | _ => None, 35 | } 36 | } 37 | 38 | pub(crate) fn apply_to_field(self, field: &str) -> String { 39 | match self { 40 | Self::Lower | Self::Snake => field.to_string(), 41 | Self::Upper => field.to_ascii_uppercase(), 42 | Self::Pascal => field 43 | .split('_') 44 | .map(|s| s[..1].to_ascii_uppercase() + &s[1..]) 45 | .collect(), 46 | Self::Camel => { 47 | let pascal = Self::Pascal.apply_to_field(field); 48 | pascal[..1].to_ascii_lowercase() + &pascal[1..] 49 | } 50 | Self::ScreamingSnake => Self::Upper.apply_to_field(field), 51 | Self::Kebab => field.replace('_', "-"), 52 | Self::ScreamingKebab => Self::ScreamingSnake.apply_to_field(field).replace('_', "-"), 53 | } 54 | } 55 | 56 | pub(crate) fn apply_to_variant(self, variant: &str) -> String { 57 | match self { 58 | Self::Pascal => variant.to_string(), 59 | Self::Lower => variant.to_ascii_lowercase(), 60 | Self::Upper => variant.to_ascii_uppercase(), 61 | Self::Camel => variant[..1].to_ascii_lowercase() + &variant[1..], 62 | Self::Snake => variant 63 | .split(char::is_uppercase) 64 | .map(str::to_ascii_lowercase) 65 | .collect::>() 66 | .join("_"), 67 | Self::ScreamingSnake => Self::Snake.apply_to_variant(variant).to_ascii_uppercase(), 68 | Self::Kebab => Self::Snake.apply_to_variant(variant).replace('_', "-"), 69 | Self::ScreamingKebab => Self::ScreamingSnake 70 | .apply_to_variant(variant) 71 | .replace('_', "-"), 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /samples/realworld/src/handlers/users.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::claw::status::Created; 3 | use sqlx::PgPool; 4 | use crate::{ 5 | models::User, 6 | models::response::UserResponse, 7 | models::request::{LoginRequest, LoginRequestUser, RegisterRequest, RegisterRequestUser}, 8 | errors::RealWorldError, 9 | config, 10 | db, 11 | }; 12 | 13 | 14 | pub fn users_ohkami() -> Ohkami { 15 | Ohkami::new(( 16 | "/login" 17 | .POST(login), 18 | "/" 19 | .POST(register), 20 | )) 21 | } 22 | 23 | async fn login( 24 | Context(pool): Context<'_, PgPool>, 25 | Json(LoginRequest { 26 | user: LoginRequestUser { email, password }, 27 | }): Json>, 28 | ) -> Result, RealWorldError> { 29 | let credential = sqlx::query!(r#" 30 | SELECT password, salt 31 | FROM users 32 | WHERE email = $1 33 | "#, email) 34 | .fetch_one(pool).await 35 | .map_err(RealWorldError::DB)?; 36 | 37 | db::verify_password(password, &credential.salt, &credential.password)?; 38 | 39 | let u = sqlx::query_as!(db::UserEntity, r#" 40 | SELECT id, email, name, bio, image_url 41 | FROM users AS u 42 | WHERE email = $1 43 | "#, email) 44 | .fetch_one(pool).await 45 | .map_err(RealWorldError::DB)?; 46 | 47 | Ok(Json(u.into_user_response()?)) 48 | } 49 | 50 | async fn register( 51 | Context(pool): Context<'_, PgPool>, 52 | Json(RegisterRequest { 53 | user: RegisterRequestUser { username, email, password } 54 | }): Json>, 55 | ) -> Result>, RealWorldError> { 56 | let already_exists = sqlx::query!(r#" 57 | SELECT EXISTS ( 58 | SELECT id 59 | FROM users AS u 60 | WHERE 61 | u.name = $1 62 | ) 63 | "#, username) 64 | .fetch_one(pool).await 65 | .map_err(RealWorldError::DB)? 66 | .exists.unwrap(); 67 | if already_exists { 68 | return Err(RealWorldError::Validation { 69 | body: format!("User of name {username:?} is already exists") 70 | }) 71 | } 72 | 73 | let (hased_password, salt) = db::hash_password(password)?; 74 | 75 | let new_user_id = sqlx::query!(r#" 76 | INSERT INTO 77 | users (email, name, password, salt) 78 | VALUES ($1, $2, $3, $4 ) 79 | RETURNING id 80 | "#, email, username, hased_password.as_str(), salt.as_str()) 81 | .fetch_one(pool).await 82 | .map_err(RealWorldError::DB)? 83 | .id; 84 | 85 | Ok(Created(Json( 86 | UserResponse { 87 | user: User { 88 | email: email.into(), 89 | jwt: config::issue_jwt_for_user_of_id(new_user_id)?, 90 | name: username.into(), 91 | bio: None, 92 | image: None, 93 | }, 94 | } 95 | ))) 96 | } 97 | -------------------------------------------------------------------------------- /ohkami_lib/src/serde_utf8/_test.rs: -------------------------------------------------------------------------------- 1 | use crate::serde_utf8; 2 | 3 | #[test] 4 | #[allow(clippy::bool_assert_comparison)] 5 | fn serialize_bool() { 6 | assert_eq!(serde_utf8::to_string(&true).unwrap(), "true"); 7 | assert_eq!(serde_utf8::to_string(&false).unwrap(), "false"); 8 | } 9 | 10 | #[test] 11 | fn serialize_newtype() { 12 | #[derive(serde::Serialize)] 13 | struct MyText(String); 14 | 15 | assert_eq!( 16 | serde_utf8::to_string(&MyText(String::from("Hello, serde!"))).unwrap(), 17 | "Hello, serde!" 18 | ); 19 | 20 | #[derive(serde::Serialize)] 21 | struct MyCowText(std::borrow::Cow<'static, str>); 22 | 23 | assert_eq!( 24 | serde_utf8::to_string(&MyCowText(std::borrow::Cow::Borrowed("Hello, serde!"))).unwrap(), 25 | "Hello, serde!" 26 | ); 27 | } 28 | 29 | #[test] 30 | fn serialize_enum() { 31 | #![allow(dead_code)] 32 | 33 | #[derive(serde::Serialize)] 34 | enum Color { 35 | Red, 36 | Blue, 37 | Green, 38 | } 39 | 40 | assert_eq!(serde_utf8::to_string(&Color::Blue).unwrap(), "Blue"); 41 | 42 | #[derive(serde::Serialize)] 43 | enum Color2 { 44 | #[serde(rename = "red")] 45 | Red, 46 | #[serde(rename = "blue")] 47 | Blue, 48 | #[serde(rename = "green")] 49 | Green, 50 | } 51 | 52 | assert_eq!(serde_utf8::to_string(&Color2::Blue).unwrap(), "blue"); 53 | } 54 | 55 | #[test] 56 | fn serialize_non_newtype_struct_makes_err() { 57 | #[derive(serde::Serialize)] 58 | struct User { 59 | id: usize, 60 | name: String, 61 | } 62 | 63 | assert!( 64 | serde_utf8::to_string(&User { 65 | id: 42, 66 | name: String::from("ohkami"), 67 | }) 68 | .is_err() 69 | ); 70 | } 71 | 72 | #[test] 73 | #[allow(clippy::bool_assert_comparison)] 74 | fn deserialize_bool() { 75 | assert_eq!(serde_utf8::from_str::("true").unwrap(), true); 76 | 77 | assert!(serde_utf8::from_str::("unknown").is_err(),); 78 | } 79 | 80 | #[test] 81 | fn deserialize_newtype() { 82 | #[derive(serde::Deserialize, Debug, PartialEq)] 83 | struct MyText(String); 84 | 85 | assert_eq!( 86 | serde_utf8::from_str::("Hello, serde!").unwrap(), 87 | MyText(String::from("Hello, serde!")) 88 | ); 89 | 90 | #[derive(serde::Deserialize, Debug, PartialEq)] 91 | struct MyCowText(std::borrow::Cow<'static, str>); 92 | 93 | assert_eq!( 94 | serde_utf8::from_str::("Hello, serde!").unwrap(), 95 | MyCowText(std::borrow::Cow::Borrowed("Hello, serde!")) 96 | ); 97 | } 98 | 99 | #[test] 100 | fn deserialize_non_newtype_struct_makes_err() { 101 | #[derive(serde::Deserialize)] 102 | struct User { 103 | _id: usize, 104 | _name: String, 105 | } 106 | 107 | assert!(serde_utf8::from_str::("hogefuga").is_err()); 108 | } 109 | -------------------------------------------------------------------------------- /ohkami_macros/src/from_request.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{Field, GenericParam, ItemStruct, Lifetime, LifetimeParam}; 4 | 5 | pub(super) fn derive_from_request(target: TokenStream) -> syn::Result { 6 | let s: ItemStruct = syn::parse2(target)?; 7 | 8 | let name = &s.ident; 9 | 10 | let generics_params_r = &s.generics.params; 11 | let generics_params_l = &mut generics_params_r.clone(); 12 | let generics_where = &s.generics.where_clause; 13 | 14 | let impl_lifetime = match s.generics.lifetimes().count() { 15 | 0 => { 16 | let il = GenericParam::Lifetime(LifetimeParam::new(Lifetime::new( 17 | "'__impl_from_request_lifetime", 18 | Span::call_site(), 19 | ))); 20 | generics_params_l.push(il.clone()); 21 | il 22 | } 23 | 1 => s.generics.params.first().unwrap().clone(), 24 | _ => { 25 | return Err(syn::Error::new( 26 | Span::call_site(), 27 | "#[derive(FromRequest)] doesn't support multiple lifetime params", 28 | )); 29 | } 30 | }; 31 | 32 | let build = 33 | if s.semi_token.is_none() { 34 | /* struct S { 〜 } */ 35 | let fields = s.fields.into_iter() 36 | .map(|Field { ident, ty, .. }| quote! { 37 | #ident: { 38 | match <#ty as ::ohkami::FromRequest>::from_request(req)? { 39 | ::std::result::Result::Ok(field) => field, 40 | ::std::result::Result::Err(err) => return Some(::std::result::Result::Err( 41 | ::ohkami::IntoResponse::into_response(err) 42 | )), 43 | } 44 | } 45 | }); 46 | quote![ Self { #( #fields ),* } ] 47 | } else { 48 | /* struct T(); */ 49 | let fields = s.fields.into_iter() 50 | .map(|Field { ty, .. }| quote! { 51 | { 52 | match <#ty as ::ohkami::FromRequest>::from_request(req)? { 53 | ::std::result::Result::Ok(field) => field, 54 | ::std::result::Result::Err(err) => return Some(::std::result::Result::Err( 55 | ::ohkami::IntoResponse::into_response(err) 56 | )), 57 | } 58 | } 59 | }); 60 | quote![ Self(#( #fields ),*) ] 61 | }; 62 | 63 | Ok(quote! { 64 | impl<#generics_params_l> ::ohkami::FromRequest<#impl_lifetime> for #name<#generics_params_r> 65 | #generics_where 66 | { 67 | type Error = ::ohkami::Response; 68 | fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::option::Option<::std::result::Result> { 69 | ::std::option::Option::Some(::std::result::Result::Ok(#build)) 70 | } 71 | } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /ohkami/src/request/from_request.rs: -------------------------------------------------------------------------------- 1 | use crate::{IntoResponse, Request}; 2 | 3 | #[cfg(feature = "openapi")] 4 | use crate::openapi; 5 | 6 | /// "Retirieved from a `Request`". 7 | /// 8 | /// ### required 9 | /// - `type Errpr: IntoResponse` 10 | /// - `fn from_request(req: &Request) -> Option>` 11 | /// 12 | /// Of course, you can manually implement for your structs that can be extracted from a request: 13 | /// 14 | ///
15 | /// 16 | /// *example.rs* 17 | /// ``` 18 | /// use ohkami::prelude::*; 19 | /// 20 | /// struct IsGetRequest(bool); 21 | /// 22 | /// impl ohkami::FromRequest<'_> for IsGetRequest { 23 | /// type Error = std::convert::Infallible; 24 | /// fn from_request(req: &Request) -> Option> { 25 | /// Some(Ok(Self( 26 | /// req.method.isGET() 27 | /// ))) 28 | /// } 29 | /// } 30 | /// ``` 31 | /// 32 | ///
33 | /// 34 | /// ### Note 35 | /// 36 | /// *MUST NOT impl both `FromRequest` and `FromParam`*. 37 | pub trait FromRequest<'req>: Sized { 38 | /// If this extraction never fails, `std::convert::Infallible` is recomended. 39 | type Error: IntoResponse; 40 | 41 | fn from_request(req: &'req Request) -> Option>; 42 | 43 | #[cfg(feature = "openapi")] 44 | fn openapi_inbound() -> openapi::Inbound { 45 | openapi::Inbound::None 46 | } 47 | 48 | #[doc(hidden)] 49 | /// intent to be used by `claw::param::Path` and by the assertion in `router::base::Router::finalize` 50 | fn n_pathparams() -> usize { 51 | 0 52 | } 53 | } 54 | const _: () = { 55 | impl<'req> FromRequest<'req> for &'req Request { 56 | type Error = std::convert::Infallible; 57 | fn from_request(req: &'req Request) -> Option> { 58 | Some(Ok(req)) 59 | } 60 | } 61 | impl<'req, FR: FromRequest<'req>> FromRequest<'req> for Option { 62 | type Error = FR::Error; 63 | 64 | #[inline] 65 | fn from_request(req: &'req Request) -> Option> { 66 | match FR::from_request(req) { 67 | None => Some(Ok(None)), 68 | Some(fr) => Some(fr.map(Some)), 69 | } 70 | } 71 | 72 | #[cfg(feature = "openapi")] 73 | fn openapi_inbound() -> openapi::Inbound { 74 | FR::openapi_inbound() 75 | } 76 | } 77 | }; 78 | #[cfg(feature = "rt_worker")] 79 | const _: () = { 80 | impl<'req> FromRequest<'req> for &'req ::worker::Env { 81 | type Error = std::convert::Infallible; 82 | #[inline(always)] 83 | fn from_request(req: &'req Request) -> Option> { 84 | Some(Ok(req.context.env())) 85 | } 86 | } 87 | impl<'req> FromRequest<'req> for &'req ::worker::Context { 88 | type Error = std::convert::Infallible; 89 | #[inline(always)] 90 | fn from_request(req: &'req Request) -> Option> { 91 | Some(Ok(req.context.worker())) 92 | } 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /ohkami/src/response/into_response.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use crate::{Response, Status}; 4 | 5 | #[cfg(feature = "openapi")] 6 | use crate::openapi; 7 | 8 | /// A trait implemented to be a returned value of a handler 9 | /// 10 | ///
11 | /// 12 | /// *example.rs* 13 | /// ```no_run 14 | /// use ohkami::prelude::*; 15 | /// 16 | /// struct MyResponse { 17 | /// message: String, 18 | /// } 19 | /// impl IntoResponse for MyResponse { 20 | /// fn into_response(self) -> Response { 21 | /// Response::OK().with_text(self.message) 22 | /// } 23 | /// } 24 | /// 25 | /// async fn handler() -> MyResponse { 26 | /// MyResponse { 27 | /// message: String::from("Hello!") 28 | /// } 29 | /// } 30 | /// 31 | /// #[tokio::main] 32 | /// async fn main() { 33 | /// Ohkami::new( 34 | /// "/".GET(handler) 35 | /// ).howl("localhost:5050").await 36 | /// } 37 | /// ``` 38 | pub trait IntoResponse { 39 | fn into_response(self) -> Response; 40 | 41 | #[cfg(feature = "openapi")] 42 | fn openapi_responses() -> openapi::Responses { 43 | openapi::Responses::new([]) 44 | } 45 | } 46 | 47 | impl IntoResponse for Response { 48 | #[inline] 49 | fn into_response(self) -> Response { 50 | self 51 | } 52 | 53 | #[cfg(feature = "openapi")] 54 | fn openapi_responses() -> openapi::Responses { 55 | openapi::Responses::new([]) 56 | } 57 | } 58 | 59 | impl IntoResponse for Status { 60 | #[inline(always)] 61 | fn into_response(self) -> Response { 62 | Response::new(self) 63 | } 64 | 65 | #[cfg(feature = "openapi")] 66 | fn openapi_responses() -> openapi::Responses { 67 | openapi::Responses::new([]) 68 | } 69 | } 70 | 71 | impl IntoResponse for Result { 72 | #[inline(always)] 73 | fn into_response(self) -> Response { 74 | match self { 75 | Ok(ok) => ok.into_response(), 76 | Err(e) => e.into_response(), 77 | } 78 | } 79 | 80 | #[cfg(feature = "openapi")] 81 | fn openapi_responses() -> openapi::Responses { 82 | let mut res = E::openapi_responses(); 83 | res.merge(T::openapi_responses()); 84 | res 85 | } 86 | } 87 | 88 | impl IntoResponse for std::convert::Infallible { 89 | #[cold] 90 | #[inline(never)] 91 | fn into_response(self) -> Response { 92 | unsafe { std::hint::unreachable_unchecked() } 93 | } 94 | 95 | #[cfg(feature = "openapi")] 96 | fn openapi_responses() -> openapi::Responses { 97 | openapi::Responses::new([]) 98 | } 99 | } 100 | 101 | #[cfg(feature = "rt_worker")] 102 | impl IntoResponse for worker::Error { 103 | fn into_response(self) -> Response { 104 | Response::InternalServerError().with_text(self.to_string()) 105 | } 106 | 107 | #[cfg(feature = "openapi")] 108 | fn openapi_responses() -> openapi::Responses { 109 | openapi::Responses::new([(500, openapi::Response::when("Internal error in worker"))]) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/chatgpt/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod fangs; 3 | pub mod models; 4 | 5 | use fangs::APIKey; 6 | use error::Error; 7 | use models::{ChatMessage, ChatCompletions, Role}; 8 | 9 | use ohkami::prelude::*; 10 | use ohkami::claw::content::Text; 11 | use ohkami::sse::DataStream; 12 | use ohkami::util::StreamExt; 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | let api_key = APIKey::from_env(); 17 | 18 | println!("Try:\n\ 19 | curl -v 'http://localhost:5050/chat-once' -H 'Content-Type: text/plain' -d '<your question>'\n\ 20 | "); 21 | 22 | Ohkami::new(( 23 | api_key, 24 | "/chat-once" 25 | .POST(relay_chat_completion), 26 | )).howl("localhost:5050").await 27 | } 28 | 29 | pub async fn relay_chat_completion( 30 | Context(APIKey(api_key)): Context<'_, APIKey>, 31 | Text(content): Text, 32 | ) -> Result { 33 | let mut gpt_response = reqwest::Client::new() 34 | .post("https://api.openai.com/v1/chat/completions") 35 | .bearer_auth(api_key) 36 | .json(&ChatCompletions { 37 | model: "gpt-4o", 38 | stream: true, 39 | messages: vec![ 40 | ChatMessage { 41 | role: Role::user, 42 | content, 43 | } 44 | ], 45 | }) 46 | .send().await? 47 | .bytes_stream(); 48 | 49 | Ok(DataStream::new(|mut s| async move { 50 | let mut send_line = |mut line: String| { 51 | #[cfg(debug_assertions)] { 52 | assert!(line.ends_with("\n\n")) 53 | } 54 | if line.ends_with("\n\n") { 55 | line.truncate(line.len() - 2); 56 | } 57 | 58 | #[cfg(debug_assertions)] { 59 | if line != "[DONE]" { 60 | use ohkami::serde::json; 61 | 62 | let chunk: models::ChatCompletionChunk = json::from_slice(line.as_bytes()).unwrap(); 63 | print!("{}", chunk.choices[0].delta.content.as_deref().unwrap_or("")); 64 | std::io::Write::flush(&mut std::io::stdout()).unwrap(); 65 | } else { 66 | println!() 67 | } 68 | } 69 | 70 | s.send(line); 71 | }; 72 | 73 | let mut remaining = String::new(); 74 | while let Some(Ok(raw_chunk)) = gpt_response.next().await { 75 | for line in std::str::from_utf8(&raw_chunk).unwrap() 76 | .split_inclusive("\n\n") 77 | { 78 | if let Some(data) = line.strip_prefix("data: ") { 79 | if data.ends_with("\n\n") { 80 | send_line(data.to_string()) 81 | } else { 82 | remaining = data.into() 83 | } 84 | } else { 85 | #[cfg(debug_assertions)] { 86 | assert!(line.ends_with("\n\n")) 87 | } 88 | send_line(std::mem::take(&mut remaining) + line) 89 | } 90 | } 91 | } 92 | })) 93 | } 94 | -------------------------------------------------------------------------------- /ohkami/src/response/content.rs: -------------------------------------------------------------------------------- 1 | use ohkami_lib::CowSlice; 2 | 3 | #[cfg(feature = "sse")] 4 | use ohkami_lib::Stream; 5 | 6 | #[cfg(not(feature="rt_lambda"/* currently */))] 7 | #[cfg(all(feature = "ws", feature = "__rt__"))] 8 | use crate::ws::Session; 9 | 10 | #[derive(Default)] 11 | pub enum Content { 12 | #[default] 13 | None, 14 | 15 | Payload(CowSlice), 16 | 17 | #[cfg(feature = "sse")] 18 | Stream(std::pin::Pin + Send>>), 19 | 20 | #[cfg(not(feature="rt_lambda"/* currently */))] 21 | #[cfg(all(feature = "ws", feature = "__rt__"))] 22 | WebSocket(Session), 23 | } 24 | const _: () = { 25 | impl PartialEq for Content { 26 | fn eq(&self, other: &Self) -> bool { 27 | match (self, other) { 28 | (Content::None, Content::None) => true, 29 | 30 | (Content::Payload(p1), Content::Payload(p2)) => p1 == p2, 31 | 32 | _ => false, 33 | } 34 | } 35 | } 36 | 37 | impl std::fmt::Debug for Content { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | Self::None => f.write_str("None"), 41 | 42 | Self::Payload(bytes) => f.write_str(&bytes.escape_ascii().to_string()), 43 | 44 | #[cfg(feature = "sse")] 45 | Self::Stream(_) => f.write_str("{stream}"), 46 | 47 | #[cfg(not(feature="rt_lambda"/* currently */))] 48 | #[cfg(all(feature = "ws", feature = "__rt__"))] 49 | Self::WebSocket(_) => f.write_str("{websocket}"), 50 | } 51 | } 52 | } 53 | }; 54 | 55 | impl Content { 56 | #[inline] 57 | pub const fn is_none(&self) -> bool { 58 | matches!(self, Self::None) 59 | } 60 | 61 | pub fn take(&mut self) -> Content { 62 | std::mem::take(self) 63 | } 64 | 65 | #[inline(always)] 66 | pub fn as_bytes(&self) -> Option<&[u8]> { 67 | match self { 68 | Self::Payload(bytes) => Some(bytes), 69 | _ => None, 70 | } 71 | } 72 | pub fn into_bytes(self) -> Option> { 73 | match self { 74 | Self::Payload(bytes) => Some(unsafe { bytes.into_cow_static_bytes_uncheked() }), 75 | _ => None, 76 | } 77 | } 78 | } 79 | 80 | #[cfg(feature = "rt_worker")] 81 | impl Content { 82 | pub(crate) fn into_worker_response(self) -> ::worker::Response { 83 | match self { 84 | Self::None => ::worker::Response::empty(), 85 | 86 | Self::Payload(bytes) => ::worker::Response::from_bytes(bytes.into()), 87 | 88 | #[cfg(feature = "sse")] 89 | Self::Stream(stream) => ::worker::Response::from_stream({ 90 | use {ohkami_lib::StreamExt, std::convert::Infallible}; 91 | stream.map(Result::<_, Infallible>::Ok) 92 | }), 93 | 94 | #[cfg(feature = "ws")] 95 | Self::WebSocket(ws) => ::worker::Response::from_websocket(ws), 96 | } 97 | .expect("failed to convert Ohkami Response to Workers one") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /samples/openapi-schema-from-into/openapi.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Dummy Server for serde From & Into", 5 | "version": "0" 6 | }, 7 | "servers": [], 8 | "paths": { 9 | "/from": { 10 | "get": { 11 | "operationId": "dummy", 12 | "requestBody": { 13 | "required": true, 14 | "content": { 15 | "application/json": { 16 | "schema": { 17 | "type": "object", 18 | "properties": { 19 | "age": { 20 | "type": "integer", 21 | "format": "uint8" 22 | }, 23 | "name": { 24 | "type": "string" 25 | } 26 | }, 27 | "required": [ 28 | "name", 29 | "age" 30 | ] 31 | } 32 | } 33 | } 34 | }, 35 | "responses": { 36 | "200": { 37 | "description": "OK" 38 | } 39 | } 40 | } 41 | }, 42 | "/into": { 43 | "get": { 44 | "operationId": "dummy", 45 | "responses": { 46 | "200": { 47 | "description": "OK", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "type": "object", 52 | "properties": { 53 | "user": { 54 | "type": "object", 55 | "properties": { 56 | "age": { 57 | "type": "integer", 58 | "format": "uint8" 59 | }, 60 | "name": { 61 | "type": "string" 62 | } 63 | }, 64 | "required": [ 65 | "name", 66 | "age" 67 | ] 68 | } 69 | }, 70 | "required": [ 71 | "user" 72 | ] 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | }, 80 | "/try_from": { 81 | "get": { 82 | "operationId": "dummy", 83 | "requestBody": { 84 | "required": true, 85 | "content": { 86 | "application/json": { 87 | "schema": { 88 | "type": "object", 89 | "properties": { 90 | "age": { 91 | "type": "integer", 92 | "format": "uint8" 93 | }, 94 | "name": { 95 | "type": "string" 96 | } 97 | }, 98 | "required": [ 99 | "name", 100 | "age" 101 | ] 102 | } 103 | } 104 | } 105 | }, 106 | "responses": { 107 | "200": { 108 | "description": "OK" 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ohkami/src/request/context.rs: -------------------------------------------------------------------------------- 1 | use crate::fang::SendSyncOnThreaded; 2 | use ohkami_lib::map::TupleMap; 3 | use std::any::{Any, TypeId}; 4 | 5 | #[cfg(feature = "__rt_threaded__")] 6 | type StoreItem = Box; 7 | #[cfg(not(feature = "__rt_threaded__"))] 8 | type StoreItem = Box; 9 | 10 | pub struct Context { 11 | store: Option>>, 12 | 13 | #[cfg(feature = "rt_worker")] 14 | worker: std::mem::MaybeUninit<(::worker::Context, ::worker::Env)>, 15 | 16 | #[cfg(feature = "rt_lambda")] 17 | lambda: Option>, 18 | } 19 | 20 | impl Context { 21 | #[cfg(feature = "__rt__")] 22 | pub(super) const fn init() -> Self { 23 | Self { 24 | store: None, 25 | 26 | #[cfg(feature = "rt_worker")] 27 | worker: std::mem::MaybeUninit::uninit(), 28 | 29 | #[cfg(feature = "rt_lambda")] 30 | lambda: None, 31 | } 32 | } 33 | 34 | #[cfg(feature = "rt_worker")] 35 | pub(super) fn load(&mut self, worker: (::worker::Context, ::worker::Env)) { 36 | self.worker.write(worker); 37 | } 38 | 39 | #[cfg(feature = "rt_lambda")] 40 | pub(super) fn load(&mut self, request_context: crate::x_lambda::LambdaHTTPRequestContext) { 41 | self.lambda = Some(Box::new(request_context)); 42 | } 43 | 44 | #[cfg(feature = "__rt_native__")] 45 | pub(super) fn clear(&mut self) { 46 | if let Some(map) = &mut self.store { 47 | map.clear() 48 | } 49 | } 50 | } 51 | 52 | impl Context { 53 | #[inline] 54 | pub fn set(&mut self, value: Data) { 55 | if self.store.is_none() { 56 | self.store = Some(Box::new(TupleMap::new())); 57 | } 58 | (unsafe { self.store.as_mut().unwrap_unchecked() }) 59 | .insert(TypeId::of::(), Box::new(value)); 60 | } 61 | 62 | #[inline] 63 | pub fn get(&self) -> Option<&Data> { 64 | self.store.as_ref().and_then(|map| { 65 | map.get(&TypeId::of::()).map(|boxed| { 66 | let data: &dyn Any = &**boxed; 67 | #[cfg(debug_assertions)] 68 | { 69 | assert!(data.is::(), "Request store is poisoned!!!"); 70 | } 71 | unsafe { &*(data as *const dyn Any as *const Data) } 72 | }) 73 | }) 74 | } 75 | 76 | #[cfg(feature = "rt_worker")] 77 | #[inline(always)] 78 | pub fn worker(&self) -> &::worker::Context { 79 | // SAFETY: User can touch here **ONLY AFTER `Self::load`** called by `Request` 80 | unsafe { &self.worker.assume_init_ref().0 } 81 | } 82 | #[cfg(feature = "rt_worker")] 83 | #[inline(always)] 84 | pub fn env(&self) -> &::worker::Env { 85 | // SAFETY: User can touch here **ONLY AFTER `Self::load`** called by `Request` 86 | unsafe { &self.worker.assume_init_ref().1 } 87 | } 88 | 89 | #[cfg(feature = "rt_lambda")] 90 | #[inline(always)] 91 | pub fn lambda(&self) -> &crate::x_lambda::LambdaHTTPRequestContext { 92 | // SAFETY: User can touch here **ONLY AFTER `Self::load`** called by `Request` 93 | unsafe { self.lambda.as_ref().unwrap_unchecked() } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ohkami/src/claw/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Claws - handler parts for desclarative request/response handling 2 | //! 3 | //! Claws provides various components for typed, declarative way to 4 | //! extract request data and construct response data: 5 | //! 6 | //! - [`body`]: for handling request and response bodies 7 | //! - [`header`]: for handling request headers 8 | //! - [`param`]: for handling request parameters (path and query) 9 | //! - [`status`]: for handling response HTTP status codes 10 | //! 11 | //! See individual modules or each component's documentation for details. 12 | //! 13 | //! ## Example 14 | //! 15 | //! ``` 16 | //! use ohkami::claw::{Path, Json, status}; 17 | //! use ohkami::serde::{Serialize, Deserialize}; 18 | //! 19 | //! #[derive(Deserialize)] 20 | //! struct CreateUserRequest<'req> { 21 | //! name: &'req str, 22 | //! } 23 | //! 24 | //! #[derive(Serialize)] 25 | //! struct User { 26 | //! id: u64, 27 | //! name: String, 28 | //! } 29 | //! 30 | //! # enum AppError {} 31 | //! # impl ohkami::IntoResponse for AppError { 32 | //! # fn into_response(self) -> ohkami::Response { 33 | //! # todo!() 34 | //! # } 35 | //! # } 36 | //! 37 | //! async fn get_user( 38 | //! // Extract a path parameter as `u64` 39 | //! Path(id): Path, 40 | //! // Serialize `User` into `application/json` response body 41 | //! ) -> Result, AppError> { 42 | //! Ok(Json(User { 43 | //! id, 44 | //! name: todo!(), 45 | //! })) 46 | //! } 47 | //! 48 | //! async fn create_user( 49 | //! // Extract `application/json` request body 50 | //! Json(body): Json>, 51 | //! // Serialize `User` into `application/json` response body 52 | //! // with `201 Created` status 53 | //! ) -> Result>, AppError> { 54 | //! Ok(status::Created(Json(User { 55 | //! id: todo!(), 56 | //! name: body.name.to_owned(), 57 | //! }))) 58 | //! } 59 | //! ``` 60 | 61 | pub mod content; 62 | pub mod header; 63 | pub mod param; 64 | pub mod status; 65 | 66 | pub use content::Json; 67 | pub use header::Cookie; 68 | pub use param::{Path, Query}; 69 | 70 | #[cold] 71 | #[inline(never)] 72 | fn reject(msg: impl std::fmt::Display) -> crate::Response { 73 | crate::Response::BadRequest().with_text(msg.to_string()) 74 | } 75 | 76 | #[cfg(feature = "openapi")] 77 | mod bound { 78 | use crate::openapi; 79 | use serde::{Deserialize, Serialize}; 80 | 81 | pub trait Schema: openapi::Schema {} 82 | impl Schema for S {} 83 | 84 | pub trait Incoming<'req>: Deserialize<'req> + openapi::Schema {} 85 | impl<'req, T> Incoming<'req> for T where T: Deserialize<'req> + openapi::Schema {} 86 | 87 | pub trait Outgoing: Serialize + openapi::Schema {} 88 | impl Outgoing for T where T: Serialize + openapi::Schema {} 89 | } 90 | #[cfg(not(feature = "openapi"))] 91 | mod bound { 92 | use serde::{Deserialize, Serialize}; 93 | 94 | pub trait Schema {} 95 | impl Schema for S {} 96 | 97 | pub trait Incoming<'req>: Deserialize<'req> {} 98 | impl<'req, T> Incoming<'req> for T where T: Deserialize<'req> {} 99 | 100 | pub trait Outgoing: Serialize {} 101 | impl Outgoing for T where T: Serialize {} 102 | } 103 | -------------------------------------------------------------------------------- /examples/hello/src/main.rs: -------------------------------------------------------------------------------- 1 | mod health_handler { 2 | use ohkami::claw::status::NoContent; 3 | 4 | pub async fn health_check() -> NoContent { 5 | NoContent 6 | } 7 | } 8 | 9 | 10 | mod hello_handler { 11 | use ohkami::claw::{Query, Json}; 12 | use ohkami::serde::Deserialize; 13 | 14 | #[derive(Deserialize)] 15 | #[serde(deny_unknown_fields)] 16 | pub struct HelloQuery<'q> { 17 | #[serde(rename = "n")] 18 | repeat: Option, 19 | name: &'q str, 20 | } 21 | 22 | pub async fn hello_by_query( 23 | Query(HelloQuery { name, repeat }): Query> 24 | ) -> String { 25 | tracing::info!("\ 26 | Called `hello_by_query`\ 27 | "); 28 | 29 | name.repeat(repeat.unwrap_or(1)) 30 | } 31 | 32 | 33 | #[derive(Deserialize)] 34 | pub struct HelloRequest<'n> { 35 | name: &'n str, 36 | repeat: Option, 37 | } 38 | #[cfg(feature="nightly")] 39 | impl ohkami::format::V for HelloRequest<'_> { 40 | type ErrorMessage = &'static str; 41 | fn validate(&self) -> Result<(), Self::ErrorMessage> { 42 | let _: () = (! self.name.is_empty()).then_some(()) 43 | .ok_or_else(|| "`name` mustn't be empty")?; 44 | 45 | let _: () = (self.repeat.unwrap_or_default() < 10).then_some(()) 46 | .ok_or_else(|| "`repeat` must be less than 10")?; 47 | 48 | Ok(()) 49 | } 50 | } 51 | 52 | pub async fn hello_by_json( 53 | Json(HelloRequest { name, repeat }): Json> 54 | ) -> String { 55 | tracing::info!("\ 56 | Called `hello_by_json`\ 57 | "); 58 | 59 | name.repeat(repeat.unwrap_or(1)) 60 | } 61 | } 62 | 63 | 64 | mod fangs { 65 | use ohkami::prelude::*; 66 | 67 | #[derive(Clone)] 68 | pub struct SetServer; 69 | impl FangAction for SetServer { 70 | fn back<'a>(&'a self, res: &'a mut Response) -> impl std::future::Future + Send { 71 | res.headers.set().server("ohkami"); 72 | 73 | tracing::info!("\ 74 | Called `SetServer`\n\ 75 | [current headers]\n\ 76 | {:?}\n\ 77 | ", res.headers); 78 | 79 | async {} 80 | } 81 | } 82 | 83 | #[derive(Clone)] 84 | pub struct LogRequest; 85 | impl FangAction for LogRequest { 86 | async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { 87 | tracing::info!("\nGot request: {req:#?}"); 88 | Ok(()) 89 | } 90 | } 91 | } 92 | 93 | 94 | #[tokio::main] 95 | async fn main() { 96 | use ohkami::{Ohkami, Route}; 97 | 98 | tracing_subscriber::fmt() 99 | .with_max_level(tracing::Level::INFO) 100 | .init(); 101 | 102 | Ohkami::new(( 103 | fangs::LogRequest, 104 | "/hc" .GET(health_handler::health_check), 105 | "/api".By(Ohkami::new(( 106 | fangs::SetServer, 107 | "/query" 108 | .GET(hello_handler::hello_by_query), 109 | "/json" 110 | .POST(hello_handler::hello_by_json), 111 | ))), 112 | )).howl("localhost:3000").await 113 | } 114 | -------------------------------------------------------------------------------- /samples/openapi-tags/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused/* just for demo of `openapi::Tag` */)] 2 | 3 | use ohkami::prelude::*; 4 | use ohkami::claw::status; 5 | use ohkami::openapi; 6 | 7 | mod users { 8 | use super::*; 9 | 10 | pub(super) fn ohkami() -> Ohkami { 11 | Ohkami::new(( 12 | openapi::Tag("users"), 13 | "/" 14 | .GET(list_users) 15 | .POST(create_user), 16 | "/:id" 17 | .GET(get_user_profile), 18 | )) 19 | } 20 | 21 | #[derive(Serialize, openapi::Schema)] 22 | struct User { 23 | id: i32, 24 | name: String, 25 | age: Option, 26 | } 27 | 28 | #[derive(Deserialize, openapi::Schema)] 29 | struct CreateUser<'req> { 30 | name: &'req str, 31 | age: Option, 32 | } 33 | 34 | async fn list_users() -> Json> { 35 | Json(vec![]) 36 | } 37 | 38 | async fn create_user( 39 | Json(req): Json>, 40 | ) -> status::Created> { 41 | status::Created(Json(User { 42 | id: 42, 43 | name: req.name.into(), 44 | age: req.age 45 | })) 46 | } 47 | 48 | async fn get_user_profile(Path(id): Path) -> Json { 49 | Json(User { 50 | id, 51 | name: "unknown".into(), 52 | age: Some(42) 53 | }) 54 | } 55 | } 56 | 57 | mod tasks { 58 | use super::*; 59 | 60 | pub(super) fn ohkami() -> Ohkami { 61 | Ohkami::new(( 62 | openapi::Tag("tasks"), 63 | "/list" 64 | .GET(list_tasks), 65 | "/:id/edit" 66 | .PUT(edit_task), 67 | )) 68 | } 69 | 70 | #[derive(Serialize, openapi::Schema)] 71 | struct Task { 72 | id: i32, 73 | title: String, 74 | #[openapi(schema_with = "description_schema")] 75 | description: String, 76 | } 77 | 78 | #[derive(Deserialize, openapi::Schema)] 79 | struct EditTask<'req> { 80 | title: Option<&'req str>, 81 | description: Option<&'req str>, 82 | } 83 | 84 | async fn list_tasks() -> Json> { 85 | Json(vec![]) 86 | } 87 | 88 | async fn edit_task( 89 | Json(req): Json> 90 | ) -> status::NoContent { 91 | status::NoContent 92 | } 93 | 94 | fn description_schema() -> impl Into { 95 | openapi::string() 96 | .format("Japanese") 97 | } 98 | } 99 | 100 | fn main() { 101 | let users_ohkami = users::ohkami(); 102 | let tasks_ohkami = tasks::ohkami(); 103 | 104 | let api_ohkami = Ohkami::new(( 105 | openapi::Tag("api"), 106 | "/" 107 | .GET(|| async {"Hello, tags!"}), 108 | "/users".By(users_ohkami), 109 | "/tasks".By(tasks_ohkami), 110 | )); 111 | 112 | let o = Ohkami::new(( 113 | "/health" 114 | .GET(|| async {status::NoContent}), 115 | "/api".By(api_ohkami), 116 | )); 117 | 118 | o.generate(openapi::OpenAPI { 119 | title: "Sample API", 120 | version: "0.0.0", 121 | servers: &[openapi::Server::at("http://localhost:6666")] 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /samples/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -Ceu 4 | 5 | SAMPLES=$(pwd) 6 | 7 | cd $SAMPLES/issue_550_glommio && \ 8 | cargo check 9 | 10 | cd $SAMPLES/openapi-schema-enums && \ 11 | cargo run && \ 12 | diff openapi.json openapi.json.sample 13 | test $? -ne 0 && exit 150 || : 14 | 15 | cd $SAMPLES/openapi-schema-from-into && \ 16 | cargo run && \ 17 | diff openapi.json openapi.json.sample 18 | test $? -ne 0 && exit 151 || : 19 | 20 | cd $SAMPLES/openapi-tags && \ 21 | cargo run && \ 22 | diff openapi.json openapi.json.sample 23 | test $? -ne 0 && exit 152 || : 24 | 25 | cd $SAMPLES/petstore && \ 26 | cargo build && \ 27 | cd client && \ 28 | npm install && \ 29 | cd .. && \ 30 | (timeout -sKILL 5 cargo run &) && \ 31 | sleep 1 && \ 32 | diff openapi.json openapi.json.sample && \ 33 | cd client && \ 34 | npm run gen && \ 35 | npm run main 36 | # FIXME 37 | # this is a little flaky; sometimes cause connection refused 38 | test $? -ne 0 && exit 153 || : 39 | 40 | cd $SAMPLES/readme-openapi && \ 41 | cargo build && \ 42 | (timeout -sKILL 1 cargo run &) && \ 43 | sleep 1 && \ 44 | diff openapi.json openapi.json.sample 45 | test $? -ne 0 && exit 154 || : 46 | 47 | cd $SAMPLES/realworld && \ 48 | docker compose up -d && \ 49 | sleep 5 && \ 50 | sqlx migrate run && \ 51 | cargo test && \ 52 | docker compose down 53 | test $? -ne 0 && exit 155 || : 54 | 55 | cd $SAMPLES/streaming && \ 56 | cargo build && \ 57 | (timeout -sKILL 1 cargo run &) && \ 58 | sleep 1 && \ 59 | diff openapi.json openapi.json.sample 60 | test $? -ne 0 && exit 156 || : 61 | 62 | cd $SAMPLES/worker-bindings && \ 63 | cargo check && \ 64 | wasm-pack build --target nodejs --dev --no-opt --no-pack --no-typescript && \ 65 | node dummy_env_test.js 66 | test $? -ne 0 && exit 157 || : 67 | 68 | cd $SAMPLES/worker-bindings-jsonc && \ 69 | cargo check 70 | test $? -ne 0 && exit 158 || : 71 | 72 | cd $SAMPLES/worker-durable-websocket && \ 73 | cargo check 74 | test $? -ne 0 && exit 159 || : 75 | 76 | cd $SAMPLES/worker-with-global-bindings && \ 77 | npm run openapi 78 | test $? -ne 0 && exit 160 || : 79 | 80 | cd $SAMPLES/worker-with-openapi && \ 81 | cp wrangler.toml.sample wrangler.toml && \ 82 | (test -f openapi.json || echo '{}' >> openapi.json) && \ 83 | npm run openapi && \ 84 | diff openapi.json openapi.json.sample && \ 85 | sed -i -r 's/^#\[ohkami::worker.*]$/#[ohkami::worker({ title: "Ohkami Worker with OpenAPI", version: "0.1.1", servers: [] })]/' ./src/lib.rs && \ 86 | npm run openapi && \ 87 | diff openapi.json openapi.json.manual-title-version-empty_servers.sample && \ 88 | sed -i -r 's/^#\[ohkami::worker.*]$/#[ohkami::worker({ title: "Ohkami Worker with OpenAPI", version: "0.1.2", servers: [{url: "https:\/\/example.example.workers.dev"}] })]/' ./src/lib.rs && \ 89 | npm run openapi && \ 90 | diff openapi.json openapi.json.manual-title-version-nonempty_servers.sample && \ 91 | sed -i -r 's/^#\[ohkami::worker.*]$/#[ohkami::worker({servers: [{url: "https:\/\/example.example.workers.dev"}]})]/' ./src/lib.rs && \ 92 | npm run openapi && \ 93 | diff openapi.json openapi.json.manual-only_nonempty_servers.sample && \ 94 | sed -i -r 's/^#\[ohkami::worker.*]$/#[ohkami::worker]/' ./src/lib.rs # reset to default 95 | test $? -ne 0 && exit 161 || : 96 | -------------------------------------------------------------------------------- /samples/realworld/src/models.rs: -------------------------------------------------------------------------------- 1 | use ohkami::serde::{Serialize, Deserialize}; 2 | use ohkami::fang::JwtToken; 3 | use chrono::{DateTime, Utc}; 4 | 5 | pub mod request; 6 | pub mod response; 7 | 8 | 9 | mod serde_datetime { 10 | use ohkami::serde::{Deserialize, Deserializer, Serializer}; 11 | use chrono::{DateTime, Utc, SecondsFormat}; 12 | 13 | pub(super) fn serialize( 14 | date_time: &DateTime, 15 | serializer: S, 16 | ) -> Result { 17 | serializer.serialize_str(&date_time.to_rfc3339_opts(SecondsFormat::Millis, true)) 18 | } 19 | 20 | #[allow(unused)] // used in test 21 | pub(super) fn deserialize<'de, D: Deserializer<'de>>( 22 | deserializer: D, 23 | ) -> Result, D::Error> { 24 | let s = String::deserialize(deserializer)?; 25 | let datetime = DateTime::parse_from_rfc3339(&s) 26 | .map_err(ohkami::serde::de::Error::custom)?; 27 | Ok(datetime.into()) 28 | } 29 | } 30 | 31 | 32 | #[derive(Serialize)] 33 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 34 | pub struct User { 35 | pub email: String, 36 | #[serde(rename = "token")] 37 | pub jwt: JwtToken, 38 | #[serde(rename = "username")] 39 | pub name: String, 40 | pub bio: Option, 41 | pub image: Option, 42 | } 43 | 44 | #[derive(Serialize)] 45 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 46 | pub struct Profile { 47 | pub username: String, 48 | pub bio: Option, 49 | pub image: Option, 50 | pub following: bool, 51 | } 52 | 53 | #[derive(Serialize)] 54 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 55 | pub struct Article { 56 | pub title: String, 57 | pub slug: String, 58 | pub description: String, 59 | pub body: String, 60 | #[serde(rename = "tagList")] 61 | pub tag_list: Vec, 62 | #[serde(rename = "createdAt", with = "serde_datetime")] 63 | pub created_at: DateTime, 64 | #[serde(rename = "updatedAt", with = "serde_datetime")] 65 | pub updated_at: DateTime, 66 | pub favorited: bool, 67 | #[serde(rename = "favoritesCount")] 68 | pub favorites_count: usize, 69 | pub author: Profile, 70 | } 71 | 72 | #[derive(Serialize)] 73 | #[cfg_attr(test, derive(ohkami::serde::Deserialize, Debug, PartialEq))] 74 | pub struct Comment { 75 | pub id: usize, 76 | #[serde(rename = "createdAt", with = "serde_datetime")] 77 | pub created_at: DateTime, 78 | #[serde(rename = "updatedAt", with = "serde_datetime")] 79 | pub updated_at: DateTime, 80 | pub body: String, 81 | pub author: Profile, 82 | } 83 | 84 | #[derive(Serialize, Deserialize)] 85 | #[cfg_attr(test, derive(Debug, PartialEq))] 86 | pub struct Tag<'t>(std::borrow::Cow<'t, str>); 87 | const _: () = { 88 | impl Tag<'static> { 89 | pub fn new(name: impl Into>) -> Self { 90 | Self(name.into()) 91 | } 92 | } 93 | 94 | impl<'t> std::ops::Deref for Tag<'t> { 95 | type Target = str; 96 | #[inline] fn deref(&self) -> &Self::Target { 97 | &self.0 98 | } 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /ohkami_lib/src/serde_multipart/de.rs: -------------------------------------------------------------------------------- 1 | use serde::de::IntoDeserializer; 2 | 3 | use super::Error; 4 | use super::parse::{Multipart, Next, Part}; 5 | 6 | pub(crate) struct MultipartDesrializer<'de> { 7 | parsed: Multipart<'de>, 8 | } 9 | 10 | impl<'de> MultipartDesrializer<'de> { 11 | pub(crate) fn new(input: &'de [u8]) -> Result { 12 | Ok(Self { 13 | parsed: Multipart::parse(input)?, 14 | }) 15 | } 16 | } 17 | 18 | impl<'de> serde::de::Deserializer<'de> for &mut MultipartDesrializer<'de> { 19 | type Error = Error; 20 | 21 | fn deserialize_struct( 22 | self, 23 | _name: &'static str, 24 | _fields: &'static [&'static str], 25 | visitor: V, 26 | ) -> Result 27 | where 28 | V: serde::de::Visitor<'de>, 29 | { 30 | self.deserialize_map(visitor) 31 | } 32 | fn deserialize_map(self, visitor: V) -> Result 33 | where 34 | V: serde::de::Visitor<'de>, 35 | { 36 | visitor.visit_map(self) 37 | } 38 | 39 | fn deserialize_newtype_struct( 40 | self, 41 | _name: &'static str, 42 | visitor: V, 43 | ) -> Result 44 | where 45 | V: serde::de::Visitor<'de>, 46 | { 47 | visitor.visit_newtype_struct(self) 48 | } 49 | 50 | serde::forward_to_deserialize_any! { 51 | bool str string char 52 | unit unit_struct 53 | tuple tuple_struct 54 | bytes byte_buf 55 | option enum seq identifier 56 | ignored_any 57 | i8 i16 i32 i64 58 | u8 u16 u32 u64 59 | f32 f64 60 | } 61 | fn deserialize_any(self, visitor: V) -> Result 62 | where 63 | V: serde::de::Visitor<'de>, 64 | { 65 | self.deserialize_map(visitor) 66 | } 67 | } 68 | 69 | impl<'de> serde::de::MapAccess<'de> for &mut MultipartDesrializer<'de> { 70 | type Error = Error; 71 | 72 | fn next_entry_seed( 73 | &mut self, 74 | kseed: K, 75 | vseed: V, 76 | ) -> Result, Self::Error> 77 | where 78 | K: serde::de::DeserializeSeed<'de>, 79 | V: serde::de::DeserializeSeed<'de>, 80 | { 81 | match self.parsed.next() { 82 | Some(Next { name, item }) => Ok(Some(( 83 | kseed.deserialize(name.into_deserializer())?, 84 | vseed.deserialize(item.into_deserializer())?, 85 | ))), 86 | None => Ok(None), 87 | } 88 | } 89 | 90 | fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> 91 | where 92 | K: serde::de::DeserializeSeed<'de>, 93 | { 94 | let Some(part) = self.parsed.peek() else { 95 | return Ok(None); 96 | }; 97 | let name = match part { 98 | Part::File { name, .. } => name, 99 | Part::Text { name, .. } => name, 100 | }; 101 | seed.deserialize(name.into_deserializer()).map(Some) 102 | } 103 | fn next_value_seed(&mut self, seed: V) -> Result 104 | where 105 | V: serde::de::DeserializeSeed<'de>, 106 | { 107 | let Next { item, .. } = self.parsed.next().unwrap(); 108 | seed.deserialize(item.into_deserializer()) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /samples/openapi-schema-from-into/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused/* just generating openapi.json for dummy handlers */)] 2 | 3 | use ohkami::prelude::*; 4 | use ohkami::serde::json; 5 | use ohkami::openapi; 6 | 7 | #[derive(Deserialize, openapi::Schema)] 8 | struct RawCreateUser<'req> { 9 | name: &'req str, 10 | age: u8, 11 | } 12 | 13 | #[derive(Deserialize, openapi::Schema, Debug, PartialEq)] 14 | #[serde(from = "RawCreateUser")] 15 | struct CreateUser<'req> { 16 | username: &'req str, 17 | age: u8, 18 | } 19 | impl<'req> From> for CreateUser<'req> { 20 | fn from(raw: RawCreateUser<'req>) -> Self { 21 | Self { 22 | username: raw.name, 23 | age: raw.age, 24 | } 25 | } 26 | } 27 | 28 | #[derive(Deserialize, openapi::Schema)] 29 | #[serde(try_from = "RawCreateUser")] 30 | struct ValidatedCreateUser<'req> { 31 | username: &'req str, 32 | age: u8, 33 | } 34 | impl<'req> TryFrom> for ValidatedCreateUser<'req> { 35 | type Error = String; 36 | 37 | fn try_from(raw: RawCreateUser<'req>) -> Result { 38 | if raw.age < 18 { 39 | return Err(format!("User's age must be 18 or more")) 40 | } 41 | 42 | Ok(Self { 43 | username: raw.name, 44 | age: raw.age, 45 | }) 46 | } 47 | } 48 | 49 | #[derive(Serialize, openapi::Schema, Clone)] 50 | #[serde(into = "UserResponse")] 51 | struct User { 52 | name: String, 53 | age: u8, 54 | } 55 | 56 | #[derive(Serialize, openapi::Schema)] 57 | struct UserResponse { 58 | user: UserResponseUser, 59 | } 60 | #[derive(Serialize, openapi::Schema)] 61 | struct UserResponseUser { 62 | name: String, 63 | age: u8, 64 | } 65 | impl From for UserResponse { 66 | fn from(user: User) -> Self { 67 | Self { 68 | user: UserResponseUser { 69 | name: user.name, 70 | age: user.age, 71 | } 72 | } 73 | } 74 | } 75 | 76 | fn main() { 77 | macro_rules! dummy_handler { 78 | (-> $return_type:ty) => { 79 | {async fn dummy() -> Json<$return_type> {todo!()}; dummy} 80 | }; 81 | ($req_type:ty) => { 82 | {async fn dummy(_: Json<$req_type>) {}; dummy} 83 | }; 84 | } 85 | 86 | let o = Ohkami::new(( 87 | "/from".GET(dummy_handler!(CreateUser<'_>)), 88 | "/try_from".GET(dummy_handler!(ValidatedCreateUser<'_>)), 89 | "/into".GET(dummy_handler!(-> UserResponse)), 90 | )); 91 | 92 | assert!(Result::is_err(&json::from_str::(r#"{ 93 | "username": "ohkami", 94 | "age": 4 95 | }"#))); 96 | assert_eq!(json::from_str::(r#"{ 97 | "name": "ohkami", 98 | "age": 4 99 | }"#).unwrap(), CreateUser { 100 | username: "ohkami", 101 | age: 4 102 | }); 103 | 104 | let u = User { 105 | name: format!("ohkami"), 106 | age: 4 107 | }; 108 | assert_ne!( 109 | json::to_string(&u).unwrap(), 110 | r#"{"name":"ohkami","age":4}"# 111 | ); 112 | assert_eq!( 113 | json::to_string(&u).unwrap(), 114 | r#"{"user":{"name":"ohkami","age":4}}"# 115 | ); 116 | 117 | o.generate(openapi::OpenAPI { 118 | title: "Dummy Server for serde From & Into", 119 | version: "0", 120 | servers: &[], 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /examples/uibeam/src/main.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use ohkami::serde::Deserialize; 3 | use ohkami::claw::{Query, content::Html}; 4 | use uibeam::{UI, Beam}; 5 | 6 | struct Layout { 7 | title: String, 8 | children: UI, 9 | } 10 | impl Beam for Layout { 11 | fn render(self) -> UI { 12 | UI! { 13 | 14 | 15 | 16 | {&*self.title} 17 | 18 | 19 | {self.children} 20 | 21 | 22 | } 23 | } 24 | } 25 | impl Layout { 26 | fn fang_with_title(title: &str) -> impl FangAction { 27 | #[derive(Clone)] 28 | struct Fang { 29 | title: String, 30 | } 31 | 32 | impl FangAction for Fang { 33 | async fn back(&self, res: &mut Response) { 34 | if res.headers.content_type().is_some_and(|x| x.starts_with("text/html")) { 35 | let content = res.drop_content().into_bytes().unwrap(); 36 | let content = std::str::from_utf8(&*content).unwrap(); 37 | res.set_html(uibeam::shoot(UI! { 38 | 39 | unsafe {content} 40 | 41 | })); 42 | } 43 | } 44 | } 45 | 46 | Fang { 47 | title: title.to_string(), 48 | } 49 | } 50 | } 51 | 52 | struct Counter { 53 | initial_count: i32, 54 | } 55 | impl Beam for Counter { 56 | fn render(self) -> UI { 57 | UI! { 58 |
59 |

60 | "count: "{self.initial_count} 61 |

62 | 66 | 70 | 71 | 80 |
81 | } 82 | } 83 | } 84 | 85 | #[derive(Deserialize)] 86 | struct CounterMeta { 87 | init: Option, 88 | } 89 | 90 | async fn index(Query(q): Query) -> Html> { 91 | let initial_count = q.init.unwrap_or(0); 92 | 93 | Html(uibeam::shoot(UI! { 94 | 95 | })) 96 | } 97 | 98 | #[tokio::main] 99 | async fn main() { 100 | Ohkami::new(( 101 | Layout::fang_with_title("Counter Example"), 102 | "/".GET(index), 103 | )).howl("localhost:5555").await 104 | } 105 | -------------------------------------------------------------------------------- /ohkami_openapi/src/_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use super::*; 4 | use serde_json::json; 5 | 6 | macro_rules! assert_eq { 7 | ($left:expr, $right:expr) => {{ 8 | let (left, right) = ($left, $right); 9 | if $left != $right { 10 | panic!( 11 | "\ 12 | \n[left]\n\ 13 | {left:#?}\n\ 14 | \n[right]\n\ 15 | {right:#?}\n\ 16 | " 17 | ) 18 | } 19 | }}; 20 | } 21 | 22 | #[test] 23 | fn test_openapi_doc_serialization() { 24 | let doc = document::Document::new( 25 | "Sample API", "0.1.9", [ 26 | document::Server::at("http://api.example.com/v1") 27 | .description("Optional server description, e.g. Main (production) server"), 28 | document::Server::at("http://staging-api.example.com") 29 | .description("Optional server description, e.g. Internal staging server for testing") 30 | ] 31 | ) 32 | .description("Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.") 33 | .path("/users", paths::Operations::new() 34 | .get( 35 | Operation::with( 36 | Responses::new([( 37 | 200, Response::when("A JSON array of user names") 38 | .content("application/json", array(string())) 39 | )]) 40 | ) 41 | .summary("Returns a list of users.") 42 | .description("Optional extended description in CommonMark or HTML.") 43 | ) 44 | ); 45 | 46 | assert_eq!( 47 | serde_json::to_value(&doc).unwrap(), 48 | json!({ 49 | "openapi": "3.1.0", 50 | "info": { 51 | "title": "Sample API", 52 | "description": "Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.", 53 | "version": "0.1.9" 54 | }, 55 | "servers": [ 56 | { 57 | "url": "http://api.example.com/v1", 58 | "description": "Optional server description, e.g. Main (production) server" 59 | }, 60 | { 61 | "url": "http://staging-api.example.com", 62 | "description": "Optional server description, e.g. Internal staging server for testing" 63 | } 64 | ], 65 | "paths": { 66 | "/users": { 67 | "get": { 68 | "summary": "Returns a list of users.", 69 | "description": "Optional extended description in CommonMark or HTML.", 70 | "responses": { 71 | "200": { 72 | "description": "A JSON array of user names", 73 | "content": { 74 | "application/json": { 75 | "schema": { 76 | "type": "array", 77 | "items": { 78 | "type": "string" 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }) 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /ohkami_lib/src/serde_multipart.rs: -------------------------------------------------------------------------------- 1 | mod de; 2 | mod file; 3 | mod parse; 4 | 5 | #[cfg(test)] 6 | mod _test_de; 7 | #[cfg(test)] 8 | mod _test_parse; 9 | 10 | pub use file::File; 11 | 12 | #[inline(always)] 13 | pub fn from_bytes<'de, D: serde::Deserialize<'de>>(input: &'de [u8]) -> Result { 14 | let mut d = de::MultipartDesrializer::new(input)?; 15 | D::deserialize(&mut d) 16 | } 17 | 18 | use std::borrow::Cow; 19 | #[derive(Debug)] 20 | pub struct Error(Cow<'static, str>); 21 | const _: () = { 22 | impl std::fmt::Display for Error { 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | f.write_str(&self.0) 25 | } 26 | } 27 | impl std::error::Error for Error {} 28 | 29 | impl serde::ser::Error for Error { 30 | fn custom(msg: T) -> Self 31 | where 32 | T: std::fmt::Display, 33 | { 34 | Self(Cow::Owned(msg.to_string())) 35 | } 36 | } 37 | impl serde::de::Error for Error { 38 | fn custom(msg: T) -> Self 39 | where 40 | T: std::fmt::Display, 41 | { 42 | Self(Cow::Owned(msg.to_string())) 43 | } 44 | } 45 | }; 46 | #[allow(non_snake_case)] 47 | impl Error { 48 | const fn NotSupportedMultipartMixed() -> Self { 49 | Self(Cow::Borrowed( 50 | "Ohkami doesn't support `multipart/mixed` nested in `multipart/form-data`, this is DEPRECATED!", 51 | )) 52 | } 53 | const fn UnexpectedMultipleFiles() -> Self { 54 | Self(Cow::Borrowed( 55 | "Expected a single file for the name, but found multiple parts of the same name holding files in multipart/form-data", 56 | )) 57 | } 58 | const fn ExpectedBoundary() -> Self { 59 | Self(Cow::Borrowed("Expected multipart boundary")) 60 | } 61 | const fn MissingCRLF() -> Self { 62 | Self(Cow::Borrowed("Missing CRLF in multipart")) 63 | } 64 | const fn ExpectedFile() -> Self { 65 | Self(Cow::Borrowed( 66 | "Expected file but found non-file field in multipart", 67 | )) 68 | } 69 | const fn ExpectedNonFileField() -> Self { 70 | Self(Cow::Borrowed( 71 | "Expected non-file field but found file(s) in multipart", 72 | )) 73 | } 74 | const fn ExpectedFilename() -> Self { 75 | Self(Cow::Borrowed("Expected `filename=\"...\"`")) 76 | } 77 | const fn ExpectedValidHeader() -> Self { 78 | Self(Cow::Borrowed( 79 | "Expected `Content-Type` or `Content-Disposition` header in multipart section", 80 | )) 81 | } 82 | const fn ExpectedFormdataAndName() -> Self { 83 | Self(Cow::Borrowed( 84 | "Expected `form-data; name=\"...\"` after `Content-Disposition: `", 85 | )) 86 | } 87 | const fn InvalidFilename() -> Self { 88 | Self(Cow::Borrowed("Invalid filename; filename must be UTF-8")) 89 | } 90 | const fn InvalidMimeType() -> Self { 91 | Self(Cow::Borrowed("Invalid mime type")) 92 | } 93 | const fn InvalidPartName() -> Self { 94 | Self(Cow::Borrowed( 95 | "Invalid `name` in multipart; name must be UTF-8 enclosed by \"\"", 96 | )) 97 | } 98 | const fn NotUTF8NonFileField() -> Self { 99 | Self(Cow::Borrowed( 100 | "Expected a non-file field to be a UTF-8 text; ohkami doesn't support multipart/form-data with not-file fields have raw byte streams", 101 | )) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /samples/realworld/src/handlers/user.rs: -------------------------------------------------------------------------------- 1 | use ohkami::prelude::*; 2 | use sqlx::PgPool; 3 | use crate::{ 4 | fangs::Auth, 5 | models::User, 6 | models::response::UserResponse, 7 | models::request::{UpdateProfileRequest, UpdateProfileRequestUser}, 8 | errors::RealWorldError, 9 | config::JwtPayload, 10 | db::{UserEntity, hash_password}, 11 | }; 12 | 13 | 14 | pub fn user_ohkami() -> Ohkami { 15 | Ohkami::new(( 16 | Auth::required(), 17 | "/" 18 | .GET(get_user) 19 | .PUT(update), 20 | )) 21 | } 22 | 23 | async fn get_user( 24 | Context(pool): Context<'_, PgPool>, 25 | Context(auth): Context<'_, JwtPayload>, 26 | ) -> Result, RealWorldError> { 27 | let user = util::get_current_user(pool, auth).await?; 28 | Ok(Json(UserResponse { user })) 29 | } 30 | 31 | async fn update( 32 | Json(req): Json>, 33 | Context(auth): Context<'_, JwtPayload>, 34 | Context(pool): Context<'_, PgPool>, 35 | ) -> Result, RealWorldError> { 36 | let user_entity = { 37 | let UpdateProfileRequest { 38 | user: UpdateProfileRequestUser { 39 | email, username, image, bio, password:raw_password 40 | } 41 | } = req; 42 | let new_password_and_salt = raw_password.map(|rp| hash_password(&rp)).transpose()?; 43 | 44 | let mut set_once = false; 45 | macro_rules! set_if_some { 46 | ($field:ident -> $query:ident . $column:ident) => { 47 | if let Some($field) = $field { 48 | if set_once {$query.push(',');} 49 | $query.push(concat!(" ",stringify!($column)," = ")).push_bind($field); 50 | set_once = true; 51 | } 52 | }; 53 | } 54 | 55 | let mut query = sqlx::QueryBuilder::new("UPDATE users SET"); 56 | set_if_some!(email -> query.email); 57 | set_if_some!(username -> query.name); 58 | set_if_some!(image -> query.image_url); 59 | set_if_some!(bio -> query.bio); 60 | if let Some((hash, salt)) = new_password_and_salt { 61 | if set_once {query.push(',');} 62 | query.push(" password = ").push_bind(hash.as_str().to_string()); 63 | query.push(" salt = ").push_bind(salt.as_str().to_string()); 64 | } 65 | query.push(" WHERE id = ").push_bind(auth.user_id); 66 | query.push(" RETURNING id, email, name, image_url, bio"); 67 | 68 | if !set_once { 69 | // Requested to update nothing, then not perform UPDATE query 70 | let user = util::get_current_user(pool, auth).await?; 71 | return Ok(Json(UserResponse { user })) 72 | } 73 | 74 | query.build_query_as::() 75 | .fetch_one(pool).await 76 | .map_err(RealWorldError::DB)? 77 | }; 78 | 79 | Ok(Json(user_entity.into_user_response()?)) 80 | } 81 | 82 | mod util { 83 | use super::*; 84 | 85 | pub async fn get_current_user<'a>( 86 | pool: &'a PgPool, 87 | auth: &'a JwtPayload, 88 | ) -> Result { 89 | let u = sqlx::query_as!(UserEntity, r#" 90 | SELECT id, email, name, bio, image_url 91 | FROM users AS u 92 | WHERE 93 | u.id = $1 94 | "#, auth.user_id) 95 | .fetch_one(pool).await 96 | .map_err(RealWorldError::DB)?; 97 | 98 | Ok(u.into_user()?) 99 | } 100 | } 101 | --------------------------------------------------------------------------------