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 rocket example with utoipa and utoipa-swagger-ui
- Loading branch information
Showing
3 changed files
with
336 additions
and
0 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 |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "rocket-todo" | ||
description = "Simple rocket 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] | ||
rocket = { version = "0.5.0-rc.1", features = ["json"] } | ||
utoipa = { path = "../..", features = ["rocket_extras"] } | ||
utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["rocket"] } | ||
serde = { version = "1.0", features = ["derive"] } | ||
serde_json = "1.0" | ||
env_logger = "0.9.0" | ||
log = "0.4" | ||
|
||
[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-rocket ~ utoipa with utoipa-swagger-ui example | ||
|
||
This is demo `rocket` 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/`. | ||
```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,298 @@ | ||
use rocket::{catch, catchers, routes, Build, Request, Rocket}; | ||
use serde_json::json; | ||
use todo::RequireApiKey; | ||
use utoipa::{ | ||
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, | ||
Modify, OpenApi, | ||
}; | ||
use utoipa_swagger_ui::SwaggerUi; | ||
|
||
use crate::todo::{Todo, TodoError, TodoStore}; | ||
|
||
#[rocket::launch] | ||
fn rocket() -> Rocket<Build> { | ||
env_logger::init(); | ||
|
||
#[derive(OpenApi)] | ||
#[openapi( | ||
handlers( | ||
todo::get_tasks, | ||
todo::create_todo, | ||
todo::mark_done, | ||
todo::delete_todo, | ||
todo::search_todos | ||
), | ||
components(Todo, TodoError), | ||
tags( | ||
(name = "todo", description = "Todo management endpoints.") | ||
), | ||
modifiers(&SecurityAddon) | ||
)] | ||
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"))), | ||
) | ||
} | ||
} | ||
|
||
rocket::build() | ||
.manage(TodoStore::default()) | ||
.register("/todo", catchers![unauthorized]) | ||
.mount( | ||
"/", | ||
SwaggerUi::new("/swagger-ui/<_..>").url("/api-doc/openapi.json", ApiDoc::openapi()), | ||
) | ||
.mount( | ||
"/todo", | ||
routes![ | ||
todo::get_tasks, | ||
todo::create_todo, | ||
todo::mark_done, | ||
todo::delete_todo, | ||
todo::search_todos | ||
], | ||
) | ||
} | ||
|
||
#[catch(401)] | ||
async fn unauthorized(req: &Request<'_>) -> serde_json::Value { | ||
let (_, todo_error) = req.guard::<RequireApiKey>().await.failed().unwrap(); | ||
|
||
json!(todo_error) | ||
} | ||
|
||
mod todo { | ||
use std::sync::{Arc, Mutex}; | ||
|
||
use rocket::{ | ||
delete, get, | ||
http::Status, | ||
outcome::Outcome, | ||
post, put, | ||
request::{self, FromRequest}, | ||
response::{status::Custom, Responder}, | ||
serde::json::Json, | ||
Request, State, | ||
}; | ||
use serde::{Deserialize, Serialize}; | ||
use utoipa::Component; | ||
|
||
pub(super) type TodoStore = Arc<Mutex<Vec<Todo>>>; | ||
|
||
/// Todo operation error. | ||
#[derive(Serialize, Component, Responder, Debug)] | ||
pub(super) enum TodoError { | ||
/// When there is conflict creating a new todo. | ||
#[response(status = 409)] | ||
Conflict(String), | ||
|
||
/// When todo item is not found from storage. | ||
#[response(status = 404)] | ||
NotFound(String), | ||
|
||
/// When unauthorized to complete operation | ||
#[response(status = 401)] | ||
Unauthorized(String), | ||
} | ||
|
||
pub(super) struct RequireApiKey; | ||
|
||
#[rocket::async_trait] | ||
impl<'r> FromRequest<'r> for RequireApiKey { | ||
type Error = TodoError; | ||
|
||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { | ||
match request.headers().get("todo_apikey").next() { | ||
Some(key) if key == "utoipa-rocks" => Outcome::Success(RequireApiKey), | ||
None => Outcome::Failure(( | ||
Status::Unauthorized, | ||
TodoError::Unauthorized(String::from("missing api key")), | ||
)), | ||
_ => Outcome::Failure(( | ||
Status::Unauthorized, | ||
TodoError::Unauthorized(String::from("invalid api key")), | ||
)), | ||
} | ||
} | ||
} | ||
|
||
pub(super) struct LogApiKey; | ||
|
||
#[rocket::async_trait] | ||
impl<'r> FromRequest<'r> for LogApiKey { | ||
type Error = TodoError; | ||
|
||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { | ||
match request.headers().get("todo_apikey").next() { | ||
Some(key) if key == "utoipa-rocks" => { | ||
log::info!("authenticated"); | ||
Outcome::Success(LogApiKey) | ||
} | ||
_ => { | ||
log::info!("no api key"); | ||
Outcome::Forward(()) | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Task to do. | ||
#[derive(Serialize, Deserialize, Component, Clone)] | ||
pub(super) struct Todo { | ||
/// Unique todo id. | ||
#[component(example = 1)] | ||
id: i32, | ||
/// Description of a taks. | ||
#[component(example = "Buy groceries")] | ||
value: String, | ||
/// Indicatation whether task is done or not. | ||
done: bool, | ||
} | ||
|
||
/// List all available todo items. | ||
#[utoipa::path( | ||
context_path = "/todo", | ||
responses( | ||
(status = 200, description = "Get all todos", body = [Todo]) | ||
) | ||
)] | ||
#[get("/")] | ||
pub(super) async fn get_tasks(store: &State<TodoStore>) -> Json<Vec<Todo>> { | ||
Json(store.lock().unwrap().clone()) | ||
} | ||
|
||
/// Create new todo item. | ||
/// | ||
/// Create new todo item and add it to the storage. | ||
#[utoipa::path( | ||
context_path = "/todo", | ||
request_body = Todo, | ||
responses( | ||
(status = 201, description = "Todo item created successfully", body = Todo), | ||
(status = 409, description = "Todo already exists", body = TodoError, example = json!(TodoError::Conflict(String::from("id = 1")))) | ||
) | ||
)] | ||
#[post("/", data = "<todo>")] | ||
pub(super) async fn create_todo( | ||
todo: Json<Todo>, | ||
store: &State<TodoStore>, | ||
) -> Result<Custom<Json<Todo>>, TodoError> { | ||
let mut todos = store.lock().unwrap(); | ||
todos | ||
.iter() | ||
.find(|existing| existing.id == todo.id) | ||
.map(|todo| Err(TodoError::Conflict(format!("id = {}", todo.id)))) | ||
.unwrap_or_else(|| { | ||
todos.push(todo.0.clone()); | ||
|
||
Ok(Custom(Status::Created, Json(todo.0))) | ||
}) | ||
} | ||
|
||
/// Mark Todo item done by given id | ||
/// | ||
/// Tries to find todo item by given id and mark it done if found. Will return not found in case todo | ||
/// item does not exists. | ||
#[utoipa::path( | ||
context_path = "/todo", | ||
responses( | ||
(status = 200, description = "Todo item marked done successfully"), | ||
(status = 404, description = "Todo item not found from storage", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) | ||
), | ||
params( | ||
("id", description = "Todo item unique id") | ||
), | ||
security( | ||
(), | ||
("api_key" = []) | ||
) | ||
)] | ||
#[put("/<id>")] | ||
pub(super) async fn mark_done( | ||
id: i32, | ||
_api_key: LogApiKey, | ||
store: &State<TodoStore>, | ||
) -> Result<Status, TodoError> { | ||
store | ||
.lock() | ||
.unwrap() | ||
.iter_mut() | ||
.find(|todo| todo.id == id) | ||
.map(|todo| { | ||
todo.done = true; | ||
|
||
Ok(Status::Ok) | ||
}) | ||
.unwrap_or_else(|| Err(TodoError::NotFound(format!("id = {id}")))) | ||
} | ||
|
||
/// Delete Todo by given id. | ||
/// | ||
/// Delete Todo from storage by Todo id if found. | ||
#[utoipa::path( | ||
context_path = "/todo", | ||
responses( | ||
(status = 200, description = "Todo deleted successfully"), | ||
(status = 401, description = "Unauthorized to delete Todos", body = TodoError, example = json!(TodoError::Unauthorized(String::from("id = 1")))), | ||
(status = 404, description = "Todo not found", body = TodoError, example = json!(TodoError::NotFound(String::from("id = 1")))) | ||
), | ||
params( | ||
("id", description = "Todo item id") | ||
), | ||
security( | ||
("api_key" = []) | ||
) | ||
)] | ||
#[delete("/<id>")] | ||
pub(super) async fn delete_todo( | ||
id: i32, | ||
_api_key: RequireApiKey, | ||
store: &State<TodoStore>, | ||
) -> Result<Status, TodoError> { | ||
let mut todos = store.lock().unwrap(); | ||
let len = todos.len(); | ||
todos.retain(|todo| todo.id != id); | ||
|
||
if len == todos.len() { | ||
Err(TodoError::NotFound(format!("id = {id}"))) | ||
} else { | ||
Ok(Status::Ok) | ||
} | ||
} | ||
|
||
/// Search Todo items by their value. | ||
/// | ||
/// Search is performed in case sensitive manner from value of Todo. | ||
#[utoipa::path( | ||
context_path = "/todo", | ||
responses( | ||
(status = 200, description = "Found Todo items", body = [Todo]) | ||
) | ||
)] | ||
#[get("/search?<value>")] | ||
pub(super) async fn search_todos( | ||
value: Option<&str>, | ||
store: &State<TodoStore>, | ||
) -> Json<Vec<Todo>> { | ||
Json( | ||
store | ||
.lock() | ||
.unwrap() | ||
.iter() | ||
.filter(|todo| { | ||
value | ||
.map(|value| todo.value.to_lowercase().contains(&value.to_lowercase())) | ||
.unwrap_or(true) | ||
}) | ||
.cloned() | ||
.collect(), | ||
) | ||
} | ||
} |