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