athene 1.2.2

A simple and lightweight rust web framework based on Hyper, with routing similar to Java SpringBoot and Go gin
Documentation

example

Useage

use askama::Template;
use athene::prelude::*;
use athene::{html, json, status};
use headers::authorization::Bearer;
use headers::Authorization;
use serde::{Deserialize, Serialize};
use tracing::info;

use validator::{Validate, ValidationError};
#[derive(Serialize, Deserialize, Clone, Validate)]
pub struct User {
    #[validate(email)]
    pub username: String,
    #[validate(range(min = 18, max = 20))]
    pub age: u16,
    #[validate(custom = "validate_user_role")]
    pub role: Option<String>,
}

fn validate_user_role(role: &str) -> Result<(), ValidationError> {
    if role.ne("student") {
        return Err(ValidationError::new("invalid role"));
    }
    Ok(())
}

// ================================ Basic Controller ===================================

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct AtheneController {
    pub label: String,
    pub keyword: String,
}

impl Controller for AtheneController {
    const BASE_PATH: &'static str = "/api/v1/athene";

    fn method(&self) -> Vec<ControllerMethod<Self>>
    where
        Self: Sized,
    {
        let r = ControllerBuilder::new();
        r.get("/index", |_, _req: Request| async {
            "athene is a simple and lightweight rust web framework based on Hyper"
        })
        .post("/add", Self::add)
        .delete("/{label}/{keyword}", Self::delete)
        .put("/update", Self::update)
        .get("/get", Self::get)
        .build()
    }
}

impl AtheneController {
    pub async fn add(&self, mut req: Request) -> impl Responder {
        let obj = req.parse::<Self>().await?;
        Ok::<_, Error>((200, Json(obj)))
    }

    pub async fn delete(&self, mut req: Request) -> impl Responder {
        let lable = req.param("label");
        let keyword = req.param("keyword");
        /*
           ...
        */
        info!("{}, .... {}", lable, keyword);
        (200, "delete success")
    }

    async fn update(&self, mut req: Request) -> impl Responder {
        if let Ok(obj) = req.body_mut().parse::<Form<Self>>().await {
            /*
               ...
            */
            info!("{:#?}", obj);
            (200, String::from("update success"))
        } else {
            (400, String::from("update failed"))
        }
    }

    async fn get(&self, req: Request) -> impl Responder {
        #[derive(Deserialize, Serialize)]
        struct QueryParam<'a> {
            label: &'a str,
            keyword: &'a str,
        }

        let arg = req.query::<QueryParam>()?;
        /*
           ...
        */
        let res = json!(&arg);
        Ok::<_, Error>(res)
    }
}

// ================================ Macro Controller ===================================
pub struct UserController {}

// http://127.0.0.1:7878/api/v1/user
#[controller(prefix = "api", version = 1, name = "user")]
impl UserController {
    #[get("/*/**")]
    pub async fn match_any_route(&self, req: Request) -> impl Responder {
        let uri_path = req.uri().path().to_string();
        let method = req.method().to_string();
        (200, format!("uri : {}, method: {}", uri_path, method))
    }

    #[delete("/{username}/{age}")]
    pub async fn delete_by_param(&self, username: String, age: Option<u16>) -> impl Responder {
        (
            200,
            format!("username is : {}, and age is : {:?}", username, age),
        )
    }

    #[get("/get_query_1")]
    pub async fn get_query_1(&self, username: String, age: u16) -> impl Responder {
        (
            200,
            Json(User {
                username,
                age,
                role: None,
            }),
        )
    }

    #[get("/get_query_2")]
    pub async fn get_query_2(&self, request: Request) -> impl Responder {
        let user = request.query::<User>()?;
        Ok::<_, Error>((200, Json(user)))
    }

    // Context-Type : application/json
    #[post("/post_json")]
    #[validator] // The user parameter will be validated
    pub async fn post_json(&self, user: Json<User>) -> impl Responder {
        Ok::<_,Error>((200, user))
    }

    // Context-Type : application/x-www-form-urlencoded
    #[get("/user_form")]
    #[post("/user_form")]
    #[validator(exclude("user"))] // The user parameter will not be validated
    async fn user_form(&self, user: Form<User>) -> impl Responder {
        (200, user)
    }

    // Context-Type : application/json  Or application/x-www-form-urlencoded
    // Context-Type : application/msgpack Or application/cbor
    #[post("/parse_body")]
    #[validator] // User will be validated
    async fn parse_body(&self, mut req: Request) ->impl Responder{
        let user = req.parse::<User>().await?;
        Ok::<_,Error>((
            200,
            format!("username = {} and age = {}", user.username, user.age),
        ))
    }

    // Context-Type : application/multipart-formdata
    #[post("/files")]
    async fn files(&self, mut req: Request) ->impl Responder {
        let files = req.files("files").await?;
        let fist_file_name = files[0].name().unwrap();
        let second_file_name = files[1].name().unwrap();
        let third_file_name = files[2].name().unwrap();
        Ok::<_,Error>((
            200,
            format!(
                "fist {}, second {}, third {}",
                fist_file_name, second_file_name, third_file_name
            ),
        ))
    }
}

