From f46f456db0c14e793ed0c26e31c077e4dab4e9ac Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Sun, 14 May 2023 00:48:23 +1000 Subject: [PATCH] Add API docs with utoipa --- backend/Cargo.lock | 171 ++++++++++++++++++++++++++++ backend/Cargo.toml | 2 + backend/src/main.rs | 32 ++++++ backend/src/payloads.rs | 13 ++- backend/src/routes/create_event.rs | 21 +++- backend/src/routes/get_event.rs | 14 +++ backend/src/routes/get_people.rs | 14 +++ backend/src/routes/get_person.rs | 19 ++++ backend/src/routes/get_stats.rs | 10 ++ backend/src/routes/mod.rs | 12 +- backend/src/routes/update_person.rs | 19 ++++ 11 files changed, 311 insertions(+), 16 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index ad21c05..4e5bcab 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -8,6 +8,12 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.7.6" @@ -609,6 +615,17 @@ dependencies = [ "tower_governor", "tracing", "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", ] [[package]] @@ -813,6 +830,16 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.10.14" @@ -1226,6 +1253,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1402,12 +1430,31 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.6" @@ -2034,6 +2081,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b68543d5527e158213414a92832d2aab11a84d2571a5eb021ebe22c43aab066" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4e0f0ced47ded9a68374ac145edd65a6c1fa13a96447b873660b2a568a0fd7" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 1.0.109", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust_decimal" version = "1.29.1" @@ -2078,6 +2160,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.21" @@ -2374,6 +2465,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2923,6 +3023,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2973,6 +3082,46 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utoipa" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062bba5a3568e126ac72049a63254f4cb1da2eb713db0c1ab2a4c76be191db8c" +dependencies = [ + "axum", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.3.2" @@ -3016,6 +3165,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -3316,3 +3475,15 @@ name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zip" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e92305c174683d78035cbf1b70e18db6329cc0f1b9cae0a52ca90bf5bfe7125" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 8f0dfd3..63c90b9 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -26,3 +26,5 @@ bcrypt = "0.14.0" tower-http = { version = "0.4.0", features = ["cors", "trace"] } tower_governor = "0.0.4" tower = "0.4.13" +utoipa = { version = "3.3.0", features = ["axum_extras", "preserve_order"] } +utoipa-swagger-ui = { version = "3.1.3", features = ["axum"] } diff --git a/backend/src/main.rs b/backend/src/main.rs index a875d68..8d7b5f1 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -13,6 +13,8 @@ use tokio::sync::Mutex; use tower::ServiceBuilder; use tower_governor::{errors::display_error, governor::GovernorConfigBuilder, GovernorLayer}; use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; mod errors; mod payloads; @@ -31,6 +33,35 @@ async fn main() { // Load env dotenv::dotenv().ok(); + #[derive(OpenApi)] + #[openapi( + info(title = "Crab Fit API"), + paths( + routes::get_stats::get_stats, + routes::create_event::create_event, + routes::get_event::get_event, + routes::get_people::get_people, + routes::get_person::get_person, + routes::update_person::update_person, + ), + components( + schemas( + payloads::StatsResponse, + payloads::EventResponse, + payloads::PersonResponse, + payloads::EventInput, + payloads::GetPersonInput, + payloads::UpdatePersonInput, + ), + ), + tags( + (name = "info"), + (name = "event"), + (name = "person"), + ), + )] + struct ApiDoc; + let shared_state = Arc::new(Mutex::new(ApiState { adaptor: SqlAdaptor::new().await, })); @@ -62,6 +93,7 @@ async fn main() { }); let app = Router::new() + .merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi())) .route("/", get(get_root)) .route("/stats", get(get_stats)) .route("/event", post(create_event)) diff --git a/backend/src/payloads.rs b/backend/src/payloads.rs index 2bfe430..60779e6 100644 --- a/backend/src/payloads.rs +++ b/backend/src/payloads.rs @@ -1,19 +1,20 @@ use axum::Json; use common::{event::Event, person::Person, stats::Stats}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use crate::errors::ApiError; pub type ApiResult = Result, ApiError>; -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct EventInput { pub name: Option, pub times: Vec, pub timezone: String, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct EventResponse { pub id: String, pub name: String, @@ -34,7 +35,7 @@ impl From for EventResponse { } } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct StatsResponse { pub event_count: i32, pub person_count: i32, @@ -51,7 +52,7 @@ impl From for StatsResponse { } } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct PersonResponse { pub name: String, pub availability: Vec, @@ -68,12 +69,12 @@ impl From for PersonResponse { } } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct GetPersonInput { pub password: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct UpdatePersonInput { pub password: Option, pub availability: Vec, diff --git a/backend/src/routes/create_event.rs b/backend/src/routes/create_event.rs index 9e0d6f8..0de6a4b 100644 --- a/backend/src/routes/create_event.rs +++ b/backend/src/routes/create_event.rs @@ -1,18 +1,31 @@ -use axum::{extract, Json}; +use axum::{extract, http::StatusCode, Json}; use common::{adaptor::Adaptor, event::Event}; use rand::{seq::SliceRandom, thread_rng, Rng}; use regex::Regex; use crate::{ errors::ApiError, - payloads::{ApiResult, EventInput, EventResponse}, + payloads::{EventInput, EventResponse}, State, }; +#[utoipa::path( + post, + path = "/event", + request_body(content = EventInput, description = "New event details"), + responses( + (status = 201, description = "Created", body = EventResponse), + (status = 415, description = "Unsupported input format"), + (status = 422, description = "Invalid input provided"), + (status = 429, description = "Too many requests"), + ), + tag = "event", +)] +/// Create a new event pub async fn create_event( extract::State(state): State, Json(input): Json, -) -> ApiResult { +) -> Result<(StatusCode, Json), ApiError> { let adaptor = &state.lock().await.adaptor; // Get the current timestamp @@ -55,7 +68,7 @@ pub async fn create_event( .await .map_err(ApiError::AdaptorError)?; - Ok(Json(event.into())) + Ok((StatusCode::CREATED, Json(event.into()))) } // Generate a random name based on an adjective and a crab species diff --git a/backend/src/routes/get_event.rs b/backend/src/routes/get_event.rs index 5ce0ca1..36759fa 100644 --- a/backend/src/routes/get_event.rs +++ b/backend/src/routes/get_event.rs @@ -10,6 +10,20 @@ use crate::{ State, }; +#[utoipa::path( + get, + path = "/event/{event_id}", + params( + ("event_id", description = "The ID of the event"), + ), + responses( + (status = 200, description = "Ok", body = EventResponse), + (status = 404, description = "Not found"), + (status = 429, description = "Too many requests"), + ), + tag = "event", +)] +/// Get details about an event pub async fn get_event( extract::State(state): State, Path(event_id): Path, diff --git a/backend/src/routes/get_people.rs b/backend/src/routes/get_people.rs index e8fb9c2..cee8f6b 100644 --- a/backend/src/routes/get_people.rs +++ b/backend/src/routes/get_people.rs @@ -10,6 +10,20 @@ use crate::{ State, }; +#[utoipa::path( + get, + path = "/event/{event_id}/people", + params( + ("event_id", description = "The ID of the event"), + ), + responses( + (status = 200, description = "Ok", body = [PersonResponse]), + (status = 404, description = "Event not found"), + (status = 429, description = "Too many requests"), + ), + tag = "person", +)] +/// Get availabilities for an event pub async fn get_people( extract::State(state): State, Path(event_id): Path, diff --git a/backend/src/routes/get_person.rs b/backend/src/routes/get_person.rs index 0e00df3..90c104f 100644 --- a/backend/src/routes/get_person.rs +++ b/backend/src/routes/get_person.rs @@ -10,6 +10,25 @@ use crate::{ State, }; +#[utoipa::path( + get, + path = "/event/{event_id}/people/{person_name}", + params( + ("event_id", description = "The ID of the event"), + ("person_name", description = "The name of the person"), + ), + request_body(content = GetPersonInput, description = "Person details"), + responses( + (status = 200, description = "Ok", body = PersonResponse), + (status = 401, description = "Incorrect password"), + (status = 404, description = "Event not found"), + (status = 415, description = "Unsupported input format"), + (status = 422, description = "Invalid input provided"), + (status = 429, description = "Too many requests"), + ), + tag = "person", +)] +/// Login or create a person for an event pub async fn get_person( extract::State(state): State, Path((event_id, person_name)): Path<(String, String)>, diff --git a/backend/src/routes/get_stats.rs b/backend/src/routes/get_stats.rs index 46d9efc..46398ce 100644 --- a/backend/src/routes/get_stats.rs +++ b/backend/src/routes/get_stats.rs @@ -7,6 +7,16 @@ use crate::{ State, }; +#[utoipa::path( + get, + path = "/stats", + responses( + (status = 200, description = "Ok", body = StatsResponse), + (status = 429, description = "Too many requests"), + ), + tag = "info", +)] +/// Get current stats pub async fn get_stats(extract::State(state): State) -> ApiResult { let adaptor = &state.lock().await.adaptor; diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 4881408..e9e6bed 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -1,17 +1,17 @@ -mod get_event; +pub mod get_event; pub use get_event::get_event; -mod get_stats; +pub mod get_stats; pub use get_stats::get_stats; -mod create_event; +pub mod create_event; pub use create_event::create_event; -mod get_people; +pub mod get_people; pub use get_people::get_people; -mod get_person; +pub mod get_person; pub use get_person::get_person; -mod update_person; +pub mod update_person; pub use update_person::update_person; diff --git a/backend/src/routes/update_person.rs b/backend/src/routes/update_person.rs index 99b3804..521f71b 100644 --- a/backend/src/routes/update_person.rs +++ b/backend/src/routes/update_person.rs @@ -12,6 +12,25 @@ use crate::{ use super::get_person::verify_password; +#[utoipa::path( + patch, + path = "/event/{event_id}/people/{person_name}", + params( + ("event_id", description = "The ID of the event"), + ("person_name", description = "The name of the person"), + ), + request_body(content = UpdatePersonInput, description = "Person details"), + responses( + (status = 200, description = "Ok", body = PersonResponse), + (status = 401, description = "Incorrect password"), + (status = 404, description = "Event or person not found"), + (status = 415, description = "Unsupported input format"), + (status = 422, description = "Invalid input provided"), + (status = 429, description = "Too many requests"), + ), + tag = "person", +)] +/// Update a person's availabilities pub async fn update_person( extract::State(state): State, Path((event_id, person_name)): Path<(String, String)>,