Skip to main content

fraiseql_server/routes/api/
types.rs

1//! Shared types for API responses and errors.
2
3use std::fmt;
4
5use axum::{
6    Json,
7    http::StatusCode,
8    response::{IntoResponse, Response},
9};
10use serde::{Deserialize, Serialize};
11
12/// Standard API error response.
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct ApiError {
15    /// Human-readable error message.
16    pub error:   String,
17    /// Machine-readable error code (e.g. `"NOT_FOUND"`, `"VALIDATION_ERROR"`).
18    pub code:    String,
19    /// Optional additional context about the error.
20    pub details: Option<String>,
21}
22
23impl ApiError {
24    /// Create a new API error with error message and code.
25    pub fn new(error: impl Into<String>, code: impl Into<String>) -> Self {
26        Self {
27            error:   error.into(),
28            code:    code.into(),
29            details: None,
30        }
31    }
32
33    /// Add details to the error.
34    pub fn with_details(mut self, details: impl Into<String>) -> Self {
35        self.details = Some(details.into());
36        self
37    }
38
39    /// Create a parse error.
40    pub fn parse_error(msg: impl fmt::Display) -> Self {
41        Self::new(format!("Parse error: {}", msg), "PARSE_ERROR")
42    }
43
44    /// Create a validation error.
45    pub fn validation_error(msg: impl fmt::Display) -> Self {
46        Self::new(format!("Validation error: {}", msg), "VALIDATION_ERROR")
47    }
48
49    /// Create an internal server error.
50    pub fn internal_error(msg: impl fmt::Display) -> Self {
51        Self::new(format!("Internal server error: {}", msg), "INTERNAL_ERROR")
52    }
53
54    /// Create an unauthorized error.
55    #[must_use]
56    pub fn unauthorized() -> Self {
57        Self::new("Unauthorized", "UNAUTHORIZED")
58    }
59
60    /// Create a not found error.
61    pub fn not_found(msg: impl fmt::Display) -> Self {
62        Self::new(format!("Not found: {}", msg), "NOT_FOUND")
63    }
64}
65
66impl fmt::Display for ApiError {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "{}: {}", self.code, self.error)
69    }
70}
71
72impl IntoResponse for ApiError {
73    fn into_response(self) -> Response {
74        let status = match self.code.as_str() {
75            "UNAUTHORIZED" => StatusCode::UNAUTHORIZED,
76            "FORBIDDEN" => StatusCode::FORBIDDEN,
77            "NOT_FOUND" => StatusCode::NOT_FOUND,
78            "VALIDATION_ERROR" | "PARSE_ERROR" => StatusCode::BAD_REQUEST,
79            "UNSUPPORTED_OPERATION" => StatusCode::NOT_IMPLEMENTED,
80            "SERVICE_UNAVAILABLE" => StatusCode::SERVICE_UNAVAILABLE,
81            _ => StatusCode::INTERNAL_SERVER_ERROR,
82        };
83
84        (status, Json(self)).into_response()
85    }
86}
87
88/// Standard API success response wrapper.
89#[derive(Debug, Serialize, Deserialize)]
90pub struct ApiResponse<T> {
91    /// Always `"success"` for successful responses.
92    pub status: String,
93    /// The response payload.
94    pub data:   T,
95}
96
97impl<T: Serialize> ApiResponse<T> {
98    /// Create a successful response.
99    pub fn success(data: T) -> Json<Self> {
100        Json(Self {
101            status: "success".to_string(),
102            data,
103        })
104    }
105}
106
107/// Sanitized server configuration for API exposure.
108///
109/// Removes sensitive fields like database URLs, API keys, and tokens
110/// while preserving operational settings for client consumption.
111#[derive(Debug, Serialize, Deserialize, Clone)]
112pub struct SanitizedConfig {
113    /// Server port
114    pub port: u16,
115
116    /// Server host address
117    pub host: String,
118
119    /// Number of worker threads
120    pub workers: Option<usize>,
121
122    /// Whether TLS is enabled
123    pub tls_enabled: bool,
124
125    /// Indicates configuration has been sanitized
126    pub sanitized: bool,
127}
128
129impl SanitizedConfig {
130    /// Create sanitized configuration from `ServerConfig`.
131    ///
132    /// Removes sensitive fields:
133    /// - TLS private keys and certificates (replaced with boolean flag)
134    /// - Database connection strings (not included)
135    /// - API keys and tokens (not included)
136    #[must_use]
137    pub fn from_config(config: &crate::config::HttpServerConfig) -> Self {
138        Self {
139            port:        config.port,
140            host:        config.host.clone(),
141            workers:     config.workers,
142            tls_enabled: config.tls.is_some(),
143            sanitized:   true,
144        }
145    }
146
147    /// Verify configuration has been properly sanitized.
148    #[must_use]
149    pub const fn is_sanitized(&self) -> bool {
150        self.sanitized
151    }
152}