Skip to content

Ohkami - intuitive and declarative web framework for Rust

License

Notifications You must be signed in to change notification settings

ohkami-rs/ohkami

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ohkami

Ohkami - [ç‹Ľ] wolf in Japanese - is intuitive and declarative web framework.

  • macro-less and type-safe APIs for intuitive and declarative code
  • various runtimes are supported:tokio, async-std, smol, nio, glommio and worker (Cloudflare Workers), lambda (AWS Lambda)
  • extremely fast, no-network testing, well-structured middlewares, Server-Sent Events, WebSocket, highly integrated OpenAPI document generation, ...
License build check status of ohkami crates.io

Quick Start

  1. Add to dependencies :
[dependencies]
ohkami = { version = "0.23", features = ["rt_tokio"] }
tokio  = { version = "1",    features = ["full"] }
  1. Write your first code with Ohkami : examples/quick_start
use ohkami::prelude::*;
use ohkami::typed::status;

async fn health_check() -> status::NoContent {
    status::NoContent
}

async fn hello(name: &str) -> String {
    format!("Hello, {name}!")
}

#[tokio::main]
async fn main() {
    Ohkami::new((
        "/healthz"
            .GET(health_check),
        "/hello/:name"
            .GET(hello),
    )).howl("localhost:3000").await
}
  1. Run and check the behavior :
$ cargo run
$ curl http://localhost:3000/healthz
$ curl http://localhost:3000/hello/your_name
Hello, your_name!

Feature flags

"rt_tokio", "rt_async-std", "rt_smol", "rt_nio", "rt_glommio" : native async runtime

"rt_worker" : Cloudflare Workers

Works with worker crate.

npm create cloudflare <project dir> -- --template https://github.com/ohkami-rs/ohkami-templates/worker

then <project dir> will have wrangler.toml, package.json and a Rust library crate.

A #[ohkami::worker] (async/sync) fn returning Ohkami is the Worker definition.

Local dev by npm run dev and deploy by npm run deploy !

See README of template for details.

Or, here are Workers + OpenAPI template and Workers + SPA with Yew template.

"rt_lambda" : AWS Lambda

experimental

  • Both Function URLs and API Gateway are supported
  • WebSocket is not supported now
  • Please let us know any bugs or unexpected behavior on PR!

Works with lambda_runtime crate ( and tokio ).

cargo lambda will be good partner.

Let's :

cargo lambda new <project dir> --template https://github.com/ohkami-rs/ohkami-templates

lambda_runtime::run(your_ohkami) make you_ohkami run on Lambda Function.

Local dev by

cargo lambda watch

and deploy by

cargo lambda build --release [--compiler cargo] [and more]
cargo lambda deploy [--role <arn-of-a-iam-role>] [and more]

See

for details.

"sse" : Server-Sent Events

Ohkami responds with HTTP/1.1 Transfer-Encoding: chunked.
Use some reverse proxy to do with HTTP/2,3.

use ohkami::prelude::*;
use ohkami::sse::DataStream;
use tokio::time::{sleep, Duration};

async fn handler() -> DataStream {
    DataStream::new(|mut s| async move {
        s.send("starting streaming...");
        for i in 1..=5 {
            sleep(Duration::from_secs(1)).await;
            s.send(format!("MESSAGE #{i}"));
        }
        s.send("streaming finished!");
    })
}

#[tokio::main]
async fn main() {
    Ohkami::new((
        "/sse".GET(handler),
    )).howl("localhost:3020").await
}

"ws" : WebSocket

Ohkami only handles ws://.
Use some reverse proxy to do with wss://.

use ohkami::prelude::*;
use ohkami::ws::{WebSocketContext, WebSocket, Message};

async fn echo_text(ctx: WebSocketContext<'_>) -> WebSocket {
    ctx.upgrade(|mut conn| async move {
        while let Ok(Some(Message::Text(text))) = conn.recv().await {
            conn.send(text).await.expect("failed to send text");
        }
    })
}

#[tokio::main]
async fn main() {
    Ohkami::new((
        "/ws".GET(echo_text),
    )).howl("localhost:3030").await
}
  • On "rt_worker", both normal ( stateless ) WebSocket and WebSocket on Durable Object are available!
  • On "rt_lambda", WebSocket is currently not supported.

"openapi" : OpenAPI document generation

"openapi" provides highly integrated OpenAPI support.

