1#![allow(clippy::needless_for_each)]
8
9use crate::build_info::{BuildInfo, BuildInfoProvider};
10use axum::{extract::Extension, http::StatusCode, response::IntoResponse, Json};
11use serde::Serialize;
12use utoipa::{OpenApi, ToSchema};
13
14#[derive(Debug, Serialize, ToSchema)]
16#[serde(rename_all = "camelCase")]
17pub struct ProblemDetails {
18 #[serde(rename = "type")]
20 pub problem_type: String,
21 pub title: String,
23 pub status: u16,
25 pub detail: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub instance: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub extensions: Option<ProblemExtensions>,
33}
34
35#[derive(Debug, Serialize, ToSchema)]
37pub struct ProblemExtensions {
38 pub code: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub field: Option<String>,
43}
44
45impl ProblemDetails {
46 #[must_use]
48 pub fn internal_error(detail: &str) -> Self {
49 Self {
50 problem_type: "https://tinycongress.com/errors/internal".to_string(),
51 title: "Internal Server Error".to_string(),
52 status: 500,
53 detail: detail.to_string(),
54 instance: None,
55 extensions: Some(ProblemExtensions {
56 code: "INTERNAL_ERROR".to_string(),
57 field: None,
58 }),
59 }
60 }
61}
62
63impl IntoResponse for ProblemDetails {
64 fn into_response(self) -> axum::response::Response {
65 let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
66 (status, Json(self)).into_response()
67 }
68}
69
70#[derive(OpenApi)]
72#[openapi(
73 info(
74 title = "TinyCongress API",
75 version = "1.0.0",
76 description = "REST API for TinyCongress",
77 license(name = "MIT")
78 ),
79 servers(
80 (url = "/api/v1", description = "REST API v1")
81 ),
82 paths(get_build_info),
83 components(schemas(BuildInfo, ProblemDetails, ProblemExtensions))
84)]
85pub struct ApiDoc;
86
87#[utoipa::path(
95 get,
96 path = "/build-info",
97 tag = "System",
98 responses(
99 (status = 200, description = "Build information retrieved successfully", body = BuildInfo),
100 (status = 500, description = "Internal server error", body = ProblemDetails)
101 )
102)]
103#[allow(clippy::unused_async)] pub async fn get_build_info(
105 Extension(provider): Extension<BuildInfoProvider>,
106) -> Result<Json<BuildInfo>, ProblemDetails> {
107 Ok(Json(provider.build_info()))
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn problem_details_serializes_correctly() {
116 let problem = ProblemDetails::internal_error("Something went wrong");
117 let json = serde_json::to_string(&problem).expect("serialize");
118 assert!(json.contains("\"type\":"));
119 assert!(json.contains("INTERNAL_ERROR"));
120 }
121}