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    pub fn unauthorized() -> Self {
56        Self::new("Unauthorized", "UNAUTHORIZED")
57    }
58
59    /// Create a not found error.
60    pub fn not_found(msg: impl fmt::Display) -> Self {
61        Self::new(format!("Not found: {}", msg), "NOT_FOUND")
62    }
63}
64
65impl fmt::Display for ApiError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(f, "{}: {}", self.code, self.error)
68    }
69}
70
71impl IntoResponse for ApiError {
72    fn into_response(self) -> Response {
73        let status = match self.code.as_str() {
74            "UNAUTHORIZED" => StatusCode::UNAUTHORIZED,
75            "FORBIDDEN" => StatusCode::FORBIDDEN,
76            "NOT_FOUND" => StatusCode::NOT_FOUND,
77            "VALIDATION_ERROR" | "PARSE_ERROR" => StatusCode::BAD_REQUEST,
78            "SERVICE_UNAVAILABLE" => StatusCode::SERVICE_UNAVAILABLE,
79            _ => StatusCode::INTERNAL_SERVER_ERROR,
80        };
81
82        (status, Json(self)).into_response()
83    }
84}
85
86/// Standard API success response wrapper.
87#[derive(Debug, Serialize, Deserialize)]
88pub struct ApiResponse<T> {
89    /// Always `"success"` for successful responses.
90    pub status: String,
91    /// The response payload.
92    pub data:   T,
93}
94
95impl<T: Serialize> ApiResponse<T> {
96    /// Create a successful response.
97    pub fn success(data: T) -> Json<Self> {
98        Json(Self {
99            status: "success".to_string(),
100            data,
101        })
102    }
103}
104
105/// Sanitized server configuration for API exposure.
106///
107/// Removes sensitive fields like database URLs, API keys, and tokens
108/// while preserving operational settings for client consumption.
109#[derive(Debug, Serialize, Deserialize, Clone)]
110pub struct SanitizedConfig {
111    /// Server port
112    pub port: u16,
113
114    /// Server host address
115    pub host: String,
116
117    /// Number of worker threads
118    pub workers: Option<usize>,
119
120    /// Whether TLS is enabled
121    pub tls_enabled: bool,
122
123    /// Indicates configuration has been sanitized
124    pub sanitized: bool,
125}
126
127impl SanitizedConfig {
128    /// Create sanitized configuration from `ServerConfig`.
129    ///
130    /// Removes sensitive fields:
131    /// - TLS private keys and certificates (replaced with boolean flag)
132    /// - Database connection strings (not included)
133    /// - API keys and tokens (not included)
134    pub fn from_config(config: &crate::config::HttpServerConfig) -> Self {
135        Self {
136            port:        config.port,
137            host:        config.host.clone(),
138            workers:     config.workers,
139            tls_enabled: config.tls.is_some(),
140            sanitized:   true,
141        }
142    }
143
144    /// Verify configuration has been properly sanitized.
145    pub const fn is_sanitized(&self) -> bool {
146        self.sanitized
147    }
148}