This enables macro-less, as consistent as possible OpenAPI document generation, where most of the consistency between document and behavior is automatically assured by Ohkami's internal work.

Only you have to

  • Derive openapi::Schema for all your schema structs
  • Make your Ohkami call .generate(openapi::OpenAPI { ... })

to generate consistent OpenAPI document.

You don't need to take care of writing accurate methods, paths, parameters, contents, ... for this OpenAPI feature; All they are done by Ohkami.

Of course, you can flexibly customize schemas ( by hand-implemetation of Schema ), descriptions or other parts ( by #[operation] attribute and openapi_* hooks ).

use ohkami::prelude::*;
use ohkami::typed::status;
use ohkami::openapi;

// Derive `Schema` trait to generate
// the schema of this struct in OpenAPI document.
#[derive(Deserialize, openapi::Schema)]
struct CreateUser<'req> {
    name: &'req str,
}

#[derive(Serialize, openapi::Schema)]
// `#[openapi(component)]` to define it as component
// in OpenAPI document.
#[openapi(component)]
struct User {
    id: usize,
    name: String,
}

async fn create_user(
    JSON(CreateUser { name }): JSON<CreateUser<'_>>
) -> status::Created<JSON<User>> {
    status::Created(JSON(User {
        id: 42,
        name: name.to_string()
    }))
}

// (optionally) Set operationId, summary,
// or override descriptions by `operation` attribute.
#[openapi::operation({
    summary: "...",
    200: "List of all users",
})]
/// This doc comment is used for the
/// `description` field of OpenAPI document
async fn list_users() -> JSON<Vec<User>> {
    JSON(vec![])
}

#[tokio::main]
async fn main() {
    let o = Ohkami::new((
        "/users"
            .GET(list_users)
            .POST(create_user),
    ));

    // This make your Ohkami spit out `openapi.json`
    // ( the file name is configurable by `.generate_to` ).
    o.generate(openapi::OpenAPI {
        title: "Users Server",
        version: "0.1.0",
        servers: &[
            openapi::Server::at("localhost:5000"),
        ]
    });

    o.howl("localhost:5000").await;
}
  • Currently, only JSON is supported as the document format.
  • When the binary size matters, you should prepare a feature flag activating ohkami/openapi in your package, and put all your codes around openapi behind that feature via #[cfg(feature = ...)] or #[cfg_attr(feature = ...)].
  • In rt_worker, .generate is not available because Ohkami can't have access to your local filesystem by wasm32 binary on Minifalre. So ohkami provides a CLI tool to generate document from #[ohkami::worker] Ohkami with openapi feature.

"nightly" : nightly-only functionalities

  • try response

Snippets

Typed payload

builtin payload : JSON, Text, HTML, URLEncoded, Multipart

use ohkami::prelude::*;
use ohkami::typed::status;

/* Deserialize for request */
#[derive(Deserialize)]
struct CreateUserRequest<'req> {
    name:     &'req str,
    password: &'req str,
}

/* Serialize for response */
#[derive(Serialize)]
struct User {
    name: String,
}

async fn create_user(
    JSON(req): JSON<CreateUserRequest<'_>>
) -> status::Created<JSON<User>> {
    status::Created(JSON(
        User {
            name: String::from(req.name)
        }
    ))
}

Typed params

use ohkami::prelude::*;

#[tokio::main]
async fn main() {
    Ohkami::new((
        "/hello/:name/:n"
            .GET(hello_n),
        "/hello/:name"
            .GET(hello),
        "/search"
            .GET(search),
    )).howl("localhost:5000").await
}

async fn hello(name: &str) -> String {
    format!("Hello, {name}!")
}

async fn hello_n((name, n): (&str, usize)) -> String {
    vec![format!("Hello, {name}!"); n].join(" ")
}

#[derive(Deserialize)]
struct SearchQuery<'q> {
    #[serde(rename = "q")]
    keyword: &'q str,
    lang:    &'q str,
}

#[derive(Serialize)]
struct SearchResult {
    title: String,
}

async fn search(
    Query(query): Query<SearchQuery<'_>>
) -> JSON<Vec<SearchResult>> {
    JSON(vec![
        SearchResult { title: String::from("ohkami") },
    ])
}

Middlewares

Ohkami's request handling system is called "fangs", and middlewares are implemented on this.

builtin fang :

  • Context ( typed interaction with reuqest context )
  • CORS, JWT, BasicAuth
  • Timeout ( native runtime )
  • Enamel ( experimantal; security headers )
