diff --git a/.gitignore b/.gitignore index 7b84ad74..85eb6e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ Cargo.lock *.iml .idea .vscode +target + diff --git a/Cargo.toml b/Cargo.toml index fcf96361..4d7eb3a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,10 +37,7 @@ paste = "1" [workspace] members = [ "utoipa-gen", - "utoipa-swagger-ui", - - "examples/todo-actix", - "examples/todo-warp" + "utoipa-swagger-ui" ] [package.metadata.docs.rs] diff --git a/examples/todo-actix/Cargo.toml b/examples/todo-actix/Cargo.toml index 6a7fb637..d7b627de 100644 --- a/examples/todo-actix/Cargo.toml +++ b/examples/todo-actix/Cargo.toml @@ -18,4 +18,6 @@ env_logger = "0.9.0" log = "0.4" futures = "0.3" utoipa = { path = "../..", features = ["actix_extras"] } -utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["actix-web"] } \ No newline at end of file +utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["actix-web"] } + +[workspace] \ No newline at end of file diff --git a/examples/todo-tide/Cargo.toml b/examples/todo-tide/Cargo.toml new file mode 100644 index 00000000..c16cc4c6 --- /dev/null +++ b/examples/todo-tide/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "todo-tide" +description = "Simple tide todo example api with utoipa and Swagger UI" +version = "0.1.0" +edition = "2021" +license = "MIT" +authors = [ + "Elli Example " +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tide = "0.16.0" +async-std = { version = "1.8.0", features = ["attributes"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +env_logger = "0.9.0" +log = "0.4" +futures = "0.3" +utoipa = { path = "../.." } +utoipa-swagger-ui = { path = "../../utoipa-swagger-ui" } + +[workspace] \ No newline at end of file diff --git a/examples/todo-tide/README.md b/examples/todo-tide/README.md new file mode 100644 index 00000000..4533a284 --- /dev/null +++ b/examples/todo-tide/README.md @@ -0,0 +1,16 @@ +# todo-tide ~ utoipa with utoipa-swagger-ui example + +This is demo `tide` application with in-memory storage to manage Todo items. The API +demostrates `utoipa` with `utoipa-swagger-ui` functionalities. + +For security restricted endpoints the super secret api key is: `utoipa-rocks`. + +Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/index.html`. +```bash +cargo run +``` + +If you want to see some logging you may prepend the command with `RUST_LOG=debug` as shown below. +```bash +RUST_LOG=debug cargo run +``` diff --git a/examples/todo-tide/src/main.rs b/examples/todo-tide/src/main.rs new file mode 100644 index 00000000..257c7859 --- /dev/null +++ b/examples/todo-tide/src/main.rs @@ -0,0 +1,238 @@ +use std::sync::Arc; + +use serde_json::json; +use tide::{http::Mime, Response}; +use utoipa::{ + openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, + Modify, OpenApi, +}; +use utoipa_swagger_ui::Config; + +use crate::todo::{Store, Todo, TodoError}; + +#[async_std::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let config = Arc::new(Config::from("/api-doc/openapi.json")); + let mut app = tide::with_state(config); + + #[derive(OpenApi)] + #[openapi( + handlers( + todo::list_todos, + todo::create_todo, + todo::delete_todo, + todo::mark_done + ), + components(Todo, TodoError), + modifiers(&SecurityAddon), + tags( + (name = "todo", description = "Todo items management endpoints.") + ) + )] + struct ApiDoc; + + 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("todo_apikey"))), + ) + } + } + + // serve OpenApi json + app.at("/api-doc/openapi.json") + .get(|_| async move { Ok(Response::builder(200).body(json!(ApiDoc::openapi()))) }); + + // serve Swagger UI + app.at("/swagger-ui/*").get(serve_swagger); + + app.at("/api").nest({ + let mut todos = tide::with_state(Store::default()); + + todos.at("/todo").get(todo::list_todos); + todos.at("/todo").post(todo::create_todo); + todos.at("/todo/:id").delete(todo::delete_todo); + todos.at("/todo/:id").put(todo::mark_done); + + todos + }); + + app.listen("0.0.0.0:8080").await +} + +async fn serve_swagger(request: tide::Request>>) -> tide::Result { + let config = request.state().clone(); + let path = request.url().path().to_string(); + let tail = path.strip_prefix("/swagger-ui/").unwrap(); + + match utoipa_swagger_ui::serve(tail, config) { + Ok(swagger_file) => swagger_file + .map(|file| { + Ok(Response::builder(200) + .body(file.bytes.to_vec()) + .content_type(file.content_type.parse::()?) + .build()) + }) + .unwrap_or_else(|| Ok(Response::builder(404).build())), + Err(error) => Ok(Response::builder(500).body(error.to_string()).build()), + } +} + +mod todo { + use std::sync::{Arc, Mutex}; + + use serde::{Deserialize, Serialize}; + use serde_json::json; + use tide::{Request, Response}; + use utoipa::Component; + + /// Item to complete + #[derive(Serialize, Deserialize, Component, Clone)] + pub(super) struct Todo { + /// Unique database id for `Todo` + #[component(example = 1)] + id: i32, + /// Description of task to complete + #[component(example = "Buy coffee")] + value: String, + /// Indicates whether task is done or not + done: bool, + } + + /// Error that might occur when managing `Todo` items + #[derive(Serialize, Deserialize, Component)] + pub(super) enum TodoError { + /// Happens when Todo item alredy exists + Config(String), + /// Todo not found from storage + NotFound(String), + } + + pub(super) type Store = Arc>>; + + /// List todos from in-memory stoarge. + /// + /// List all todos from in memory storage. + #[utoipa::path( + get, + path = "/api/todo", + responses( + (status = 200, description = "List all todos successfully") + ) + )] + pub(super) async fn list_todos(req: Request) -> tide::Result { + let todos = req.state().lock().unwrap().clone(); + + Ok(Response::builder(200).body(json!(todos)).build()) + } + + /// Create new todo + /// + /// Create new todo to in-memory storage if not exists. + #[utoipa::path( + post, + path = "/api/todo", + request_body = Todo, + responses( + (status = 201, description = "Todo created successfully"), + (status = 409, description = "Todo already exists", body = TodoError, example = json!(TodoError::Config(String::from("id = 1")))) + ) + )] + pub(super) async fn create_todo(mut req: Request) -> tide::Result { + let new_todo = req.body_json::().await?; + let mut todos = req.state().lock().unwrap(); + + todos + .iter() + .find(|existing| existing.id == new_todo.id) + .map(|existing| { + Ok(Response::builder(409) + .body(json!(TodoError::Config(format!("id = {}", existing.id)))) + .build()) + }) + .unwrap_or_else(|| { + todos.push(new_todo.clone()); + + Ok(Response::builder(200).body(json!(new_todo)).build()) + }) + } + + /// Delete todo by id. + /// + /// Delete todo from in-memory storage. + #[utoipa::path( + delete, + path = "/api/todo/{id}", + responses( + (status = 200, description = "Todo deleted successfully"), + (status = 401, description = "Unauthorized to delete Todo"), + (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) + ), + params( + ("id" = i32, path, description = "Id of todo item to delete") + ), + security( + ("api_key" = []) + ) + )] + pub(super) async fn delete_todo(req: Request) -> tide::Result { + let id = req.param("id")?.parse::()?; + let api_key = req + .header("todo_apikey") + .map(|header| header.as_str().to_string()) + .unwrap_or_default(); + + if api_key != "utoipa-rocks" { + return Ok(Response::new(401)); + } + + let mut todos = req.state().lock().unwrap(); + + let old_size = todos.len(); + + todos.retain(|todo| todo.id != id); + + if old_size == todos.len() { + Ok(Response::builder(404) + .body(json!(TodoError::NotFound(format!("id = {id}")))) + .build()) + } else { + Ok(Response::new(200)) + } + } + + /// Mark todo done by id + #[utoipa::path( + put, + path = "/api/todo/{id}", + responses( + (status = 200, description = "Todo marked done successfully"), + (status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) + ), + params( + ("id" = i32, path, description = "Id of todo item to mark done") + ) + )] + pub(super) async fn mark_done(req: Request) -> tide::Result { + let id = req.param("id")?.parse::()?; + let mut todos = req.state().lock().unwrap(); + + todos + .iter_mut() + .find(|todo| todo.id == id) + .map(|todo| { + todo.done = true; + Ok(Response::new(200)) + }) + .unwrap_or_else(|| { + Ok(Response::builder(404) + .body(json!(TodoError::NotFound(format!("id = {id}")))) + .build()) + }) + } +} diff --git a/examples/todo-warp/Cargo.toml b/examples/todo-warp/Cargo.toml index 88a2e5f2..a93008a9 100644 --- a/examples/todo-warp/Cargo.toml +++ b/examples/todo-warp/Cargo.toml @@ -19,4 +19,6 @@ env_logger = "0.9.0" log = "0.4" futures = "0.3" utoipa = { path = "../.." } -utoipa-swagger-ui = { path = "../../utoipa-swagger-ui" } \ No newline at end of file +utoipa-swagger-ui = { path = "../../utoipa-swagger-ui" } + +[workspace] \ No newline at end of file