Add API docs with utoipa

This commit is contained in:
Ben Grant 2023-05-14 00:48:23 +10:00
parent 300285e84b
commit f46f456db0
11 changed files with 311 additions and 16 deletions

171
backend/Cargo.lock generated
View file

@ -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",
]

View file

@ -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"] }

View file

@ -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))

View file

@ -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<T, A> = Result<Json<T>, ApiError<A>>;
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct EventInput {
pub name: Option<String>,
pub times: Vec<String>,
pub timezone: String,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct EventResponse {
pub id: String,
pub name: String,
@ -34,7 +35,7 @@ impl From<Event> for EventResponse {
}
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct StatsResponse {
pub event_count: i32,
pub person_count: i32,
@ -51,7 +52,7 @@ impl From<Stats> for StatsResponse {
}
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct PersonResponse {
pub name: String,
pub availability: Vec<String>,
@ -68,12 +69,12 @@ impl From<Person> for PersonResponse {
}
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct GetPersonInput {
pub password: Option<String>,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct UpdatePersonInput {
pub password: Option<String>,
pub availability: Vec<String>,

View file

@ -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<A: Adaptor>(
extract::State(state): State<A>,
Json(input): Json<EventInput>,
) -> ApiResult<EventResponse, A> {
) -> Result<(StatusCode, Json<EventResponse>), ApiError<A>> {
let adaptor = &state.lock().await.adaptor;
// Get the current timestamp
@ -55,7 +68,7 @@ pub async fn create_event<A: Adaptor>(
.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

View file

@ -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<A: Adaptor>(
extract::State(state): State<A>,
Path(event_id): Path<String>,

View file

@ -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<A: Adaptor>(
extract::State(state): State<A>,
Path(event_id): Path<String>,

View file

@ -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<A: Adaptor>(
extract::State(state): State<A>,
Path((event_id, person_name)): Path<(String, String)>,

View file

@ -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<A: Adaptor>(extract::State(state): State<A>) -> ApiResult<StatsResponse, A> {
let adaptor = &state.lock().await.adaptor;

View file

@ -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;

View file

@ -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<A: Adaptor>(
extract::State(state): State<A>,
Path((event_id, person_name)): Path<(String, String)>,