use ohkami::prelude::*;

#[derive(Clone)]
struct GreetingFang(usize);

/* utility trait; automatically impl `Fang` trait */
impl FangAction for GreetingFang {
    async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> {
        let Self(id) = self;
        println!("[{id}] Welcome request!: {req:?}");
        Ok(())
    }
    async fn back<'a>(&'a self, res: &'a mut Response) {
        let Self(id) = self;
        println!("[{id}] Go, response!: {res:?}");
    }
}

#[tokio::main]
async fn main() {
    Ohkami::new((
        // register fangs to a Ohkami
        GreetingFang(1),
        
        "/hello"
            .GET(|| async {"Hello, fangs!"})
            .POST((
                // register *local fangs* to a handler
                GreetingFang(2),
                || async {"I'm `POST /hello`!"}
            ))
    )).howl("localhost:3000").await
}

Database connection management with Context

use ohkami::prelude::*;
use ohkami::typed::status;
use sqlx::postgres::{PgPoolOptions, PgPool};

#[tokio::main]
async fn main() {
    let pool = PgPoolOptions::new()
        .connect("postgres://ohkami:password@localhost:5432/db").await
        .expect("failed to connect");

    Ohkami::new((
        Context::new(pool),
        "/users".POST(create_user),
    )).howl("localhost:5050").await
}

async fn create_user(
    Context(pool): Context<'_, PgPool>,
) -> status::Created {
    //...

    status::Created(())
}

Static directory serving

use ohkami::prelude::*;

#[tokio::main]
async fn main() {
    Ohkami::new((
        "/".Dir("./dist"),
    )).howl("0.0.0.0:3030").await
}

File upload

use ohkami::prelude::*;
use ohkami::typed::status;
use ohkami::format::{Multipart, File};

#[derive(Deserialize)]
struct FormData<'req> {
    #[serde(rename = "account-name")]
    account_name: Option<&'req str>,
    pics: Vec<File<'req>>,
}

async fn post_submit(
    Multipart(data): Multipart<FormData<'_>>
) -> status::NoContent {
    println!("\n\
        ===== submit =====\n\
        [account name] {:?}\n\
        [  pictures  ] {} files (mime: [{}])\n\
        ==================",
        data.account_name,
        data.pics.len(),
        data.pics.iter().map(|f| f.mimetype).collect::<Vec<_>>().join(", "),
    );

    status::NoContent
}

Pack of Ohkamis

use ohkami::prelude::*;
use ohkami::typed::status;

#[derive(Serialize)]
struct User {
    name: String
}

async fn list_users() -> JSON<Vec<User>> {
    JSON(vec![
        User { name: String::from("actix") },
        User { name: String::from("axum") },
        User { name: String::from("ohkami") },
    ])
}

async fn create_user() -> status::Created<JSON<User>> {
    status::Created(JSON(User {
        name: String::from("ohkami web framework")
    }))
}

async fn health_check() -> status::NoContent {
    status::NoContent
}

#[tokio::main]
async fn main() {
    // ...

    let users_ohkami = Ohkami::new((
        "/"
            .GET(list_users)
            .POST(create_user),
    ));

    Ohkami::new((
        "/healthz"
            .GET(health_check),
        "/api/users"
            .By(users_ohkami), // nest by `By`
    )).howl("localhost:5000").await
}

Testing

use ohkami::prelude::*;
use ohkami::testing::*; // <--

fn hello_ohkami() -> Ohkami {
    Ohkami::new((
        "/hello".GET(|| async {"Hello, world!"}),
    ))
}

#[cfg(test)]
#[tokio::test]
async fn test_my_ohkami() {
    let t = hello_ohkami().test();

    let req = TestRequest::GET("/");
    let res = t.oneshot(req).await;
    assert_eq!(res.status(), Status::NotFound);

    let req = TestRequest::GET("/hello");
    let res = t.oneshot(req).await;
    assert_eq!(res.status(), Status::OK);
    assert_eq!(res.text(), Some("Hello, world!"));
}

Supported protocols

  • HTTP/1.1
  • HTTP/2
  • HTTP/3
  • HTTPS
  • Server-Sent Events
  • WebSocket

MSRV ( Minimum Supported Rust Version )

Latest stable

License

ohkami is licensed under MIT LICENSE ( LICENSE or https://opensource.org/licenses/MIT ).