athene 2.0.4

A simple and lightweight rust web framework based on hyper
Documentation
use athene::prelude::*;
use utoipa::ToSchema;
use utoipa::{
    openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
    Modify, OpenApi,
};
use utoipa_swagger_ui::Config;
use std::sync::Arc;

const API_KEY_NAME: &str = "todo_apikey";
const API_KEY: &str = "utoipa-rocks";

#[derive(Serialize, Deserialize, ToSchema, Clone)]
struct Todo {
    pub id: u64,
    #[schema(example = "Buy groceries")]
    pub text: String,
    pub completed: bool,
}

#[derive(Serialize, Deserialize, ToSchema)]
enum TodoError {
    /// Todo already exists conflict.
    #[schema(example = "Todo already exists")]
    Conflict(String),
    /// Todo not found by id.
    #[schema(example = "id = 1")]
    NotFound(String),
    /// Todo operation unauthorized
    #[schema(example = "missing api key")]
    Unauthorized(String),
}

// The query parameters for list todos.
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct Pagination {
    pub offset: Option<usize>,
    pub limit: Option<usize>,
}

// GET /todos?offset=0&limit=10
#[utoipa::path(
    get,
    path = "/todos",
    responses(
        (status = 200, description = "List all todos successfully", body = [Todo])
    )
)]
async fn list(_req: Request) -> impl Responder {
 
}

// POST /todos
#[utoipa::path(
    post,
    path = "/todos",
    request_body = Todo,
    responses(
        (status = 201, description = "Todo item created successfully", body = Todo),
        (status = 409, description = "Todo already exists", body = TodoError)
    )
)]
async fn create(_req: Request) -> impl Responder {}

// GET /todos/:id
#[utoipa::path(
    post,
    path = "/todos/{id}",
    responses(
        (status = 200, description = "Todo item found successfully", body = Todo),
        (status = 404, description = "Todo not found")
    ),
    params(
        ("id" = u64, Path, description = "Todo database id")
    ),
    security(
        (), // <-- make optional authentication
        ("api_key" = [])
    )
)]
async fn show(_req: Request) -> impl Responder {}

// PUT /todos/:id
#[utoipa::path(
    put,
    path = "/todos/{id}",
    responses(
        (status = 200, description = "Todo marked done successfully"),
        (status = 404, description = "Todo not found")
    ),
    params(
        ("id" = u64, Path, description = "Todo database id")
    ),
    security(
        (), // <-- make optional authentication
        ("api_key" = [])
    )
)]
async fn update(_req: Request) -> impl Responder {}

// DELETE /todos/:id
#[utoipa::path(
    delete,
    path = "/todos/{id}",
    responses(
        (status = 200, description = "Todo marked done successfully"),
        (status = 401, description = "Unauthorized to delete Todo", body = TodoError, example = json!(TodoError::Unauthorized(String::from("missing api key")))),
        (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1"))))
    ),
    params(
        ("id" = u64, Path, description = "Todo database id")
    ),
    security(
        ("api_key" = [])
    )
)]
async fn delete(req: Request) -> impl Responder {
    let res = check_api_key(true, req);
    /*
        ...
     */
    res
}

// normally you should create a middleware for this but this is sufficient for sake of example.
fn check_api_key(require_api_key: bool, req: Request) -> impl Responder {
    let res = Builder::new();
    match req.headers().get(API_KEY_NAME) {
        Some(header) if header != API_KEY => {
            res.status(StatusCode::UNAUTHORIZED).json(&TodoError::Unauthorized(String::from("incorrect api key")))
        }

        None if require_api_key => {
            res.status(StatusCode::UNAUTHORIZED).json(&TodoError::Unauthorized(String::from("missing api key")))
        }
        _ => res
    }
}

struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        if let Some(components) = openapi.components.as_mut() {
            components.add_security_scheme(
                "api_key",
                SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new(API_KEY_NAME))),
            );
        }
    }
}

#[derive(OpenApi)]
#[openapi(
    paths(
        list,
        create,
        update,
        delete,
    ),
    components(
        schemas(Todo, TodoError)
    ),
    modifiers(&SecurityAddon),
    tags(
        (name = "todo", description = "Todo items management API")
    )
)]
struct ApiDoc;

pub async fn openapi_json(_req: Request) -> impl Responder {
    let apidoc = Arc::new(ApiDoc::openapi());
    let res = Builder::new();
    res.json(&*apidoc)
}

pub async fn serve_swagger(req: Request) -> impl Responder {
    let config = Arc::new(Config::from("/api-doc/openapi.json"));
    let path = req.uri().path().to_string();
    let tail = path.strip_prefix("/swagger-ui/").unwrap();
    let res = Builder::new();

    match utoipa_swagger_ui::serve(tail, config) {
        Ok(swagger_file) => swagger_file
            .map(|file| {
                res.with(&file.content_type, file.bytes.to_vec())
            })
            .unwrap(),
        Err(error) => res.status(500).text(error.to_string()),
    }
}

pub fn openapi_router(r: Router) -> Router {

    let r = r.get(
            "/api-doc/openapi.json",
            openapi_json,
        ).get("/swagger-ui/**", serve_swagger);


    let r = r.get("/todos", list)
        .post("/todos", create)
        .get("/todos/:id", show)
        .put("/todos/:id", update)
        .delete("/todos/:id", delete);
    r
}

// Enter the website address in the browser: http://127.0.0.1:7878/swagger-ui/
#[tokio::main]
pub async fn main() -> Result<()> {
    let app = athene::new().router(openapi_router);
    app.listen("127.0.0.1:7878").await
}