athene 1.4.11

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

example

This version is built on hyper v0.14 and crate async_trait is used

use athene's full feature

use athene::prelude::*;
use headers::{authorization::Bearer, Authorization};
use serde::{Deserialize, Serialize};
use tracing::info;
use validator::Validate;

#[derive(Debug, Serialize, Deserialize, Validate, Default)]
pub struct UserController {
    #[validate(email)]
    pub email: String, // admin@outlook.com
    #[validate(range(min = 18, max = 20))]
    pub age: u16,
}

// http://127.0.0.1:7878/api/v1/user
#[controller(prefix = "api", version = 1, name = "user")]
impl UserController {

    // http://127.0.0.1:7878/api/v1/user/query/?email=admin@outlook.com&age=19
    // query params
    #[get("/query")] // user will be validate
    pub async fn query(&self, user: Query<Self>) -> impl Responder {
        (200, Json(user.0))
    }

    // http://127.0.0.1:7878/api/v1/user/create
    // Context-Type : application/json
    #[post("/create")] // user will be validate
    async fn create(&self, user: Json<Self>) -> impl Responder {
        Ok::<_, Error>((200, user))
    }

    // http://127.0.0.1:7878/api/v1/user/update
    // Context-Type : application/x-www-form-urlencoded
    #[put("/update")]
    #[validator(exclude("user"))] // user will not be validate
    async fn update(&self, user: Form<Self>) -> impl Responder {
        Ok::<_, Error>((200, user))
    }

    // http://127.0.0.1:7878/api/v1/user/admin@outlook.com/18
    // uri path params
    #[delete("/{email}/{age}")]
    pub async fn delete(&self, email: String, age: Option<u16>) -> impl Responder {
        (
            StatusCode::OK,
            format!("email is : {}, and age is : {:?}", email, age),
        )
    }

    // http://127.0.0.1:7878/api/v1/user/query_get/?email=admin@outlook.com&age=29
    // query params
    #[get("/query_get")]
    pub async fn query_get(&self, email: String, age: u16) -> impl Responder {
        (200, Json(Self { email, age }))
    }
}

// Function Middleware
pub async fn log_middleware(ctx: Context, next: &dyn Next) -> Result {
    let uri_path = ctx.state.request().map(|req| req.uri().path());
    info!("new request on path: {:?}", uri_path);
    let ctx = next.next(ctx).await?;
    let status = ctx.state.response().map(|res| res.status());
    info!("new response with status: {:?}", status);
    Ok(ctx)
}

struct ApiKeyMiddleware {
    api_key: String,
}

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

// Macro Middleware
#[middleware]
impl ApiKeyMiddleware {
    async fn next(&self, ctx: Context, chain: &dyn Next) -> Result {
        if let Some(req) = ctx.state.request() {
            if let Some(bearer) = req.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");
                    let res = MiddlewareError {
                        code: 401,
                        msg: String::from("Invalid token"),
                        data: "Invalid token",
                    };
                    Err(Error::Response(401, json!(res)))
                }
            } else {
                info!("Not Authenticated");
                Err(Error::Response(401, json!("Not Authenticated")))
            }
        } else {
            Ok(ctx)
        }
    }
}

// http://127.0.0.1:7878/upload
pub async fn upload(mut req: Request) -> impl Responder {
    let res = req.upload("file", "temp").await?;
    if res > 0 {
        Ok::<_, Error>((200, "File uploaded successfully"))
    } else {
        Ok::<_, Error>((400, "File upload failed"))
    }
}

fn file_router(r: Router) -> Router {
    r.get("/upload", |_: Request| async { Html(INDEX_HTML) })
        .post("/upload", upload)
}

// Websocket  ws://127.0.0.1:7878/hello/ws  http://www.jsons.cn/websocket/
pub async fn ws(mut req: Request, mut tx: WebSocketSender, mut rx: WebSocketReceiver) -> Result<()> {
    let name = req.param::<String>("name")?;
    while let Ok(msg) = rx.receive().await {
        if let Some(msg) = msg {
            match msg {
                Message::Text(text) => {
                    let text = format!("{},{}",name,text);
                    tx.send(Message::Text(text)).await?;
                },
                Message::Close(_) => break,
                _ => {}
            }
        }
    }
    Ok(())
}

#[tokio::main]
pub async fn main() -> Result<()> {

    tracing_subscriber::fmt().compact().init();

    let app = athene::new()
        .router(file_router)
        .router(|r| 
            r.controller(UserController::default())
             .ws("/{name}/ws", ws)
             .get("/**", StaticDir::new("./").with_listing(true))
        );
        
    let app = app.middleware(|m| {
        m.apply(log_middleware, vec!["/"], None)
         .apply(
            ApiKeyMiddleware {
                api_key: "athene".to_string(),
            },
            vec!["/api/v1/user"],
            vec!["/upload"],
        )
    });
    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>
"#;