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, pub limit: Option, } // 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 }