oxigaf_cli/
json_output.rs1use std::path::PathBuf;
11
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Serialize, Deserialize)]
20pub struct JsonOutput {
21 pub version: String,
23
24 pub command: String,
26
27 pub status: Status,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub result: Option<serde_json::Value>,
33
34 #[serde(skip_serializing_if = "Vec::is_empty", default)]
36 pub artifacts: Vec<Artifact>,
37
38 #[serde(skip_serializing_if = "Vec::is_empty", default)]
40 pub warnings: Vec<String>,
41
42 #[serde(skip_serializing_if = "Vec::is_empty", default)]
44 pub errors: Vec<String>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub metadata: Option<serde_json::Value>,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum Status {
55 Success,
57 Error,
59 Warning,
61}
62
63#[derive(Debug, Serialize, Deserialize)]
65pub struct Artifact {
66 #[serde(rename = "type")]
68 pub artifact_type: String,
69
70 pub path: PathBuf,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub size_bytes: Option<u64>,
76}
77
78impl JsonOutput {
83 #[must_use]
85 pub fn new(command: &str) -> Self {
86 Self {
87 version: env!("CARGO_PKG_VERSION").to_string(),
88 command: command.to_string(),
89 status: Status::Success,
90 result: None,
91 artifacts: Vec::new(),
92 warnings: Vec::new(),
93 errors: Vec::new(),
94 metadata: None,
95 }
96 }
97
98 #[must_use]
100 pub fn success(command: &str, result: serde_json::Value) -> Self {
101 let mut output = Self::new(command);
102 output.result = Some(result);
103 output
104 }
105
106 #[must_use]
108 pub fn error(command: &str, error: String) -> Self {
109 let mut output = Self::new(command);
110 output.status = Status::Error;
111 output.errors.push(error);
112 output
113 }
114
115 pub fn add_artifact(&mut self, artifact_type: String, path: PathBuf) {
119 let size_bytes = std::fs::metadata(&path).ok().map(|m| m.len());
120 self.artifacts.push(Artifact {
121 artifact_type,
122 path,
123 size_bytes,
124 });
125 }
126
127 #[allow(dead_code)]
129 pub fn add_warning(&mut self, warning: String) {
130 self.warnings.push(warning);
131 if !self.errors.is_empty() {
132 } else {
134 self.status = Status::Warning;
135 }
136 }
137
138 #[allow(dead_code)]
140 pub fn add_error(&mut self, error: String) {
141 self.errors.push(error);
142 self.status = Status::Error;
143 }
144
145 pub fn print(&self) {
150 match serde_json::to_string_pretty(self) {
151 Ok(json) => {
152 println!("{}", json);
153 }
154 Err(e) => {
155 eprintln!(
157 "{{\"error\": \"Failed to serialize JSON: {}\"}}",
158 e.to_string().replace('"', "\\\"")
159 );
160 }
161 }
162 }
163}
164
165#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_json_output_new() {
175 let output = JsonOutput::new("test");
176 assert_eq!(output.command, "test");
177 assert_eq!(output.version, env!("CARGO_PKG_VERSION"));
178 assert!(matches!(output.status, Status::Success));
179 assert!(output.result.is_none());
180 assert!(output.artifacts.is_empty());
181 assert!(output.warnings.is_empty());
182 assert!(output.errors.is_empty());
183 }
184
185 #[test]
186 fn test_json_output_success() {
187 let result = serde_json::json!({"iterations": 1000});
188 let output = JsonOutput::success("train", result.clone());
189 assert_eq!(output.command, "train");
190 assert!(matches!(output.status, Status::Success));
191 assert_eq!(output.result, Some(result));
192 }
193
194 #[test]
195 fn test_json_output_error() {
196 let output = JsonOutput::error("export", "File not found".to_string());
197 assert_eq!(output.command, "export");
198 assert!(matches!(output.status, Status::Error));
199 assert_eq!(output.errors.len(), 1);
200 assert_eq!(output.errors[0], "File not found");
201 }
202
203 #[test]
204 fn test_add_warning() {
205 let mut output = JsonOutput::new("test");
206 output.add_warning("Low memory".to_string());
207 assert!(matches!(output.status, Status::Warning));
208 assert_eq!(output.warnings.len(), 1);
209 }
210
211 #[test]
212 fn test_add_error() {
213 let mut output = JsonOutput::new("test");
214 output.add_error("Fatal error".to_string());
215 assert!(matches!(output.status, Status::Error));
216 assert_eq!(output.errors.len(), 1);
217 }
218
219 #[test]
220 #[allow(clippy::expect_used)]
221 fn test_serialization() {
222 let output = JsonOutput::success(
223 "test",
224 serde_json::json!({
225 "key": "value"
226 }),
227 );
228
229 let json = serde_json::to_string(&output).expect("Serialization should succeed");
230 assert!(json.contains("\"command\":\"test\""));
231 assert!(json.contains("\"status\":\"success\""));
232 }
233
234 #[test]
235 #[allow(clippy::expect_used)]
236 fn test_skip_serializing_empty_fields() {
237 let output = JsonOutput::new("test");
238 let json = serde_json::to_string(&output).expect("Serialization should succeed");
239
240 assert!(!json.contains("\"artifacts\""));
242 assert!(!json.contains("\"warnings\""));
243 assert!(!json.contains("\"errors\""));
244 assert!(!json.contains("\"result\""));
246 assert!(!json.contains("\"metadata\""));
247 }
248}