//! HTTP handlers, one file per area (SPEC §14.7), and the extractors and response types they share. //! //! M2a implements `health` (E-1) and `setup` (E-2); every other handler is registered and annotated but returns 501 //! `not_implemented` until M3 (E-3 to E-30, E-33) and M4 (E-31, E-32) fill in the bodies. pub mod accounts; pub mod apk; pub mod categories; pub mod devices; pub mod health; pub mod invites; pub mod landing; pub mod me; pub mod pictures; pub mod proposal; pub mod rate_limit; pub mod recurring; pub mod rows; pub mod setup; pub mod sync; pub mod validate; use axum::extract::rejection::{JsonRejection, PathRejection}; use axum::extract::{FromRequest, FromRequestParts, Request}; use axum::http::request::Parts; use axum::http::{HeaderValue, StatusCode, header}; use axum::response::{IntoResponse, Response}; use serde::Serialize; use serde::de::DeserializeOwned; use uuid::Uuid; use crate::error::AppError; /// The content type of every JSON body (SPEC §14.3.1). pub const JSON_CONTENT_TYPE: &str = "application/json; charset=utf-8"; /// A JSON response body with `Content-Type: application/json; charset=utf-8`. #[derive(Debug, Clone)] pub struct JsonBody(pub T); impl IntoResponse for JsonBody { fn into_response(self) -> Response { match serde_json::to_vec(&self.0) { Ok(bytes) => { let mut response = bytes.into_response(); response.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE), ); response } Err(e) => { tracing::error!(detail = %e, "response serialisation failed"); (StatusCode::INTERNAL_SERVER_ERROR, "internal").into_response() } } } } /// A JSON request body whose rejections map to the error codes of SPEC §14.3.2: malformed JSON, missing fields and /// wrong types are 400 `invalid_request`; a body over the limit is 413 `payload_too_large`. #[derive(Debug, Clone)] pub struct ApiJson(pub T); impl FromRequest for ApiJson where T: DeserializeOwned, S: Send + Sync, { type Rejection = AppError; async fn from_request(request: Request, state: &S) -> Result { match axum::Json::::from_request(request, state).await { Ok(axum::Json(value)) => Ok(Self(value)), Err(rejection) => Err(json_rejection(rejection)), } } } fn json_rejection(rejection: JsonRejection) -> AppError { if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE { return AppError::new( crate::error::ErrorCode::PayloadTooLarge, "request body too large", ); } match rejection { JsonRejection::MissingJsonContentType(_) => { AppError::invalid_request("Content-Type must be application/json") } other => AppError::invalid_request(other.body_text()), } } /// One UUID path segment. Anything that is not a UUID is 404 `not_found` (SPEC §14.3.1). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IdPath(pub Uuid); impl FromRequestParts for IdPath { type Rejection = AppError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let axum::extract::Path(text): axum::extract::Path = axum::extract::Path::from_request_parts(parts, state) .await .map_err(|_: PathRejection| AppError::not_found())?; Uuid::parse_str(&text) .ok() .filter(|id| id.hyphenated().to_string() == text) .map(Self) .ok_or_else(AppError::not_found) } } /// The body of every handler that is registered but not yet implemented (501 `not_implemented`). pub fn not_implemented() -> AppError { AppError::not_implemented() }