ruvector_scipix/api/
responses.rs

1use axum::{
2    http::StatusCode,
3    response::{IntoResponse, Response},
4    Json,
5};
6use serde::{Deserialize, Serialize};
7
8use super::jobs::JobStatus;
9
10/// Standard text/OCR response
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct TextResponse {
13    /// Unique request identifier
14    pub request_id: String,
15
16    /// Recognized text
17    pub text: String,
18
19    /// Confidence score (0.0 - 1.0)
20    pub confidence: f64,
21
22    /// LaTeX output (if requested)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub latex: Option<String>,
25
26    /// MathML output (if requested)
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub mathml: Option<String>,
29
30    /// HTML output (if requested)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub html: Option<String>,
33}
34
35/// PDF processing response
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct PdfResponse {
38    /// PDF job identifier
39    pub pdf_id: String,
40
41    /// Current job status
42    pub status: JobStatus,
43
44    /// Status message
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub message: Option<String>,
47
48    /// Processing result (when completed)
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub result: Option<String>,
51
52    /// Error details (if failed)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub error: Option<String>,
55}
56
57/// Error response
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ErrorResponse {
60    /// Error code
61    pub error_code: String,
62
63    /// Human-readable error message
64    pub message: String,
65
66    /// HTTP status code
67    #[serde(skip)]
68    pub status: StatusCode,
69}
70
71impl ErrorResponse {
72    /// Create a validation error response
73    pub fn validation_error(message: impl Into<String>) -> Self {
74        Self {
75            error_code: "VALIDATION_ERROR".to_string(),
76            message: message.into(),
77            status: StatusCode::BAD_REQUEST,
78        }
79    }
80
81    /// Create an unauthorized error response
82    pub fn unauthorized(message: impl Into<String>) -> Self {
83        Self {
84            error_code: "UNAUTHORIZED".to_string(),
85            message: message.into(),
86            status: StatusCode::UNAUTHORIZED,
87        }
88    }
89
90    /// Create a not found error response
91    pub fn not_found(message: impl Into<String>) -> Self {
92        Self {
93            error_code: "NOT_FOUND".to_string(),
94            message: message.into(),
95            status: StatusCode::NOT_FOUND,
96        }
97    }
98
99    /// Create a rate limit error response
100    pub fn rate_limited(message: impl Into<String>) -> Self {
101        Self {
102            error_code: "RATE_LIMIT_EXCEEDED".to_string(),
103            message: message.into(),
104            status: StatusCode::TOO_MANY_REQUESTS,
105        }
106    }
107
108    /// Create an internal error response
109    pub fn internal_error(message: impl Into<String>) -> Self {
110        Self {
111            error_code: "INTERNAL_ERROR".to_string(),
112            message: message.into(),
113            status: StatusCode::INTERNAL_SERVER_ERROR,
114        }
115    }
116
117    /// Create a service unavailable error response
118    /// Used when the service is not fully configured (e.g., missing models)
119    pub fn service_unavailable(message: impl Into<String>) -> Self {
120        Self {
121            error_code: "SERVICE_UNAVAILABLE".to_string(),
122            message: message.into(),
123            status: StatusCode::SERVICE_UNAVAILABLE,
124        }
125    }
126
127    /// Create a not implemented error response
128    pub fn not_implemented(message: impl Into<String>) -> Self {
129        Self {
130            error_code: "NOT_IMPLEMENTED".to_string(),
131            message: message.into(),
132            status: StatusCode::NOT_IMPLEMENTED,
133        }
134    }
135}
136
137impl IntoResponse for ErrorResponse {
138    fn into_response(self) -> Response {
139        let status = self.status;
140        (status, Json(self)).into_response()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_text_response_serialization() {
150        let response = TextResponse {
151            request_id: "test-123".to_string(),
152            text: "Hello World".to_string(),
153            confidence: 0.95,
154            latex: Some("x^2".to_string()),
155            mathml: None,
156            html: None,
157        };
158
159        let json = serde_json::to_string(&response).unwrap();
160        assert!(json.contains("request_id"));
161        assert!(json.contains("test-123"));
162        assert!(!json.contains("mathml"));
163    }
164
165    #[test]
166    fn test_error_response_creation() {
167        let error = ErrorResponse::validation_error("Invalid input");
168        assert_eq!(error.status, StatusCode::BAD_REQUEST);
169        assert_eq!(error.error_code, "VALIDATION_ERROR");
170
171        let error = ErrorResponse::unauthorized("Invalid credentials");
172        assert_eq!(error.status, StatusCode::UNAUTHORIZED);
173
174        let error = ErrorResponse::rate_limited("Too many requests");
175        assert_eq!(error.status, StatusCode::TOO_MANY_REQUESTS);
176    }
177}