40 releases (7 breaking)

0.8.3 Jan 15, 2023
0.7.3 Jan 9, 2023
0.6.3 Dec 30, 2022

#71 in HTTP server

Download history 88/week @ 2023-02-02 40/week @ 2023-02-09 181/week @ 2023-02-16 7/week @ 2023-02-23 46/week @ 2023-03-02 1/week @ 2023-03-09 2/week @ 2023-03-16 129/week @ 2023-03-23 2/week @ 2023-03-30 50/week @ 2023-04-06 81/week @ 2023-04-13 5/week @ 2023-04-20 1/week @ 2023-04-27 79/week @ 2023-05-04 4/week @ 2023-05-11 2/week @ 2023-05-18

87 downloads per month

MIT license

145KB
3K SLoC

ohkami

ohkami - [狼] means wolf in Japanese - is simple and macro free web framework for Rust.


Features

  • simple: Less things to learn / Less code to write / Less time to hesitate.
  • macro free: No need for using macros.
  • async handlers
  • easy error handling

0.7 → 0.8

Reorganized middleware system and added after-handling middleware:

fn main() -> Result<()> {
    let middleware = Middleware::new()
        .beforeGET("/", async |c| {
            tracing::info!("Helllo, middleware!");
            c
        })
        .afterANY("/api/*", async |res| {
            res.add_header(
                Header::AccessControlAllowOrigin,
                "mydomain:8000"
            );
            res
        });
    
    // ...
}
  • Before-handling middleware takes Context and returns Context
  • After-handling middlware takes Response and returns Response
  • Middleware routes can use wildcard ( * ). In current ohkami, wildcard doesn't match empty string. This design may change in future version.

0.8.3

Added Context::store (experimentally):

async fn handler(c: Context, id: u64) -> Result<Response> {
    let cache = c.store().await;

    let object = match cache.get(&id.to_string()) {
        Some(name) => Object::new(name),
        None => sqlx::query_as::<_, Object>(
            "SELECT id, name FROM table WHERE id = $1"
        ).bind(id)
            .fetch_one(ctx.pool())
            .await?
    };

    c.OK(object)
}

Quick start

  1. Add dependencies:
[dependencies]
ohkami = "0.8.3"
  1. Write your first code with ohkami:
use ohkami::prelude::*;

fn main() -> Result<()> {
    Ohkami::default()
        .GET("/", || async {
            Response::OK("Hello, world!")
        })
        .howl(":3000")
}
  1. If you're interested in ohkami, learn more by examples and documentation !

signature of handler

async fn ( Context?, {path param 1}?, {path param 2}?, {impl JSON}? ) -> Result<Response>

// `?` means "this is optional".
  • path param:String | usize | u64 | usize | i64 | i32
  • Current ohkami doesn't handle more than 2 path parameters. This design may change in future version.

Snippets

handle query params

// c: Context

let name: &str = c.req.query("name")?;

let count: usize = c.req.query("count")?;

handle path params

use std::{thread::sleep, time::Duration};

fn main() -> Result<()> {
    Ohkami::default()
        .GET("/sleepy/:time/:name", sleepy_hello)
        .howl("localhost:8080")
}

async fn sleepy_hello(time: u64, name: String) -> Result<Response> {
    (time < 30)
        ._else(|| Response::BadRequest(
            "sleeping time (sec) must be less than 30."
        ))?;
        
    sleep(Duration::from_secs(time));
    Response::OK(format!("Hello {name}, I'm so sleepy..."))
}

handle request body

Add serde = { version = "1.0", features = ["derive"] } in your dependencies ( JSON requires it internally )

#[derive(JSON)]
struct User {
    id:   i64,
    name: String,
}

async fn reflect(user: User) -> Result<Response> {
    Response::OK(user)
}

async fn reflect_name(user: User) -> Result<Response> {
    let name = user.name;
    Response::OK(name)
}

group handlers (like axum)

use ohkami::{
    prelude::*,
    group::{GET, POST} // import this
};

#[derive(JSON)]
struct User {
    id:   usize,
    name: String,
}

fn main() -> Result<()> {
    Ohkami::default()
        .GET("/", || async {
            Response::OK("Hello!")
        })
        .route("/api",
            GET(hello_api).POST(reflect)
        )
        .howl(":3000")
}

async fn hello_api() -> Result<Response> {
    Response::OK("Hello, api!")
}

async fn reflect(payload: User) -> Result<Response> {
    Response::OK(payload)
}

get request headers

let host = c.req.header(Header::Host)?;
async fn reflect_xcustom_header_value(c: Context) -> Result<Response> {
    let custom_header_value = c.req.header("X-Custom")?;
    c.OK(format!("`X-Custom`'s value is {custom_header_value}"))
}

add response headers

