hypers 0.5.1

A simple rust web framework based on hyper 1.0
Documentation

HTTP Request URL Parameters Syntax

Pattern Kind Description
:name Normal Matches a path piece, excludes /
:name? Optional Matches an optional path piece, excludes /
/:name?/ /:name? OptionalSegment Matches an optional path segment, excludes /, prefix or suffix should be /
+ :name+ OneOrMore Matches a path piece, includes /
* :name* ZeroOrMore Matches an optional path piece, includes /
/*/ /* /:name*/ /:name* ZeroOrMoreSegment Matches zero or more path segments, prefix or suffix should be /

Supports

Case Parameters
:a:b a b
:a:b? a b
:a-:b :a.:b :a~:b a b
:a_a-:b_b a_a b_b
:a\\: :a\\_ a
:a\\::b :a\\_:b a b
:a* a
* *1
*.* *1 *2
:a+ a
+ +1
+.+ +1 +2
/*/abc/+/def/g *1 +2

⚡️ Quick Start

use hypers's full feature

use hypers::prelude::*;
use std::time::Instant;
use utoipa::{
    openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
    Modify,
};
use utoipa::{IntoParams, OpenApi, ToSchema};
struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        let components = openapi.components.as_mut().unwrap(); // we can unwrap safely since there already is components registered.
        components.add_security_scheme(
            "api_key",
            SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("access_token"))),
        )
    }
}

mod extract {
    use super::*;

    pub struct Extract;

    #[derive(Serialize, Deserialize, ToSchema, Extract)]
    #[extract(source("header"))]
    pub struct HeaderParam {
        pub host: Option<String>,
        #[serde(rename = "user-agent")]
        pub user_agent: Option<String>,
        pub accept: Option<String>,
    }

    #[derive(Serialize, Deserialize, Extract)]
    #[extract(source("cookie"))]
    pub struct CookieParams {
        hypers: String,
        rust: String,
    }

    #[derive(Serialize, Deserialize, IntoParams, ToSchema, Extract, Debug)]
    #[into_params(parameter_in = Query)]
    pub struct Data {
        pub id: Option<u16>,
        pub name: Option<String>,
        pub age: Option<u16>,
        //#[serde(rename = "firstName")]
        pub first_name: Option<String>,
        //#[serde(rename = "lastName")]
        pub last_name: Option<String>,
    }

    #[derive(Serialize, Deserialize, IntoParams, ToSchema, Extract, Debug)]
    pub struct Params {
        pub id: Option<u16>,
        pub name: Option<String>,
        pub age: Option<u16>,
    }

    /// Error that might occur when managing `Data` items
    #[derive(Serialize, Deserialize, ToSchema)]
    pub enum DataError {
        /// Happens when Data item already exists
        Config(String),
        /// Data not found from storage
        NotFound(String),
        Normal(Params),
    }

    impl Responder for DataError {
        fn response(self, builder: Response) -> Response {
            match self {
                Self::Config(e) => builder.status(409).text(e.to_string()),
                Self::NotFound(e) => builder.status(404).text(e.to_string()),
                Self::Normal(p) => builder.status(200).json(&p),
            }
        }
    }

    #[openapi(prefix = "api", version = 1, name = "extract")]
    impl Extract {
        #[get("/header")]
        pub fn header(input: HeaderParam) -> impl Responder {
            (200, input)
        }

        #[get("/set_cookies")]
        pub async fn set_cookies() -> impl Responder {
            let mut cookie1 = Cookie::new("hypers", "hypers2023");
            cookie1.set_path("/extract");

            let mut cookie2 = Cookie::new("rust", "rust2023");
            cookie2.set_path("/extract");

            let mut cookie_jar = CookieJar::new();
            cookie_jar.add(cookie1);
            cookie_jar.add(cookie2);
            (200, format!("cookie_jar is :{:?}", cookie_jar))
        }

        #[get("/cookies")]
        pub async fn cookies(input: CookieParams) -> impl Responder {
            (200, input)
        }

        #[get("param/:id/:name/:age")]
        pub async fn param(data: Params) -> impl Responder {
            if data.age.ne(&Some(18)) {
                (
                    404,
                    DataError::NotFound(String::from("age must be equal to 18")),
                )
            } else {
                (200, DataError::Normal(data))
            }
        }

        #[post("post_query")]
        #[get("query")]
        pub async fn query(data: Data) -> impl Responder {
            (200, data)
        }

        // Context-Type : application/x-www-form-urlencoded
        #[get("get_form")]
        #[patch("form")]
        pub async fn form(user: Data) -> impl Responder {
            Ok::<_, Error>((200, user))
        }

        // Context-Type : multipart/form-data Form Fields
        #[post("multipart_form")]
        pub async fn multipart_form(form_data: Data) -> impl Responder {
            Ok::<_, Error>((200, form_data))
        }

        // Context-Type : application/json
        #[post("json")]
        pub async fn json(user: Data) -> impl Responder {
            Ok::<_, Error>((200, user))
        }
    }
}

mod parse {
    use super::*;

    #[derive(Serialize, Deserialize, ToSchema, Debug)]
    pub struct Data {
        pub id: Option<u16>,
        pub name: Option<String>,
        pub age: Option<u16>,
        //#[serde(rename(deserialize = "firstName"))]
        pub first_name: Option<String>,
        //#[hypers(extract(rename = "lastName"))]
        // #[serde(rename(serialize = "lastName", deserialize = "lastName"))]
        pub last_name: Option<String>,
    }

    #[utoipa::path(
    get,
    path = "/parse/header",
    tag = "parse request headers",
    responses(
        (status = 200, description = "parse request header successfully"),
        (status = 400, description = "parse request header failed")
    ))]
    #[handler]
    pub fn header(host: Header<String, true>, accept: Header<String, true>) -> impl Responder {
        (
            200,
            format!(
                "host is :{}\n,accept is :{}\n",
                host.inner(),
                accept.inner(),
            ),
        )
    }

    #[utoipa::path(
    get,
    path = "/parse/cookies",
    tag = "parse cookies",
    responses(
        (status = 200, description = "Parse cookies from request successfully",body = String),
        (status = 400, description = "Parse cookies from request failed")
    ))]
    #[handler]
    pub fn cookies(
        hypers: CookieParam<String, true>,
        rust: CookieParam<String, true>,
    ) -> impl Responder {
        Ok::<_, Error>((
            200,
            format!("first cookie is {}, second cookie is {}", hypers, rust),
        ))
    }

    #[utoipa::path(
    delete,
    path = "/parse/param/{id}/{name}/{age}",
    tag = "parse request url path params",
    params(
        ("id" = u16, Path, description = "Id of readme to delete"),
        ("age" = u16, Path, description = "Age of readme to delete"),
        ("name" = String, Path, description = "Name of readme to delete"),
    ),
    responses(
        (status = 200, description = "Parse Url Path Params successfully",body = Data),
        (status = 400, description = "Parse Url Path Params failed")
    ),
    security(
        ("api_key" = [])
    ))]
    #[handler]
    pub fn param(
        req: &mut Request,
        id: Path<u16>,
        name: Path<String>,
        age: Path<u16>,
    ) -> impl Responder {
        let app_state = req.get::<&str>("key");
        println!("app_state = {:?}", app_state);
        let api_key = req
            .headers()
            .get("readme_apikey")
            .map(|v| v.to_str().unwrap_or_default().to_string())
            .unwrap_or_default();
        println!("api_key = {:?}", api_key);
        (
            200,
            Json(Data {
                id: Some(id.0),
                name: Some(name.0),
                age: Some(age.0),
                first_name: None,
                last_name: None,
            }),
        )
    }

    #[utoipa::path(
    get,
    path = "/parse/query",
    tag = "parse request url query params",
    params(
        ("id" = u16, Query, description = "Id of readme to get"),
        ("age" = u16, Query, description = "Age of readme to get"),
        ("name" = String, Query, description = "Name of readme to get"),
    ),
    responses(
        (status = 200, description = "Parse request url query params successfully", body = Data),
        (status = 400, description = "Parse request url query params failed")
    ))]
    #[handler]
    pub fn query(
        id: Query<u16, true>,
        name: Query<String, true>,
        age: Query<u16, true>,
    ) -> impl Responder {
        Ok::<_, Error>((
            200,
            Json(Data {
                id: Some(id.inner()),
                name: Some(name.inner()),
                age: Some(age.inner()),
                first_name: None,
                last_name: None,
            }),
        ))
    }

    // Url Query Params
    #[utoipa::path(
    get,
    path = "/parse/query_vec",
    tag = "parse request url query params",
    params(
        ("name" = Vec<String>, Query, description = "Url Query Params name"),
        ("age" = u16, Query, description = "Url Query Params age"),
    ),
    responses(
        (status = 200, description = "successfully, response = String"),
        (status = 400, description = "failed")
    ))]
    #[handler]
    pub fn query_vec(req: &mut Request) -> impl Responder {
        let name = req.query::<Vec<String>>("name")?;
        let age = req.query::<u16>("age")?;
        Some((200, format!("name = {:?} , age = {}", name, age)))
    }

    // Context-Type : application/x-www-form-urlencoded
    #[utoipa::path(
    patch,
    path = "/parse/form",
    tag = "parse request body",
    request_body(
        content = Data,
        content_type = "application/x-www-form-urlencoded",
    ),
    responses(
        (status = 200, description = "FormParams created successfully", body = Data),
        (status = 409, description = "FormParams already exists")
    ))]
    #[handler]
    pub fn form(user: Form<Data>) -> impl Responder {
        Ok::<_, Error>((200, Json(user.0)))
    }

    // Context-Type : multipart/form-data Form Fields
    #[utoipa::path(
    post,
    path = "/parse/multipart_form",
    tag = "parse request body",
    request_body(
        content = Data,
        content_type = "multipart/form-data",
    ),
    responses(
        (status = 200, description = "FormParams created successfully", body = Data),
        (status = 409, description = "FormParams already exists")
    ))]
    #[handler]
    pub fn multipart_form(form_data: Form<Data>) -> impl Responder {
        Ok::<_, Error>((200, Json(form_data.0)))
    }

    // Context-Type : multipart/form-data Files
    #[utoipa::path(
    post,
    path = "/parse/multipart_file",
    tag = "upload files",
    responses(
        (status = 200, description = "upload files successfully", body = String),
        (status = 400, description = "upload files failed")
    ))]
    #[handler]
    pub async fn multipart_file(req: &mut Request) -> impl Responder {
        let file = req.file("file").await?;
        let file_name = file.name()?;
        let file_name = file_name.to_string();

        let img = req.files("imgs").await?;
        let imgs_name = img
            .iter()
            .map(|m| m.name().unwrap().to_string())
            .collect::<Vec<String>>()
            .join(",");

        Some((
            200,
            format!("file_name = {}, imgs_name = {}", file_name, imgs_name),
        ))
    }

    // Context-Type : application/json
    #[utoipa::path(
    post,
    path = "/parse/json",
    tag = "parse request body",
    request_body(
        content = Data,
        content_type = "application/json",
    ),
    responses(
        (status = 200, description = "Data item created successfully", body = Data),
        (status = 409, description = "Data already exists")
    ))]
    #[handler]
    pub fn json(user: Json<Data>) -> impl Responder {
        Ok::<_, Error>((200, user))
    }

    #[derive(OpenApi)]
    #[openapi(
    info(title = "Parse Api", description = "Parse Api description"),
    paths(header,cookies,param,query,query_vec,form,multipart_form,multipart_file,json),
    components(schemas(Data)),
    modifiers(&SecurityAddon),
    tags(
        (name = "parse", description = "Parse items management API")
    ))]
    struct ParseApiDoc;

    pub fn parse_router() -> Router {
        let mut parse = Router::new("parse");

        parse.get("header", header);
        parse.get("cookies", cookies);
        parse.delete("param/:id/:name/:age", param);
        parse.get("query", query);
        parse.get("query_vec", query_vec);
        parse.patch("form", form);
        parse.post("multipart_form", multipart_form);
        parse.post("multipart_file", multipart_file);
        parse.post("json", json);

        parse.openapi(ParseApiDoc::openapi());
        parse
    }
}

mod middleware {
    use super::*;

    // Middleware Function
    #[handler]
    pub async fn stat_time(req: &mut Request, next: &Next<'_>) -> Result {
        // Before executing the request processing function
        let start_time = Instant::now();

        // Calling subsequent request processing functions
        let res = next.next(req).await?;

        // After the execution of the request processing function
        let elapsed_time = start_time.elapsed();

        println!(
            "The current request processing function takes time :{:?}",
            elapsed_time
        );
        Ok(res)
    }

    // Middleware Function
    #[handler]
    pub async fn app_state(req: &mut Request, next: &Next<'_>) -> Result {
        req.set("key", "Hello World");
        next.next(req).await
    }

    use jsonwebtoken::{
        decode, encode, errors::ErrorKind, DecodingKey, EncodingKey, Header, Validation,
    };

    pub const SECRET_KEY: &str = "afafava-rust-lang";

    #[derive(Debug, Serialize, Deserialize, Default)]
    pub struct JWTClaims {
        pub username: String,
        pub exp: i64,
    }

    #[handler]
    impl JWTClaims {
        pub fn generate_token(&self, secret: &str) -> Result<String> {
            return match encode(
                &Header::default(),
                &self,
                &EncodingKey::from_secret(secret.as_ref()),
            ) {
                Ok(t) => Ok(t),
                Err(_) => Err(Error::Response(401, json!("JWTToken encode fail!"))),
            };
        }

        pub fn verify(secret: &str, token: &str) -> Result<Self, Error> {
            let validation = Validation::default();
            return match decode::<JWTClaims>(
                token,
                &DecodingKey::from_secret(secret.as_ref()),
                &validation,
            ) {
                Ok(c) => Ok(c.claims),
                Err(err) => match *err.kind() {
                    ErrorKind::InvalidToken => {
                        return Err(Error::Response(401, json!("InvalidToken")))
                    }
                    ErrorKind::InvalidIssuer => {
                        return Err(Error::Response(401, json!("InvalidIssuer")))
                    }
                    _ => return Err(Error::Response(401, json!("InvalidToken other errors"))),
                },
            };
        }

        pub fn checked_token(token: &str) -> Result<JWTClaims> {
            let claims = JWTClaims::verify(SECRET_KEY, token);
            match claims {
                Ok(token) => Ok(token),
                Err(e) => Err(Error::Other(e.to_string())),
            }
        }

        async fn handle(&self, req: &mut Request, next: &Next<'_>) -> Result<Response> {
            let token = req
                .headers()
                .get("access_token")
                .map(|v| v.to_str().unwrap_or_default().to_string())
                .unwrap_or_default();
            match Self::checked_token(&token) {
                Ok(_) => next.next(req).await,
                Err(e) => Err(Error::Other(e.to_string())),
            }
        }
    }
}

mod root {
    use super::*;
    use middleware::{JWTClaims, SECRET_KEY};
    use time::{Duration, OffsetDateTime};

    #[utoipa::path(
    get,
    path = "/html",
    tag = "html",
    responses(
        (status = 200, description = "html successfully", body = String, content_type = "text/html" ),
        (status = 400, description = "html failed")
    ))]
    #[handler]
    pub async fn html() -> impl Responder {
        Text::Html("<html><body>hello</body></html>")
    }

    // Websocket  ws://127.0.0.1:7878/hello/ws  http://www.jsons.cn/websocket/
    #[handler]
    pub async fn websocket(req: &mut Request) -> impl Responder {
        let name = req.param::<String>("name").unwrap_or_default();
        WebSocketUpgrade::new()
            .upgrade(req, |mut ws| async move {
                while let Ok(msg) = ws.receive().await {
                    if let Some(msg) = msg {
                        match msg {
                            Message::Text(text) => {
                                let text = format!("{},{}", name, text);
                                ws.send(Message::Text(text)).await.unwrap();
                            }
                            Message::Close(_) => break,
                            _ => {}
                        }
                    }
                }
            })
            .await
    }

    #[derive(Serialize, Deserialize, Default, ToSchema, Debug)]
    pub struct User {
        pub name: String,
        pub age: u16,
    }

    #[derive(Serialize, Deserialize, Default)]
    pub struct ApiResponse<T> {
        code: u32,
        msg: String,
        data: T,
    }

    #[utoipa::path(
    post,
    path = "/register",
    tag = "register user",
    request_body(
        content = User,
        content_type = "application/json",
    ),
    responses(
        // (status = 200, description = "register user successfully", body = ApiResponse),
        (status = 200, description = "register user successfully"),
        (status = 400, description = "register user failed")
    ))]
    #[handler]
    async fn register(user: Json<User>) -> impl Responder {
        let exp = OffsetDateTime::now_utc() + Duration::days(14);
        let exp = exp.unix_timestamp();
        let jwt = JWTClaims {
            username: user.0.name,
            exp,
        };

        let token = jwt.generate_token(SECRET_KEY)?;
        Ok::<_, Error>((
            200,
            Json(ApiResponse {
                code: 200,
                msg: String::from("successful"),
                data: token,
            }),
        ))
    }

    #[derive(OpenApi)]
    #[openapi(
    info(title = "Root Api", description = "Root Api description"),
    paths(html,register),
    components(schemas(User)),
    tags(
        (name = "root", description = "Root items management API")
    ))]
    struct RootApiDoc;

    pub fn root_router() -> Router {
        // The Root Router
        let mut root = Router::new("");
        root.get("/*", StaticDir::new("src").listing(true));
        root.get("html", html);
        root.get(":name/ws", websocket);
        root.post("register", register);

        // Add Middleware  ( Middleware can be added to any routing node )
        root.hook(middleware::stat_time, vec!["/"], None);
        root.hook(
            middleware::JWTClaims::default(),
            vec!["api/v1/extract"],
            vec!["/register"],
        );
        root.hook(
            middleware::app_state,
            vec!["/parse/param/:id/:name/:age"],
            None,
        );
        // Add sub router to the root router
        root.controller(extract::Extract);
        root.push(parse::parse_router());

        root.openapi(RootApiDoc::openapi());
        root
    }
}

// Write router like a tree
fn main() -> Result<()> {
    // The Root Router
    let mut root = crate::root::root_router();

    // Accessing in a browser  http://127.0.0.1:7878/swagger-ui/
    root.swagger("swagger-ui");

    println!("root router = {:#?}", root);

    // Start Server
    hypers::run(root, "127.0.0.1:7878")
}