forked from juhaku/utoipa
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tide application example (juhaku#71)
- Loading branch information
Showing
7 changed files
with
287 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,5 @@ Cargo.lock | |
*.iml | ||
.idea | ||
.vscode | ||
target | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <[email protected]>" | ||
] | ||
|
||
# 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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Arc<Config<'_>>>) -> tide::Result<Response> { | ||
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::<Mime>()?) | ||
.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<Mutex<Vec<Todo>>>; | ||
|
||
/// 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<Store>) -> tide::Result<Response> { | ||
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<Store>) -> tide::Result<Response> { | ||
let new_todo = req.body_json::<Todo>().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<Store>) -> tide::Result<Response> { | ||
let id = req.param("id")?.parse::<i32>()?; | ||
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<Store>) -> tide::Result<Response> { | ||
let id = req.param("id")?.parse::<i32>()?; | ||
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()) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters