feat: document api contract

This commit is contained in:
2026-06-13 11:29:23 +02:00
parent b910e77460
commit cf11e20f90
12 changed files with 417 additions and 47 deletions

View File

@@ -1,4 +1,8 @@
use axum::{Json, http::StatusCode, response::IntoResponse};
use axum::{
Json,
http::{HeaderMap, StatusCode},
response::IntoResponse,
};
use serde::Serialize;
#[derive(Debug, thiserror::Error)]
@@ -29,20 +33,20 @@ impl AppError {
Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let status = self.status_code();
let body = ErrorBody {
error: self.public_message(),
};
(status, Json(body)).into_response()
#[must_use]
pub const fn code(&self) -> &'static str {
match self {
Self::BadRequest(_) => "bad_request",
Self::NotFound => "pokemon_not_found",
Self::InvalidUpstreamData(_) => "upstream_invalid_data",
Self::Upstream(_) | Self::Timeout => "upstream_unavailable",
Self::Internal => "internal_error",
}
}
}
impl AppError {
fn public_message(&self) -> String {
#[must_use]
pub fn public_message(&self) -> String {
match self {
Self::BadRequest(message) => message.clone(),
Self::NotFound => "pokemon not found".to_owned(),
@@ -53,7 +57,64 @@ impl AppError {
}
}
#[derive(Serialize)]
struct ErrorBody {
error: String,
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
ApiError::new(self, None).into_response()
}
}
pub struct ApiError {
source: AppError,
request_id: Option<String>,
}
impl ApiError {
#[must_use]
pub const fn new(source: AppError, request_id: Option<String>) -> Self {
Self { source, request_id }
}
#[must_use]
pub fn from_headers(source: AppError, headers: &HeaderMap) -> Self {
Self::new(source, request_id_from_headers(headers))
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let status = self.source.status_code();
let body = ErrorBody::new(
self.source.code(),
self.source.public_message(),
self.request_id,
);
(status, Json(body)).into_response()
}
}
#[derive(Serialize)]
pub struct ErrorBody {
code: &'static str,
message: String,
#[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
request_id: Option<String>,
}
impl ErrorBody {
#[must_use]
pub fn new(code: &'static str, message: impl Into<String>, request_id: Option<String>) -> Self {
Self {
code,
message: message.into(),
request_id,
}
}
}
#[must_use]
pub fn request_id_from_headers(headers: &HeaderMap) -> Option<String> {
headers
.get("x-request-id")
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
}