Skip to content

Commit

Permalink
Add tide application example (juhaku#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
juhaku authored Apr 8, 2022
1 parent a83d6a3 commit 5d220b0
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ Cargo.lock
*.iml
.idea
.vscode
target

5 changes: 1 addition & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 3 additions & 1 deletion examples/todo-actix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["actix-web"] }

[workspace]
24 changes: 24 additions & 0 deletions examples/todo-tide/Cargo.toml
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]
16 changes: 16 additions & 0 deletions examples/todo-tide/README.md
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
```
238 changes: 238 additions & 0 deletions examples/todo-tide/src/main.rs
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())
})
}
}
4 changes: 3 additions & 1 deletion examples/todo-warp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ env_logger = "0.9.0"
log = "0.4"
futures = "0.3"
utoipa = { path = "../.." }
utoipa-swagger-ui = { path = "../../utoipa-swagger-ui" }
utoipa-swagger-ui = { path = "../../utoipa-swagger-ui" }

[workspace]

0 comments on commit 5d220b0

Please sign in to comment.