Files
kvitt/server/crates/kvitt-server/src/api/mod.rs
T

119 lines
3.9 KiB
Rust

//! 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<T>(pub T);
impl<T: Serialize> IntoResponse for JsonBody<T> {
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<T>(pub T);
impl<T, S> FromRequest<S> for ApiJson<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = AppError;
async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
match axum::Json::<T>::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<S: Send + Sync> FromRequestParts<S> for IdPath {
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let axum::extract::Path(text): axum::extract::Path<String> =
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()
}