1use serde::{Deserialize, Serialize};
9use serde_json::{Value, json};
10
11#[derive(Debug, Clone)]
13#[allow(dead_code)]
14pub struct CliContext {
15 pub formatter: OutputFormatter,
17 pub verbose: bool,
19 pub debug: bool,
21}
22
23impl CliContext {
24 #[allow(
26 dead_code,
27 clippy::too_many_arguments,
28 clippy::fn_params_excessive_bools,
29 clippy::missing_const_for_fn
30 )]
31 pub fn new(json_mode: bool, quiet_mode: bool, verbose: bool, debug: bool) -> Self {
32 Self {
33 formatter: OutputFormatter::new(json_mode, quiet_mode),
34 verbose,
35 debug,
36 }
37 }
38
39 #[allow(dead_code)]
41 pub fn print_result(&self, result: &CommandResult) -> i32 {
42 let output = self.formatter.format(result);
43 if !output.is_empty() {
44 println!("{output}");
45 }
46 result.exit_code
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct OutputFormatter {
53 json_mode: bool,
54 quiet_mode: bool,
55}
56
57impl OutputFormatter {
58 pub const fn new(json_mode: bool, quiet_mode: bool) -> Self {
64 Self {
65 json_mode,
66 quiet_mode,
67 }
68 }
69
70 pub fn format(&self, result: &CommandResult) -> String {
72 match (self.json_mode, self.quiet_mode) {
73 (true, _) => serde_json::to_string(result).unwrap_or_else(|_| {
75 json!({
76 "status": "error",
77 "command": "unknown",
78 "message": "Failed to serialize response"
79 })
80 .to_string()
81 }),
82 (false, true) => String::new(),
84 (false, false) => Self::format_text(result),
86 }
87 }
88
89 fn format_text(result: &CommandResult) -> String {
90 match result.status.as_str() {
91 "success" => {
92 let mut output = format!("✓ {} succeeded", result.command);
93
94 if !result.warnings.is_empty() {
95 output.push_str("\n\nWarnings:");
96 for warning in &result.warnings {
97 output.push_str(&format!("\n • {warning}"));
98 }
99 }
100
101 output
102 },
103 "validation-failed" => {
104 let mut output = format!("✗ {} validation failed", result.command);
105
106 if !result.errors.is_empty() {
107 output.push_str("\n\nErrors:");
108 for error in &result.errors {
109 output.push_str(&format!("\n • {error}"));
110 }
111 }
112
113 output
114 },
115 "error" => {
116 let mut output = format!("✗ {} error", result.command);
117
118 if let Some(msg) = &result.message {
119 output.push_str(&format!("\n {msg}"));
120 }
121
122 if let Some(code) = &result.code {
123 output.push_str(&format!("\n Code: {code}"));
124 }
125
126 output
127 },
128 _ => format!("? {} - unknown status: {}", result.command, result.status),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct CommandResult {
136 pub status: String,
138
139 pub command: String,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub data: Option<Value>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub message: Option<String>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub code: Option<String>,
153
154 #[serde(skip_serializing_if = "Vec::is_empty")]
156 pub errors: Vec<String>,
157
158 #[serde(skip_serializing_if = "Vec::is_empty")]
160 pub warnings: Vec<String>,
161
162 #[serde(skip)]
164 #[allow(dead_code)]
165 pub exit_code: i32,
166}
167
168impl CommandResult {
169 pub fn success(command: &str, data: Value) -> Self {
171 Self {
172 status: "success".to_string(),
173 command: command.to_string(),
174 data: Some(data),
175 message: None,
176 code: None,
177 errors: Vec::new(),
178 warnings: Vec::new(),
179 exit_code: 0,
180 }
181 }
182
183 pub fn success_with_warnings(command: &str, data: Value, warnings: Vec<String>) -> Self {
185 Self {
186 status: "success".to_string(),
187 command: command.to_string(),
188 data: Some(data),
189 message: None,
190 code: None,
191 errors: Vec::new(),
192 warnings,
193 exit_code: 0,
194 }
195 }
196
197 pub fn error(command: &str, message: &str, code: &str) -> Self {
199 Self {
200 status: "error".to_string(),
201 command: command.to_string(),
202 data: None,
203 message: Some(message.to_string()),
204 code: Some(code.to_string()),
205 errors: Vec::new(),
206 warnings: Vec::new(),
207 exit_code: 1,
208 }
209 }
210
211 #[allow(dead_code)]
213 pub fn validation_failed(command: &str, errors: Vec<String>) -> Self {
214 Self {
215 status: "validation-failed".to_string(),
216 command: command.to_string(),
217 data: None,
218 message: None,
219 code: None,
220 errors,
221 warnings: Vec::new(),
222 exit_code: 2,
223 }
224 }
225
226 #[allow(dead_code)]
228 pub fn from_error(command: &str, error: anyhow::Error) -> Self {
229 Self::error(command, &error.to_string(), "INTERNAL_ERROR")
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_output_formatter_json_mode_success() {
239 let formatter = OutputFormatter::new(true, false);
240
241 let result = CommandResult::success(
242 "compile",
243 json!({
244 "files_compiled": 2,
245 "output_file": "schema.compiled.json"
246 }),
247 );
248
249 let output = formatter.format(&result);
250 assert!(!output.is_empty());
251
252 let parsed: serde_json::Value =
254 serde_json::from_str(&output).expect("Output must be valid JSON");
255 assert_eq!(parsed["status"], "success");
256 assert_eq!(parsed["command"], "compile");
257 }
258
259 #[test]
260 fn test_output_formatter_text_mode_success() {
261 let formatter = OutputFormatter::new(false, false);
262
263 let result = CommandResult::success("compile", json!({}));
264 let output = formatter.format(&result);
265
266 assert!(!output.is_empty());
267 assert!(output.contains("compile"));
268 assert!(output.contains("✓"));
269 }
270
271 #[test]
272 fn test_output_formatter_quiet_mode() {
273 let formatter = OutputFormatter::new(false, true);
274
275 let result = CommandResult::success("compile", json!({}));
276 let output = formatter.format(&result);
277
278 assert_eq!(output, "");
279 }
280
281 #[test]
282 fn test_output_formatter_json_mode_error() {
283 let formatter = OutputFormatter::new(true, false);
284
285 let result = CommandResult::error("compile", "Parse error", "PARSE_ERROR");
286
287 let output = formatter.format(&result);
288 assert!(!output.is_empty());
289
290 let parsed: serde_json::Value =
291 serde_json::from_str(&output).expect("Output must be valid JSON");
292 assert_eq!(parsed["status"], "error");
293 assert_eq!(parsed["command"], "compile");
294 assert_eq!(parsed["code"], "PARSE_ERROR");
295 }
296
297 #[test]
298 fn test_output_formatter_validation_failure() {
299 let formatter = OutputFormatter::new(true, false);
300
301 let result = CommandResult::validation_failed(
302 "validate",
303 vec![
304 "Invalid type: User".to_string(),
305 "Missing field: id".to_string(),
306 ],
307 );
308
309 let output = formatter.format(&result);
310
311 let parsed: serde_json::Value =
312 serde_json::from_str(&output).expect("Output must be valid JSON");
313 assert_eq!(parsed["status"], "validation-failed");
314 assert!(parsed["errors"].is_array());
315 assert_eq!(parsed["errors"].as_array().unwrap().len(), 2);
316 }
317
318 #[test]
319 fn test_command_result_preserves_data() {
320 let data = json!({
321 "count": 42,
322 "nested": {
323 "value": "test"
324 }
325 });
326
327 let result = CommandResult::success("test", data.clone());
328
329 assert_eq!(result.data, Some(data));
331 }
332
333 #[test]
334 fn test_output_formatter_with_warnings() {
335 let formatter = OutputFormatter::new(true, false);
336
337 let result = CommandResult::success_with_warnings(
338 "compile",
339 json!({ "status": "ok" }),
340 vec!["Optimization opportunity: add index to User.id".to_string()],
341 );
342
343 let output = formatter.format(&result);
344 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Valid JSON");
345
346 assert_eq!(parsed["status"], "success");
347 assert!(parsed["warnings"].is_array());
348 }
349
350 #[test]
351 fn test_text_mode_shows_status() {
352 let formatter = OutputFormatter::new(false, false);
353
354 let result = CommandResult::success("compile", json!({}));
355 let output = formatter.format(&result);
356
357 assert!(output.to_lowercase().contains("success") || output.contains("✓"));
359 }
360
361 #[test]
362 fn test_text_mode_shows_error() {
363 let formatter = OutputFormatter::new(false, false);
364
365 let result = CommandResult::error("compile", "File not found", "FILE_NOT_FOUND");
366 let output = formatter.format(&result);
367
368 assert!(
369 output.to_lowercase().contains("error")
370 || output.contains("✗")
371 || output.contains("file")
372 );
373 }
374
375 #[test]
376 fn test_quiet_mode_suppresses_all_output() {
377 let formatter = OutputFormatter::new(false, true);
378
379 let success = CommandResult::success("compile", json!({}));
380 let error = CommandResult::error("validate", "Invalid", "INVALID");
381
382 assert_eq!(formatter.format(&success), "");
383 assert_eq!(formatter.format(&error), "");
384 }
385
386 #[test]
387 fn test_json_mode_ignores_quiet_flag() {
388 let formatter = OutputFormatter::new(true, true);
390
391 let result = CommandResult::success("compile", json!({}));
392 let output = formatter.format(&result);
393
394 let parsed: serde_json::Value =
396 serde_json::from_str(&output).expect("Should be valid JSON");
397 assert_eq!(parsed["status"], "success");
398 }
399
400 #[test]
401 fn test_command_result_from_anyhow_error() {
402 let error = anyhow::anyhow!("Database connection failed");
403 let result = CommandResult::from_error("serve", error);
404
405 assert_eq!(result.status, "error");
406 assert_eq!(result.command, "serve");
407 }
408
409 #[test]
410 fn test_validation_failed_exit_code() {
411 let result = CommandResult::validation_failed("validate", vec!["Error 1".to_string()]);
412
413 assert_eq!(result.exit_code, 2);
415 }
416
417 #[test]
418 fn test_error_exit_code() {
419 let result = CommandResult::error("compile", "Failed", "FAILED");
420
421 assert_eq!(result.exit_code, 1);
422 }
423
424 #[test]
425 fn test_success_exit_code() {
426 let result = CommandResult::success("compile", json!({}));
427
428 assert_eq!(result.exit_code, 0);
429 }
430}