Co-locate related routes

This commit is contained in:
Ben Grant 2023-05-15 16:57:13 +10:00
parent aa3b323cb6
commit bf5bcf9992
9 changed files with 157 additions and 197 deletions

View file

@ -11,12 +11,12 @@ use utoipa::{
#[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,
routes::stats::get_stats,
routes::event::create_event,
routes::event::get_event,
routes::person::get_people,
routes::person::get_person,
routes::person::update_person,
),
components(schemas(
payloads::StatsResponse,

View file

@ -69,12 +69,18 @@ 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))
.route("/event/:event_id", get(get_event))
.route("/event/:event_id/people", get(get_people))
.route("/event/:event_id/people/:person_name", get(get_person))
.route("/event/:event_id/people/:person_name", patch(update_person))
.route("/stats", get(stats::get_stats))
.route("/event", post(event::create_event))
.route("/event/:event_id", get(event::get_event))
.route("/event/:event_id/people", get(person::get_people))
.route(
"/event/:event_id/people/:person_name",
get(person::get_person),
)
.route(
"/event/:event_id/people/:person_name",
patch(person::update_person),
)
.with_state(shared_state)
.layer(cors)
.layer(rate_limit)

View file

@ -1,14 +1,49 @@
use axum::{extract, http::StatusCode, Json};
use axum::{
extract::{self, Path},
http::StatusCode,
Json,
};
use common::{adaptor::Adaptor, event::Event};
use rand::{seq::SliceRandom, thread_rng, Rng};
use regex::Regex;
use crate::{
errors::ApiError,
payloads::{EventInput, EventResponse},
payloads::{ApiResult, EventInput, EventResponse},
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>,
) -> ApiResult<EventResponse, A> {
let adaptor = &state.lock().await.adaptor;
let event = adaptor
.get_event(event_id)
.await
.map_err(ApiError::AdaptorError)?;
match event {
Some(event) => Ok(Json(event.into())),
None => Err(ApiError::NotFound),
}
}
#[utoipa::path(
post,
path = "/event",

View file

@ -1,42 +0,0 @@
use axum::{
extract::{self, Path},
Json,
};
use common::adaptor::Adaptor;
use crate::{
errors::ApiError,
payloads::{ApiResult, EventResponse},
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>,
) -> ApiResult<EventResponse, A> {
let adaptor = &state.lock().await.adaptor;
let event = adaptor
.get_event(event_id)
.await
.map_err(ApiError::AdaptorError)?;
match event {
Some(event) => Ok(Json(event.into())),
None => Err(ApiError::NotFound),
}
}

View file

@ -1,42 +0,0 @@
use axum::{
extract::{self, Path},
Json,
};
use common::adaptor::Adaptor;
use crate::{
errors::ApiError,
payloads::{ApiResult, PersonResponse},
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>,
) -> ApiResult<Vec<PersonResponse>, A> {
let adaptor = &state.lock().await.adaptor;
let people = adaptor
.get_people(event_id)
.await
.map_err(ApiError::AdaptorError)?;
match people {
Some(people) => Ok(Json(people.into_iter().map(|p| p.into()).collect())),
None => Err(ApiError::NotFound),
}
}

View file

@ -1,17 +1,3 @@
pub mod get_event;
pub use get_event::get_event;
pub mod get_stats;
pub use get_stats::get_stats;
pub mod create_event;
pub use create_event::create_event;
pub mod get_people;
pub use get_people::get_people;
pub mod get_person;
pub use get_person::get_person;
pub mod update_person;
pub use update_person::update_person;
pub mod event;
pub mod person;
pub mod stats;

View file

@ -8,10 +8,41 @@ use common::{adaptor::Adaptor, person::Person};
use crate::{
errors::ApiError,
payloads::{ApiResult, PersonResponse},
payloads::{ApiResult, PersonInput, PersonResponse},
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>,
) -> ApiResult<Vec<PersonResponse>, A> {
let adaptor = &state.lock().await.adaptor;
let people = adaptor
.get_people(event_id)
.await
.map_err(ApiError::AdaptorError)?;
match people {
Some(people) => Ok(Json(people.into_iter().map(|p| p.into()).collect())),
None => Err(ApiError::NotFound),
}
}
#[utoipa::path(
get,
path = "/event/{event_id}/people/{person_name}",
@ -95,6 +126,73 @@ pub async fn get_person<A: Adaptor>(
}
}
#[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"),
),
security((), ("password" = [])),
request_body(content = PersonInput, 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)>,
bearer: Option<TypedHeader<Authorization<Bearer>>>,
Json(input): Json<PersonInput>,
) -> ApiResult<PersonResponse, A> {
let adaptor = &state.lock().await.adaptor;
let existing_people = adaptor
.get_people(event_id.clone())
.await
.map_err(ApiError::AdaptorError)?;
// Event not found
if existing_people.is_none() {
return Err(ApiError::NotFound);
}
// Check if the user exists
let existing_person = existing_people
.unwrap()
.into_iter()
.find(|p| p.name == person_name)
.ok_or(ApiError::NotFound)?;
// Verify password (if set)
if !verify_password(&existing_person, parse_password(bearer)) {
return Err(ApiError::NotAuthorized);
}
Ok(Json(
adaptor
.upsert_person(
event_id,
Person {
name: existing_person.name,
password_hash: existing_person.password_hash,
created_at: existing_person.created_at,
availability: input.availability,
},
)
.await
.map_err(ApiError::AdaptorError)?
.into(),
))
}
pub fn parse_password(bearer: Option<TypedHeader<Authorization<Bearer>>>) -> Option<String> {
bearer.map(|TypedHeader(Authorization(b))| {
String::from_utf8(

View file

@ -1,81 +0,0 @@
use axum::{
extract::{self, Path},
headers::{authorization::Bearer, Authorization},
Json, TypedHeader,
};
use common::{adaptor::Adaptor, person::Person};
use crate::{
errors::ApiError,
payloads::{ApiResult, PersonInput, PersonResponse},
State,
};
use super::get_person::{parse_password, 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"),
),
security((), ("password" = [])),
request_body(content = PersonInput, 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)>,
bearer: Option<TypedHeader<Authorization<Bearer>>>,
Json(input): Json<PersonInput>,
) -> ApiResult<PersonResponse, A> {
let adaptor = &state.lock().await.adaptor;
let existing_people = adaptor
.get_people(event_id.clone())
.await
.map_err(ApiError::AdaptorError)?;
// Event not found
if existing_people.is_none() {
return Err(ApiError::NotFound);
}
// Check if the user exists
let existing_person = existing_people
.unwrap()
.into_iter()
.find(|p| p.name == person_name)
.ok_or(ApiError::NotFound)?;
// Verify password (if set)
if !verify_password(&existing_person, parse_password(bearer)) {
return Err(ApiError::NotAuthorized);
}
Ok(Json(
adaptor
.upsert_person(
event_id,
Person {
name: existing_person.name,
password_hash: existing_person.password_hash,
created_at: existing_person.created_at,
availability: input.availability,
},
)
.await
.map_err(ApiError::AdaptorError)?
.into(),
))
}