LCOV - code coverage report
Current view: top level - service/src - rest.rs (source / functions) Coverage Total Hit
Test: Rust Backend Coverage Lines: 85.7 % 28 24
Test Date: 2025-12-20 21:58:40 Functions: 44.4 % 9 4

            Line data    Source code
       1              : //! REST API handlers and `OpenAPI` documentation.
       2              : //!
       3              : //! This module provides REST endpoints alongside GraphQL, sharing the same
       4              : //! domain types with `ToSchema` derives for `OpenAPI` spec generation.
       5              : 
       6              : // The OpenApi derive macro generates code that triggers this lint
       7              : #![allow(clippy::needless_for_each)]
       8              : 
       9              : use crate::build_info::{BuildInfo, BuildInfoProvider};
      10              : use axum::{extract::Extension, http::StatusCode, response::IntoResponse, Json};
      11              : use serde::Serialize;
      12              : use utoipa::{OpenApi, ToSchema};
      13              : 
      14              : /// RFC 7807 Problem Details error response.
      15              : #[derive(Debug, Serialize, ToSchema)]
      16              : #[serde(rename_all = "camelCase")]
      17              : pub struct ProblemDetails {
      18              :     /// URI reference identifying the problem type
      19              :     #[serde(rename = "type")]
      20              :     pub problem_type: String,
      21              :     /// Short human-readable summary
      22              :     pub title: String,
      23              :     /// HTTP status code
      24              :     pub status: u16,
      25              :     /// Human-readable explanation specific to this occurrence
      26              :     pub detail: String,
      27              :     /// URI reference identifying the specific occurrence
      28              :     #[serde(skip_serializing_if = "Option::is_none")]
      29              :     pub instance: Option<String>,
      30              :     /// Additional error details
      31              :     #[serde(skip_serializing_if = "Option::is_none")]
      32              :     pub extensions: Option<ProblemExtensions>,
      33              : }
      34              : 
      35              : /// Extended error information mapping to GraphQL error codes.
      36              : #[derive(Debug, Serialize, ToSchema)]
      37              : pub struct ProblemExtensions {
      38              :     /// Error code matching GraphQL error codes
      39              :     pub code: String,
      40              :     /// Field that caused the error (for validation errors)
      41              :     #[serde(skip_serializing_if = "Option::is_none")]
      42              :     pub field: Option<String>,
      43              : }
      44              : 
      45              : impl ProblemDetails {
      46              :     /// Create an internal server error response.
      47              :     #[must_use]
      48            1 :     pub fn internal_error(detail: &str) -> Self {
      49            1 :         Self {
      50            1 :             problem_type: "https://tinycongress.com/errors/internal".to_string(),
      51            1 :             title: "Internal Server Error".to_string(),
      52            1 :             status: 500,
      53            1 :             detail: detail.to_string(),
      54            1 :             instance: None,
      55            1 :             extensions: Some(ProblemExtensions {
      56            1 :                 code: "INTERNAL_ERROR".to_string(),
      57            1 :                 field: None,
      58            1 :             }),
      59            1 :         }
      60            1 :     }
      61              : }
      62              : 
      63              : impl IntoResponse for ProblemDetails {
      64            0 :     fn into_response(self) -> axum::response::Response {
      65            0 :         let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
      66            0 :         (status, Json(self)).into_response()
      67            0 :     }
      68              : }
      69              : 
      70              : /// `OpenAPI` documentation for the REST API.
      71              : #[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              : )]
      85              : pub struct ApiDoc;
      86              : 
      87              : /// Get build information
      88              : ///
      89              : /// Returns metadata about the running service including version, git SHA, and build time.
      90              : ///
      91              : /// # Errors
      92              : ///
      93              : /// Returns `ProblemDetails` on internal server errors.
      94              : #[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)] // Required for Axum handler signature
     104            3 : pub async fn get_build_info(
     105            3 :     Extension(provider): Extension<BuildInfoProvider>,
     106            3 : ) -> Result<Json<BuildInfo>, ProblemDetails> {
     107            3 :     Ok(Json(provider.build_info()))
     108            3 : }
     109              : 
     110              : #[cfg(test)]
     111              : mod tests {
     112              :     use super::*;
     113              : 
     114              :     #[test]
     115            1 :     fn problem_details_serializes_correctly() {
     116            1 :         let problem = ProblemDetails::internal_error("Something went wrong");
     117            1 :         let json = serde_json::to_string(&problem).expect("serialize");
     118            1 :         assert!(json.contains("\"type\":"));
     119            1 :         assert!(json.contains("INTERNAL_ERROR"));
     120            1 :     }
     121              : }
        

Generated by: LCOV version 2.0-1