ruvector_scipix/output/
json.rs

1//! JSON API response formatter matching Scipix API specification
2
3use super::{OcrResult, FormatsData, LineData};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7
8/// Complete API response matching Scipix format
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ApiResponse {
11    /// Request identifier
12    pub request_id: String,
13
14    /// API version
15    pub version: String,
16
17    /// Image information
18    pub image_width: u32,
19    pub image_height: u32,
20
21    /// Detection metadata
22    pub is_printed: bool,
23    pub is_handwritten: bool,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub auto_rotate_confidence: Option<f32>,
27
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub auto_rotate_degrees: Option<i32>,
30
31    /// Confidence metrics
32    pub confidence: f32,
33    pub confidence_rate: f32,
34
35    /// Available output formats
36    #[serde(flatten)]
37    pub formats: FormatsData,
38
39    /// Detailed line data
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub line_data: Option<Vec<LineData>>,
42
43    /// Error information
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub error: Option<String>,
46
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub error_info: Option<ErrorInfo>,
49
50    /// Processing metadata
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub metadata: Option<HashMap<String, Value>>,
53}
54
55/// Error information structure
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ErrorInfo {
58    pub code: String,
59    pub message: String,
60
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub details: Option<Value>,
63}
64
65impl ApiResponse {
66    /// Create response from OCR result
67    pub fn from_ocr_result(result: OcrResult) -> Self {
68        Self {
69            request_id: result.request_id,
70            version: result.version,
71            image_width: result.image_width,
72            image_height: result.image_height,
73            is_printed: result.is_printed,
74            is_handwritten: result.is_handwritten,
75            auto_rotate_confidence: Some(result.auto_rotate_confidence),
76            auto_rotate_degrees: Some(result.auto_rotate_degrees),
77            confidence: result.confidence,
78            confidence_rate: result.confidence_rate,
79            formats: result.formats,
80            line_data: result.line_data,
81            error: result.error,
82            error_info: None,
83            metadata: if result.metadata.is_empty() {
84                None
85            } else {
86                Some(result.metadata)
87            },
88        }
89    }
90
91    /// Create error response
92    pub fn error(request_id: String, code: &str, message: &str) -> Self {
93        Self {
94            request_id,
95            version: "3.0".to_string(),
96            image_width: 0,
97            image_height: 0,
98            is_printed: false,
99            is_handwritten: false,
100            auto_rotate_confidence: None,
101            auto_rotate_degrees: None,
102            confidence: 0.0,
103            confidence_rate: 0.0,
104            formats: FormatsData::default(),
105            line_data: None,
106            error: Some(message.to_string()),
107            error_info: Some(ErrorInfo {
108                code: code.to_string(),
109                message: message.to_string(),
110                details: None,
111            }),
112            metadata: None,
113        }
114    }
115
116    /// Convert to JSON string
117    pub fn to_json(&self) -> Result<String, String> {
118        serde_json::to_string(self)
119            .map_err(|e| format!("JSON serialization error: {}", e))
120    }
121
122    /// Convert to pretty JSON string
123    pub fn to_json_pretty(&self) -> Result<String, String> {
124        serde_json::to_string_pretty(self)
125            .map_err(|e| format!("JSON serialization error: {}", e))
126    }
127
128    /// Parse from JSON string
129    pub fn from_json(json: &str) -> Result<Self, String> {
130        serde_json::from_str(json)
131            .map_err(|e| format!("JSON parsing error: {}", e))
132    }
133}
134
135/// Batch API response
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct BatchApiResponse {
138    pub batch_id: String,
139    pub total: usize,
140    pub completed: usize,
141    pub results: Vec<ApiResponse>,
142
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub errors: Option<Vec<BatchError>>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct BatchError {
149    pub index: usize,
150    pub error: ErrorInfo,
151}
152
153impl BatchApiResponse {
154    pub fn new(batch_id: String, results: Vec<ApiResponse>) -> Self {
155        let total = results.len();
156        let completed = results.iter().filter(|r| r.error.is_none()).count();
157
158        let errors: Vec<BatchError> = results
159            .iter()
160            .enumerate()
161            .filter_map(|(i, r)| {
162                r.error_info.as_ref().map(|e| BatchError {
163                    index: i,
164                    error: e.clone(),
165                })
166            })
167            .collect();
168
169        Self {
170            batch_id,
171            total,
172            completed,
173            results,
174            errors: if errors.is_empty() { None } else { Some(errors) },
175        }
176    }
177
178    pub fn to_json(&self) -> Result<String, String> {
179        serde_json::to_string(self)
180            .map_err(|e| format!("JSON serialization error: {}", e))
181    }
182
183    pub fn to_json_pretty(&self) -> Result<String, String> {
184        serde_json::to_string_pretty(self)
185            .map_err(|e| format!("JSON serialization error: {}", e))
186    }
187}
188
189/// API request format
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ApiRequest {
192    /// Image source (URL or base64)
193    pub src: String,
194
195    /// Requested output formats
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub formats: Option<Vec<String>>,
198
199    /// OCR options
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub ocr: Option<OcrOptions>,
202
203    /// Additional metadata
204    #[serde(flatten)]
205    pub metadata: HashMap<String, Value>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct OcrOptions {
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub math_inline_delimiters: Option<Vec<String>>,
212
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub math_display_delimiters: Option<Vec<String>>,
215
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub rm_spaces: Option<bool>,
218
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub rm_fonts: Option<bool>,
221
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub numbers_default_to_math: Option<bool>,
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    fn create_test_result() -> OcrResult {
231        OcrResult {
232            request_id: "test_123".to_string(),
233            version: "3.0".to_string(),
234            image_width: 800,
235            image_height: 600,
236            is_printed: true,
237            is_handwritten: false,
238            auto_rotate_confidence: 0.95,
239            auto_rotate_degrees: 0,
240            confidence: 0.98,
241            confidence_rate: 0.97,
242            formats: FormatsData {
243                text: Some("E = mc^2".to_string()),
244                latex_normal: Some(r"E = mc^2".to_string()),
245                ..Default::default()
246            },
247            line_data: None,
248            error: None,
249            metadata: HashMap::new(),
250        }
251    }
252
253    #[test]
254    fn test_api_response_from_result() {
255        let result = create_test_result();
256        let response = ApiResponse::from_ocr_result(result);
257
258        assert_eq!(response.request_id, "test_123");
259        assert_eq!(response.version, "3.0");
260        assert_eq!(response.confidence, 0.98);
261        assert!(response.formats.text.is_some());
262    }
263
264    #[test]
265    fn test_api_response_to_json() {
266        let result = create_test_result();
267        let response = ApiResponse::from_ocr_result(result);
268        let json = response.to_json().unwrap();
269
270        assert!(json.contains("request_id"));
271        assert!(json.contains("test_123"));
272        assert!(json.contains("confidence"));
273    }
274
275    #[test]
276    fn test_api_response_round_trip() {
277        let result = create_test_result();
278        let response = ApiResponse::from_ocr_result(result);
279        let json = response.to_json().unwrap();
280        let parsed = ApiResponse::from_json(&json).unwrap();
281
282        assert_eq!(response.request_id, parsed.request_id);
283        assert_eq!(response.confidence, parsed.confidence);
284    }
285
286    #[test]
287    fn test_error_response() {
288        let response = ApiResponse::error(
289            "test_456".to_string(),
290            "invalid_image",
291            "Image format not supported"
292        );
293
294        assert_eq!(response.request_id, "test_456");
295        assert!(response.error.is_some());
296        assert!(response.error_info.is_some());
297
298        let error_info = response.error_info.unwrap();
299        assert_eq!(error_info.code, "invalid_image");
300    }
301
302    #[test]
303    fn test_batch_response() {
304        let result1 = create_test_result();
305        let result2 = create_test_result();
306
307        let responses = vec![
308            ApiResponse::from_ocr_result(result1),
309            ApiResponse::from_ocr_result(result2),
310        ];
311
312        let batch = BatchApiResponse::new("batch_789".to_string(), responses);
313
314        assert_eq!(batch.batch_id, "batch_789");
315        assert_eq!(batch.total, 2);
316        assert_eq!(batch.completed, 2);
317        assert!(batch.errors.is_none());
318    }
319
320    #[test]
321    fn test_batch_with_errors() {
322        let success = create_test_result();
323        let error_response = ApiResponse::error(
324            "fail_1".to_string(),
325            "timeout",
326            "Processing timeout"
327        );
328
329        let responses = vec![
330            ApiResponse::from_ocr_result(success),
331            error_response,
332        ];
333
334        let batch = BatchApiResponse::new("batch_error".to_string(), responses);
335
336        assert_eq!(batch.total, 2);
337        assert_eq!(batch.completed, 1);
338        assert!(batch.errors.is_some());
339        assert_eq!(batch.errors.unwrap().len(), 1);
340    }
341
342    #[test]
343    fn test_api_request() {
344        let request = ApiRequest {
345            src: "https://example.com/image.png".to_string(),
346            formats: Some(vec!["text".to_string(), "latex_styled".to_string()]),
347            ocr: Some(OcrOptions {
348                math_inline_delimiters: Some(vec!["$".to_string(), "$".to_string()]),
349                math_display_delimiters: Some(vec!["$$".to_string(), "$$".to_string()]),
350                rm_spaces: Some(true),
351                rm_fonts: None,
352                numbers_default_to_math: Some(false),
353            }),
354            metadata: HashMap::new(),
355        };
356
357        let json = serde_json::to_string(&request).unwrap();
358        assert!(json.contains("src"));
359        assert!(json.contains("formats"));
360    }
361}