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
168#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct CliHelp {
175 pub name: String,
177
178 pub version: String,
180
181 pub about: String,
183
184 pub global_options: Vec<ArgumentHelp>,
186
187 pub subcommands: Vec<CommandHelp>,
189
190 pub exit_codes: Vec<ExitCodeHelp>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct CommandHelp {
197 pub name: String,
199
200 pub about: String,
202
203 pub arguments: Vec<ArgumentHelp>,
205
206 pub options: Vec<ArgumentHelp>,
208
209 #[serde(skip_serializing_if = "Vec::is_empty")]
211 pub subcommands: Vec<CommandHelp>,
212
213 #[serde(skip_serializing_if = "Vec::is_empty")]
215 pub examples: Vec<String>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct ArgumentHelp {
221 pub name: String,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub short: Option<String>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub long: Option<String>,
231
232 pub help: String,
234
235 pub required: bool,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub default_value: Option<String>,
241
242 pub takes_value: bool,
244
245 #[serde(skip_serializing_if = "Vec::is_empty")]
247 pub possible_values: Vec<String>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ExitCodeHelp {
253 pub code: i32,
255
256 pub name: String,
258
259 pub description: String,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct OutputSchema {
266 pub command: String,
268
269 pub schema_version: String,
271
272 pub format: String,
274
275 pub success: serde_json::Value,
277
278 pub error: serde_json::Value,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct CommandSummary {
285 pub name: String,
287
288 pub description: String,
290
291 pub has_subcommands: bool,
293}
294
295pub fn get_exit_codes() -> Vec<ExitCodeHelp> {
297 vec![
298 ExitCodeHelp {
299 code: 0,
300 name: "success".to_string(),
301 description: "Command completed successfully".to_string(),
302 },
303 ExitCodeHelp {
304 code: 1,
305 name: "error".to_string(),
306 description: "Command failed with an error".to_string(),
307 },
308 ExitCodeHelp {
309 code: 2,
310 name: "validation_failed".to_string(),
311 description: "Validation failed (schema or input invalid)".to_string(),
312 },
313 ]
314}
315
316impl CommandResult {
317 pub fn success(command: &str, data: Value) -> Self {
319 Self {
320 status: "success".to_string(),
321 command: command.to_string(),
322 data: Some(data),
323 message: None,
324 code: None,
325 errors: Vec::new(),
326 warnings: Vec::new(),
327 exit_code: 0,
328 }
329 }
330
331 pub fn success_with_warnings(command: &str, data: Value, warnings: Vec<String>) -> Self {
333 Self {
334 status: "success".to_string(),
335 command: command.to_string(),
336 data: Some(data),
337 message: None,
338 code: None,
339 errors: Vec::new(),
340 warnings,
341 exit_code: 0,
342 }
343 }
344
345 pub fn error(command: &str, message: &str, code: &str) -> Self {
347 Self {
348 status: "error".to_string(),
349 command: command.to_string(),
350 data: None,
351 message: Some(message.to_string()),
352 code: Some(code.to_string()),
353 errors: Vec::new(),
354 warnings: Vec::new(),
355 exit_code: 1,
356 }
357 }
358
359 #[allow(dead_code)]
361 pub fn validation_failed(command: &str, errors: Vec<String>) -> Self {
362 Self {
363 status: "validation-failed".to_string(),
364 command: command.to_string(),
365 data: None,
366 message: None,
367 code: None,
368 errors,
369 warnings: Vec::new(),
370 exit_code: 2,
371 }
372 }
373
374 #[allow(dead_code)]
376 pub fn from_error(command: &str, error: anyhow::Error) -> Self {
377 Self::error(command, &error.to_string(), "INTERNAL_ERROR")
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_output_formatter_json_mode_success() {
387 let formatter = OutputFormatter::new(true, false);
388
389 let result = CommandResult::success(
390 "compile",
391 json!({
392 "files_compiled": 2,
393 "output_file": "schema.compiled.json"
394 }),
395 );
396
397 let output = formatter.format(&result);
398 assert!(!output.is_empty());
399
400 let parsed: serde_json::Value =
402 serde_json::from_str(&output).expect("Output must be valid JSON");
403 assert_eq!(parsed["status"], "success");
404 assert_eq!(parsed["command"], "compile");
405 }
406
407 #[test]
408 fn test_output_formatter_text_mode_success() {
409 let formatter = OutputFormatter::new(false, false);
410
411 let result = CommandResult::success("compile", json!({}));
412 let output = formatter.format(&result);
413
414 assert!(!output.is_empty());
415 assert!(output.contains("compile"));
416 assert!(output.contains("✓"));
417 }
418
419 #[test]
420 fn test_output_formatter_quiet_mode() {
421 let formatter = OutputFormatter::new(false, true);
422
423 let result = CommandResult::success("compile", json!({}));
424 let output = formatter.format(&result);
425
426 assert_eq!(output, "");
427 }
428
429 #[test]
430 fn test_output_formatter_json_mode_error() {
431 let formatter = OutputFormatter::new(true, false);
432
433 let result = CommandResult::error("compile", "Parse error", "PARSE_ERROR");
434
435 let output = formatter.format(&result);
436 assert!(!output.is_empty());
437
438 let parsed: serde_json::Value =
439 serde_json::from_str(&output).expect("Output must be valid JSON");
440 assert_eq!(parsed["status"], "error");
441 assert_eq!(parsed["command"], "compile");
442 assert_eq!(parsed["code"], "PARSE_ERROR");
443 }
444
445 #[test]
446 fn test_output_formatter_validation_failure() {
447 let formatter = OutputFormatter::new(true, false);
448
449 let result = CommandResult::validation_failed(
450 "validate",
451 vec![
452 "Invalid type: User".to_string(),
453 "Missing field: id".to_string(),
454 ],
455 );
456
457 let output = formatter.format(&result);
458
459 let parsed: serde_json::Value =
460 serde_json::from_str(&output).expect("Output must be valid JSON");
461 assert_eq!(parsed["status"], "validation-failed");
462 assert!(parsed["errors"].is_array());
463 assert_eq!(parsed["errors"].as_array().unwrap().len(), 2);
464 }
465
466 #[test]
467 fn test_command_result_preserves_data() {
468 let data = json!({
469 "count": 42,
470 "nested": {
471 "value": "test"
472 }
473 });
474
475 let result = CommandResult::success("test", data.clone());
476
477 assert_eq!(result.data, Some(data));
479 }
480
481 #[test]
482 fn test_output_formatter_with_warnings() {
483 let formatter = OutputFormatter::new(true, false);
484
485 let result = CommandResult::success_with_warnings(
486 "compile",
487 json!({ "status": "ok" }),
488 vec!["Optimization opportunity: add index to User.id".to_string()],
489 );
490
491 let output = formatter.format(&result);
492 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Valid JSON");
493
494 assert_eq!(parsed["status"], "success");
495 assert!(parsed["warnings"].is_array());
496 }
497
498 #[test]
499 fn test_text_mode_shows_status() {
500 let formatter = OutputFormatter::new(false, false);
501
502 let result = CommandResult::success("compile", json!({}));
503 let output = formatter.format(&result);
504
505 assert!(output.to_lowercase().contains("success") || output.contains("✓"));
507 }
508
509 #[test]
510 fn test_text_mode_shows_error() {
511 let formatter = OutputFormatter::new(false, false);
512
513 let result = CommandResult::error("compile", "File not found", "FILE_NOT_FOUND");
514 let output = formatter.format(&result);
515
516 assert!(
517 output.to_lowercase().contains("error")
518 || output.contains("✗")
519 || output.contains("file")
520 );
521 }
522
523 #[test]
524 fn test_quiet_mode_suppresses_all_output() {
525 let formatter = OutputFormatter::new(false, true);
526
527 let success = CommandResult::success("compile", json!({}));
528 let error = CommandResult::error("validate", "Invalid", "INVALID");
529
530 assert_eq!(formatter.format(&success), "");
531 assert_eq!(formatter.format(&error), "");
532 }
533
534 #[test]
535 fn test_json_mode_ignores_quiet_flag() {
536 let formatter = OutputFormatter::new(true, true);
538
539 let result = CommandResult::success("compile", json!({}));
540 let output = formatter.format(&result);
541
542 let parsed: serde_json::Value =
544 serde_json::from_str(&output).expect("Should be valid JSON");
545 assert_eq!(parsed["status"], "success");
546 }
547
548 #[test]
549 fn test_command_result_from_anyhow_error() {
550 let error = anyhow::anyhow!("Database connection failed");
551 let result = CommandResult::from_error("serve", error);
552
553 assert_eq!(result.status, "error");
554 assert_eq!(result.command, "serve");
555 }
556
557 #[test]
558 fn test_validation_failed_exit_code() {
559 let result = CommandResult::validation_failed("validate", vec!["Error 1".to_string()]);
560
561 assert_eq!(result.exit_code, 2);
563 }
564
565 #[test]
566 fn test_error_exit_code() {
567 let result = CommandResult::error("compile", "Failed", "FAILED");
568
569 assert_eq!(result.exit_code, 1);
570 }
571
572 #[test]
573 fn test_success_exit_code() {
574 let result = CommandResult::success("compile", json!({}));
575
576 assert_eq!(result.exit_code, 0);
577 }
578}