// ================================ Middleware  ======================
struct ApiKeyMiddleware {
    api_key: String,
}

#[middleware]
impl ApiKeyMiddleware {
    async fn next(&self, ctx: Context, chain: &dyn Next) -> Result<Context, Error> {
        if let Some(bearer) = ctx
            .state
            .request_unchecked()
            .header::<Authorization<Bearer>>()
        {
            let token = bearer.0.token();
            if token == self.api_key {
                info!(
                    "Handler {} will be used",
                    ctx.metadata.name.unwrap_or("unknown")
                );
                chain.next(ctx).await
            } else {
                info!("Invalid token");
                Ok(ctx)
            }
        } else {
            info!("Not Authenticated, ");
            Ok(ctx)
        }
    }
}

async fn log_middleware(ctx: Context, next: &'static dyn Next) -> Result<Context, Error> {
    info!(
        "new request on path: {}",
        ctx.state.request_unchecked().uri().path()
    );

    let ctx = next.next(ctx).await?;

    info!(
        "new response with status: {}",
        ctx.state.response_unchecked().status()
    );
    Ok(ctx)
}

// ================================  Handler ===================================

pub async fn hello(_req: Request) -> impl Responder {
    (200, "Hello, World!")
}

pub async fn user_params(mut req: Request) -> impl Responder {
    let username = req.param("username");
    let age = req.param("age");
    let age = age.parse::<u16>().unwrap();
    (
        200,
        Json(User {
            username,
            age,
            role: None,
        }),
    )
}

pub async fn login(mut req: Request) -> impl Responder {
    if let Ok(user) = req.body_mut().parse::<Json<User>>().await {
        (
            200,
            format!("username: {} , age: {} ", user.username, user.age),
        )
    } else {
        (400, String::from("Bad user format"))
    }
}

pub async fn sign_up(mut req: Request) -> impl Responder {
    let user = req.body_mut().parse::<Form<User>>().await?;
    Ok::<_,Error>((200, Json(user)))
}

pub async fn read_body(mut req: Request) -> impl Responder {
    let body = req.body_mut().parse::<String>().await?;
    Ok::<_,Error>((200, body))
}

// ================================  Router ===================================
pub fn user_router(r: Router) -> Router {
    r.get("/hello", hello)
        .get("/{username}/{age}", user_params)
        .post("/login", login)
        .post("/sign_up", sign_up)
        .put("/read_body", read_body)
}

#[tokio::main]
pub async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt().compact().init();

    let app = athene::new()
        .router(|r| {

            // ====================== Closure ==========================
            r.get("/", |_req: Request| async {
                "Welcome to the athene web framework "
            })


            // Permanent Redirect
            .get("/permanent", |_req: Request| async {
                let res = Builder::new();
                res.redirect(StatusCode::PERMANENT_REDIRECT, "/")
            })


            // Temporary Redirect
            .get("/temporary", |req: Request| async move {
                let uri = req.uri().path();
                info!("uri = {}", uri);
                let res = Builder::new();
                res.redirect(StatusCode::TEMPORARY_REDIRECT, "/download")
            })


            .get("/upload", |_req: Request| async {
                html!(INDEX_HTML)
            })


            // Context-Type : application/multipart-formdata
            .post("/upload", |mut req: Request| async move {
                req.upload("file", "temp").await
            })
            

            // Content-Disposition: application/octet-stream
            .get("/download", |_req: Request| async {
                let mut res = status!(hyper::StatusCode::OK);
                res.write_file("templates/author.txt", DispositionType::Attachment)?;
                Ok::<_, Error>(res)
            })


            .delete("/delete", |req: Request| async move {
                let path = req.uri().path().to_string();
                (200, path)
            })


            // use json!()
            .patch("/patch", |_req: Request| async {
                let user = User {
                    username: "Rustacean".to_string(),
                    age: 18,
                    role: None,
                };
                json!(&user)
            })
            

            // Template rendering
            .get("/html", |_req: Request| async {
                #[derive(Serialize, Template)]
                #[template(path = "favorite.html")]
                pub struct Favorite<'a> {
                    title: &'a str,
                    time: i32,
                }

                let love = Favorite {
                    title: "rust_lang",
                    time: 1314,
                };
                html!(love.render().unwrap())
            })
        })

        .router(user_router)

        .router(|r| {
            r.controller(UserController {})
                .controller(AtheneController::default())
        })

        .middleware(|m| {
            m.apply(
                ApiKeyMiddleware {
                    api_key: "athene".to_string(),
                },
                vec!["/api/v1/user"],
                vec!["/api/v1/athene"],
            )
            .apply(log_middleware, vec!["/"], None)
        })
        .build();

    app.listen("127.0.0.1:7878").await
}

static INDEX_HTML: &str = r#"<!DOCTYPE html>
<html>
    <head>
        <title>Upload Test</title>
    </head>
    <body>
        <h1>Upload Test</h1>
        <form action="/upload" method="post" enctype="multipart/form-data">
            <input type="file" name="file" />
            <input type="submit" value="upload" />
        </form>
    </body>
</html>
"#;