c.add_header(Header::AccessControlAllowOrigin, "mydomain:8000");
// or
c.add_header("Access-Control-Allow-Origin", "mydomain:8000");

// `Response` also has the same method
use ohkami::prelude::*;
use ohkami::Header::AccessControlAllowOrigin;

async fn cors(mut res: Response) -> Response {
    res.add_header(AccessControlAllowOrigin, "mydomain:8000");
    res
}

fn main() -> Result<()> {
    let my_middleware = Middleware::new()
        .afterANY("/api/*", cors);

    // ...

OK response with text/plain

Response::OK("Hello, world!")
c.OK("Hello, world!")

OK response with application/json

Response::OK(json!({"ok": true}))

c.OK(json!(100))

c.OK(json!("Hello, world!"))
async fn reflect_id(id: u64) -> Result<Response> {
    Response::OK(json!{"id": id})
}

OK can take JSON-derived value directly:

#[derive(JSON)]
struct User {
    id:   u64,
    name: String,
}

// ...

let user = User { id: 1, name: String::from("John") };

Response::OK(user)
// or
c.OK(user)

handle errors

make_ohkami_result()?;

// or, you can add an error context message:
make_ohkami_result()
    ._else(|e| e.error_context("failed to get user data"))?;

// or discard original error:
make_ohkami_result()
    ._else(|_| Response::InternalServerError("can't get user"))?;
    // or
    ._else(|_| Response::InternalServerError(None))?;
make_some_result(/* can't use `?` */)
    ._else(|e| Response::InternalServerError(e.to_string()))?;

make_some_result()
    ._else(|_| Response::InternalServerError(None))?;

handle Option values

let handler = self.handler
    ._else(|| Response::NotFound("handler not found"))?;
    // or
    ._else(|| Response::NotFound(None))?;

assert boolean conditions

(count < 10)
    ._else(|| Response::BadRequest("`count` must be less than 10"))?;
    // or
    ._else(|| Response::BadRequest(None))?;

log config

Add tracing and tracing_subscriber in your dependencies.

fn main() -> Result<()> {
    let config = Config {
        log_subscribe: Some(
            tracing_subscriber::fmt()
                .with_max_level(tracing::Level::TRACE)

            /* default value:

            tracing_subscriber::fmt()
                .with_mac_level(tracing::Level::DEBUG)

            */
        ),
        ..Default::default()
    };
    Ohkami::with(config)
        .GET("/", || async {Response::OK("Hello!")})
}

DB config

Eneble one of following pairs of features:

  • sqlx and postgres
  • sqlx and mysql
let config = Config {
    db_profile: DBprofile {
        options: PgPoolOptions::new().max_connections(20),
        url:     DB_URL.as_str(),
    },
    ..Default::default()
};

use sqlx

Eneble one of following pairs of features:

  • sqlx and postgres
  • sqlx and mysql
let user = sqlx::query_as::<_, User>(
    "SELECT id, name FROM users WHERE id = $1"
).bind(1)
    .fetch_one(c.pool())
    .await?; // `Response` implements `From<sqlx::Error>`

use middlewares

fn main() -> Result<()> {
    let middleware = Middleware::new()
        .beforeANY("*", |c| async {
            tracing::info!("Hello, middleware!");
            c
        });

    Ohkami::with(middleware)
        .GET("/", || async {
            Response::OK("Hello!")
        })
        .howl("localhost:3000")
}
fn main() -> Result<()> {
    let config = Config {
        log_subscribe: Some(
            tracing_subscriber::fmt()
                .with_max_level(tracing::Level::TRACE)
        ),
        ..Default::default()
    };

    let middleware = Middleware::new()
        .beforeANY("*", |c| async {
            tracing::info!("Hello, middleware!");
            c
        });

    let thirdparty_middleware = some_external_crate::x;

    Ohkami::with(config.and(middleware).and(x))
        .GET("/", || async {
            Response::OK("Hello!")
        })
        .howl("localhost:3000")
}

test

  1. split setup process from main function:
fn server() -> Ohkami {
    Ohkami::default()
        .GET("/", || async {
            Response::OK("Hello!")
        })
}

fn main() -> Result<()> {
    server().howl(":3000")
}
  1. import testing::Test and other utils
#[cfg(test)]
mod test {
    use ohkami::{Ohkami, response::Response, testing::{Test, Request, Method}};
    use once_cell::sync::Lazy;

    static SERVER: Lazy<Ohkami> = Lazy::new(|| super::server());

    #[test]
    fn test_hello() {
        let req = Request::new(Method::GET, "/");
        SERVER.assert_to_res(&req, Response::OK("Hello!"));
    }
}

Development

ohkami is not for producntion use now.
Please give me your feedback ! → GetHub issue


License

This project is licensed under MIT LICENSE (LICENSE-MIT or https://opensource.org/licenses/MIT).

Dependencies

~4–13MB
~238K SLoC