1use ripsed_core::diff::{OpResult, Summary};
2use ripsed_core::error::RipsedError;
3use serde::{Deserialize, Serialize};
4
5#[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#[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 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 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 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 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}