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            .expect("UndoResponse serialization is infallible for known field types")
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: crate::schema::CURRENT_VERSION.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: crate::schema::CURRENT_VERSION.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)
67            .expect("JsonResponse serialization is infallible for known field types")
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use ripsed_core::diff::{Change, FileChanges};
75
76    #[test]
77    fn success_response_has_correct_fields() {
78        let summary = Summary {
79            files_matched: 3,
80            files_modified: 2,
81            total_replacements: 5,
82        };
83        let results = vec![OpResult {
84            operation_index: 0,
85            files: vec![FileChanges {
86                path: "src/lib.rs".into(),
87                changes: vec![Change {
88                    line: 1,
89                    before: "old".into(),
90                    after: Some("new".into()),
91                    context: None,
92                }],
93            }],
94        }];
95        let resp = JsonResponse::success(true, summary, results);
96        assert_eq!(resp.version, "1");
97        assert!(resp.success);
98        assert!(resp.dry_run);
99        assert_eq!(resp.summary.files_matched, 3);
100        assert_eq!(resp.results.len(), 1);
101        assert!(resp.errors.is_empty());
102    }
103
104    #[test]
105    fn error_response_has_correct_fields() {
106        let err = RipsedError::invalid_request("bad input", "fix it");
107        let resp = JsonResponse::error(vec![err]);
108        assert_eq!(resp.version, "1");
109        assert!(!resp.success);
110        assert!(!resp.dry_run);
111        assert_eq!(resp.summary, Summary::default());
112        assert!(resp.results.is_empty());
113        assert_eq!(resp.errors.len(), 1);
114        assert_eq!(resp.errors[0].message, "bad input");
115    }
116
117    #[test]
118    fn to_json_produces_valid_json() {
119        let resp = JsonResponse::success(false, Summary::default(), vec![]);
120        let json_str = resp.to_json();
121        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
122        assert_eq!(parsed["version"], "1");
123        assert_eq!(parsed["success"], true);
124        assert_eq!(parsed["dry_run"], false);
125    }
126
127    #[test]
128    fn to_json_error_response_includes_errors() {
129        let err = RipsedError::internal_error("oops");
130        let resp = JsonResponse::error(vec![err]);
131        let json_str = resp.to_json();
132        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
133        assert_eq!(parsed["success"], false);
134        assert_eq!(parsed["errors"][0]["code"], "internal_error");
135    }
136
137    #[test]
138    fn response_roundtrips_through_json() {
139        let resp = JsonResponse::success(
140            true,
141            Summary {
142                files_matched: 1,
143                files_modified: 0,
144                total_replacements: 2,
145            },
146            vec![],
147        );
148        let json_str = resp.to_json();
149        let deserialized: JsonResponse = serde_json::from_str(&json_str).unwrap();
150        assert_eq!(deserialized.version, "1");
151        assert!(deserialized.success);
152        assert!(deserialized.dry_run);
153        assert_eq!(deserialized.summary.total_replacements, 2);
154    }
155
156    #[test]
157    fn undo_response_serializes() {
158        let resp = UndoResponse {
159            version: "1".into(),
160            success: true,
161            undo: UndoSummary {
162                operations_reverted: 2,
163                files_restored: 3,
164                log_entries_remaining: 8,
165            },
166        };
167        let json = serde_json::to_value(&resp).unwrap();
168        assert_eq!(json["undo"]["operations_reverted"], 2);
169        assert_eq!(json["undo"]["files_restored"], 3);
170        assert_eq!(json["undo"]["log_entries_remaining"], 8);
171    }
172}