Skip to main content

ripsed_json/
response.rs

1use ripsed_core::diff::{OpResult, Summary};
2use ripsed_core::error::RipsedError;
3use serde::{Deserialize, Serialize};
4
5/// The top-level JSON response.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct JsonResponse {
8    pub version: String,
9    pub success: bool,
10    pub dry_run: bool,
11    pub summary: Summary,
12    pub results: Vec<OpResult>,
13    pub errors: Vec<RipsedError>,
14}
15
16/// An undo response.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct UndoResponse {
19    pub version: String,
20    pub success: bool,
21    pub undo: UndoSummary,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct UndoSummary {
26    pub operations_reverted: usize,
27    pub files_restored: usize,
28    pub log_entries_remaining: usize,
29}
30
31impl UndoResponse {
32    /// Serialize to JSON string.
33    pub fn to_json(&self) -> String {
34        serde_json::to_string_pretty(self)
35            .unwrap_or_else(|_| r#"{"version":"1","success":false}"#.to_string())
36    }
37}
38
39impl JsonResponse {
40    /// Build a success response.
41    pub fn success(dry_run: bool, summary: Summary, results: Vec<OpResult>) -> Self {
42        Self {
43            version: "1".to_string(),
44            success: true,
45            dry_run,
46            summary,
47            results,
48            errors: vec![],
49        }
50    }
51
52    /// Build an error response.
53    pub fn error(errors: Vec<RipsedError>) -> Self {
54        Self {
55            version: "1".to_string(),
56            success: false,
57            dry_run: false,
58            summary: Summary::default(),
59            results: vec![],
60            errors,
61        }
62    }
63
64    /// Serialize to JSON string.
65    pub fn to_json(&self) -> String {
66        serde_json::to_string_pretty(self).unwrap_or_else(|_| {
67            r#"{"version":"1","success":false,"errors":[{"code":"internal_error","message":"Failed to serialize response"}]}"#.to_string()
68        })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use ripsed_core::diff::{Change, FileChanges};
76
77    #[test]
78    fn success_response_has_correct_fields() {
79        let summary = Summary {
80            files_matched: 3,
81            files_modified: 2,
82            total_replacements: 5,
83        };
84        let results = vec![OpResult {
85            operation_index: 0,
86            files: vec![FileChanges {
87                path: "src/lib.rs".into(),
88                changes: vec![Change {
89                    line: 1,
90                    before: "old".into(),
91                    after: Some("new".into()),
92                    context: None,
93                }],
94            }],
95        }];
96        let resp = JsonResponse::success(true, summary, results);
97        assert_eq!(resp.version, "1");
98        assert!(resp.success);
99        assert!(resp.dry_run);
100        assert_eq!(resp.summary.files_matched, 3);
101        assert_eq!(resp.results.len(), 1);
102        assert!(resp.errors.is_empty());
103    }
104
105    #[test]
106    fn error_response_has_correct_fields() {
107        let err = RipsedError::invalid_request("bad input", "fix it");
108        let resp = JsonResponse::error(vec![err]);
109        assert_eq!(resp.version, "1");
110        assert!(!resp.success);
111        assert!(!resp.dry_run);
112        assert_eq!(resp.summary, Summary::default());
113        assert!(resp.results.is_empty());
114        assert_eq!(resp.errors.len(), 1);
115        assert_eq!(resp.errors[0].message, "bad input");
116    }
117
118    #[test]
119    fn to_json_produces_valid_json() {
120        let resp = JsonResponse::success(false, Summary::default(), vec![]);
121        let json_str = resp.to_json();
122        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
123        assert_eq!(parsed["version"], "1");
124        assert_eq!(parsed["success"], true);
125        assert_eq!(parsed["dry_run"], false);
126    }
127
128    #[test]
129    fn to_json_error_response_includes_errors() {
130        let err = RipsedError::internal_error("oops");
131        let resp = JsonResponse::error(vec![err]);
132        let json_str = resp.to_json();
133        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
134        assert_eq!(parsed["success"], false);
135        assert_eq!(parsed["errors"][0]["code"], "internal_error");
136    }
137
138    #[test]
139    fn response_roundtrips_through_json() {
140        let resp = JsonResponse::success(
141            true,
142            Summary {
143                files_matched: 1,
144                files_modified: 0,
145                total_replacements: 2,
146            },
147            vec![],
148        );
149        let json_str = resp.to_json();
150        let deserialized: JsonResponse = serde_json::from_str(&json_str).unwrap();
151        assert_eq!(deserialized.version, "1");
152        assert!(deserialized.success);
153        assert!(deserialized.dry_run);
154        assert_eq!(deserialized.summary.total_replacements, 2);
155    }
156
157    #[test]
158    fn undo_response_serializes() {
159        let resp = UndoResponse {
160            version: "1".into(),
161            success: true,
162            undo: UndoSummary {
163                operations_reverted: 2,
164                files_restored: 3,
165                log_entries_remaining: 8,
166            },
167        };
168        let json = serde_json::to_value(&resp).unwrap();
169        assert_eq!(json["undo"]["operations_reverted"], 2);
170        assert_eq!(json["undo"]["files_restored"], 3);
171        assert_eq!(json["undo"]["log_entries_remaining"], 8);
172    }
173}