Skip to content

Commit

Permalink
Add axum example
Browse files Browse the repository at this point in the history
  • Loading branch information
juhaku committed Apr 29, 2022
1 parent 51c8825 commit e22a5c0
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ being portable and standalone one of it's key aspects is simple integration with
Existing [examples](./examples) for following frameworks:

* **[actix-web](https://github.com/actix/actix-web)**
* **[axum](https://github.com/tokio-rs/axum)**
* **[warp](https://github.com/seanmonstar/warp)**
* **[tide](https://github.com/http-rs/tide)**
* **[rocket](https://github.com/SergioBenitez/Rocket)**

Even if there is no example for your favourite framework `utoipa` can be used with any
web framework which supports decorating functions with macros similarly to **warp** and **tide** examples.
web framework which supports decorating functions with macros similarly to **axum**, **warp** and **tide** examples.

## What's up with the word play?

Expand Down
25 changes: 25 additions & 0 deletions examples/todo-axum/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "todo-axum"
description = "Simple axum todo example api with utoipa and Swagger UI"
version = "0.1.0"
edition = "2021"
license = "MIT"
authors = [
"Elli Example <example@example.com>"
]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = "0.5"
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1.17", features = ["full"] }
tower = "0.4"
utoipa = { path = "../.." }
utoipa-swagger-ui = { path = "../../utoipa-swagger-ui" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
env_logger = "0.9.0"
log = "0.4"

[workspace]
11 changes: 11 additions & 0 deletions examples/todo-axum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# todo-axum ~ utoipa with utoipa-swagger-ui example

This is demo `axum` 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
```
288 changes: 288 additions & 0 deletions examples/todo-axum/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
use std::{
net::{Ipv4Addr, SocketAddr},
sync::Arc,
};

use axum::{extract::Path, response::IntoResponse, routing, Extension, Json, Router, Server};
use hyper::{Error, StatusCode};
use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
Modify, OpenApi,
};
use utoipa_swagger_ui::Config;

use crate::todo::{Store, Todo, TodoError};

#[tokio::main]
async fn main() -> Result<(), Error> {
#[derive(OpenApi)]
#[openapi(
handlers(
todo::list_todos,
todo::create_todo,
todo::mark_done,
todo::delete_todo,
),
components(Todo, TodoError),
modifiers(&SecurityAddon),
tags(
(name = "todo", description = "Todo items management API")
)
)]
struct ApiDoc;

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("todo_apikey"))),
)
}
}
}

let api_doc = ApiDoc::openapi();
let config = Arc::new(Config::from("/api-doc/openapi.json"));

let store = Arc::new(Store::default());
let app = Router::new()
.route(
"/api-doc/openapi.json",
routing::get({
let doc = api_doc.clone();
move || async { Json(doc) }
}),
)
.route(
"/swagger-ui/*tail",
routing::get(serve_swagger_ui).layer(Extension(config)),
)
.route(
"/todo",
routing::get(todo::list_todos)
.post(todo::create_todo)
.put(todo::mark_done),
)
.route(
"/todo/:id",
routing::put(todo::mark_done).delete(todo::delete_todo),
)
.layer(Extension(store));

let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080));
Server::bind(&address).serve(app.into_make_service()).await
}

async fn serve_swagger_ui(
Path(tail): Path<String>,
Extension(state): Extension<Arc<Config<'static>>>,
) -> impl IntoResponse {
match utoipa_swagger_ui::serve(&tail[1..], state) {
Ok(file) => file
.map(|file| {
(
StatusCode::OK,
[("Content-Type", file.content_type)],
file.bytes,
)
.into_response()
})
.unwrap_or_else(|| StatusCode::NOT_FOUND.into_response()),
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}

mod todo {
use std::sync::Arc;

use axum::{extract::Path, response::IntoResponse, Extension, Json};
use hyper::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use utoipa::Component;

/// In-memonry todo store
pub(super) type Store = Mutex<Vec<Todo>>;

/// Item to do.
#[derive(Serialize, Deserialize, Component, Clone)]
pub(super) struct Todo {
id: i32,
#[component(example = "Buy groceries")]
value: String,
done: bool,
}

/// Todo operation errors
#[derive(Serialize, Deserialize, Component)]
pub(super) enum TodoError {
/// Todo already exists conflict.
#[component(example = "Todo already exists")]
Conflict(String),
/// Todo not found by id.
#[component(example = "id = 1")]
NotFound(String),
/// Todo operation unauthorized
#[component(example = "missing api key")]
Unauthorized(String),
}

/// List all Todo items
///
/// List all Todo items from in-memory storage.
#[utoipa::path(
get,
path = "/todo",
responses(
(status = 200, description = "List all todos successfully", body = [Todo])
)
)]
pub(super) async fn list_todos(Extension(store): Extension<Arc<Store>>) -> Json<Vec<Todo>> {
let todos = store.lock().await.clone();

Json(todos)
}

/// Create new Todo
///
/// Tries to create a new Todo item to in-memory storage or fails with 409 conflict if already exists.
#[utoipa::path(
post,
path = "/todo",
request_body = Todo,
responses(
(status = 201, description = "Todo item created successfully", body = Todo),
(status = 409, description = "Todo already exists", body = TodoError)
)
)]
pub(super) async fn create_todo(
Json(todo): Json<Todo>,
Extension(store): Extension<Arc<Store>>,
) -> impl IntoResponse {
let mut todos = store.lock().await;

todos
.iter_mut()
.find(|existing_todo| existing_todo.id == todo.id)
.map(|found| {
(
StatusCode::CONFLICT,
Json(TodoError::Conflict(format!(
"todo already exists: {}",
found.id
))),
)
.into_response()
})
.unwrap_or_else(|| {
todos.push(todo.clone());

(StatusCode::CREATED, Json(todo)).into_response()
})
}

/// Mark Todo item done by id
///
/// Mark Todo item done by given id. Return only status 200 on success or 404 if Todo is not found.
#[utoipa::path(
put,
path = "/todo/{id}",
responses(
(status = 200, description = "Todo marked done successfully"),
(status = 404, description = "Todo not found")
),
params(
("id" = i32, path, description = "Todo database id")
),
security(
(), // <-- make optional authentication
("api_key" = [])
)
)]
pub(super) async fn mark_done(
Path(id): Path<i32>,
Extension(store): Extension<Arc<Store>>,
headers: HeaderMap,
) -> StatusCode {
match check_api_key(false, headers) {
Ok(_) => (),
Err(_) => return StatusCode::UNAUTHORIZED,
}

let mut todos = store.lock().await;

todos
.iter_mut()
.find(|todo| todo.id == id)
.map(|todo| {
todo.done = true;
StatusCode::OK
})
.unwrap_or(StatusCode::NOT_FOUND)
}

/// Delete Todo item by id
///
/// Delete Todo item from in-memory storage by id. Returns either 200 success of 404 with TodoError if Todo is not found.
#[utoipa::path(
delete,
path = "/todo/{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" = i32, path, description = "Todo database id")
),
security(
("api_key" = [])
)
)]
pub(super) async fn delete_todo(
Path(id): Path<i32>,
Extension(store): Extension<Arc<Store>>,
headers: HeaderMap,
) -> impl IntoResponse {
match check_api_key(true, headers) {
Ok(_) => (),
Err(error) => return error.into_response(),
}

let mut todos = store.lock().await;

let len = todos.len();

todos.retain(|todo| todo.id != id);

if todos.len() != len {
StatusCode::OK.into_response()
} else {
(
StatusCode::NOT_FOUND,
Json(TodoError::NotFound(format!("id = {id}"))),
)
.into_response()
}
}

// normally you should create a midleware for this but this is sufficient for sake of example.
fn check_api_key(require_api_key: bool, headers: HeaderMap) -> Result<(), impl IntoResponse> {
match headers.get("todo_apikey") {
Some(header) if header != "utoipa-rocks" => Err((
StatusCode::UNAUTHORIZED,
Json(TodoError::Unauthorized(String::from("incorrect api key"))),
)
.into_response()),
None if require_api_key => Err((
StatusCode::UNAUTHORIZED,
Json(TodoError::Unauthorized(String::from("missing api key"))),
)
.into_response()),
_ => Ok(()),
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
//! Existing [examples](https://github.com/juhaku/utoipa/tree/master/examples) for following frameworks:
//!
//! * **actix-web**
//! * **axum**
//! * **warp**
//! * **tide**
//! * **rocket**
Expand Down

0 comments on commit e22a5c0

Please sign in to comment.