1use serde::{Deserialize, Serialize};
9use serde_json::{Value, json};
10
11#[derive(Debug, Clone)]
13pub struct OutputFormatter {
14 json_mode: bool,
15 quiet_mode: bool,
16}
17
18impl OutputFormatter {
19 pub const fn new(json_mode: bool, quiet_mode: bool) -> Self {
25 Self {
26 json_mode,
27 quiet_mode,
28 }
29 }
30
31 pub fn format(&self, result: &CommandResult) -> String {
33 match (self.json_mode, self.quiet_mode) {
34 (true, _) => serde_json::to_string(result).unwrap_or_else(|_| {
36 json!({
37 "status": "error",
38 "command": "unknown",
39 "message": "Failed to serialize response"
40 })
41 .to_string()
42 }),
43 (false, true) => String::new(),
45 (false, false) => Self::format_text(result),
47 }
48 }
49
50 pub fn progress(&self, msg: &str) {
55 if !self.quiet_mode && !self.json_mode {
56 eprintln!("{msg}");
57 }
58 }
59
60 pub fn section(&self, title: &str) {
62 self.progress(&format!("==> {title}"));
63 }
64
65 fn format_text(result: &CommandResult) -> String {
66 match result.status.as_str() {
67 "success" => {
68 let mut output = format!("✓ {} succeeded", result.command);
69
70 if !result.warnings.is_empty() {
71 output.push_str("\n\nWarnings:");
72 for warning in &result.warnings {
73 output.push_str(&format!("\n • {warning}"));
74 }
75 }
76
77 output
78 },
79 "validation-failed" => {
80 let mut output = format!("✗ {} validation failed", result.command);
81
82 if !result.errors.is_empty() {
83 output.push_str("\n\nErrors:");
84 for error in &result.errors {
85 output.push_str(&format!("\n • {error}"));
86 }
87 }
88
89 output
90 },
91 "error" => {
92 let mut output = format!("✗ {} error", result.command);
93
94 if let Some(msg) = &result.message {
95 output.push_str(&format!("\n {msg}"));
96 }
97
98 if let Some(code) = &result.code {
99 output.push_str(&format!("\n Code: {code}"));
100 }
101
102 output
103 },
104 _ => format!("? {} - unknown status: {}", result.command, result.status),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CommandResult {
112 pub status: String,
114
115 pub command: String,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub data: Option<Value>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub message: Option<String>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub code: Option<String>,
129
130 #[serde(skip_serializing_if = "Vec::is_empty")]
132 pub errors: Vec<String>,
133
134 #[serde(skip_serializing_if = "Vec::is_empty")]
136 pub warnings: Vec<String>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct CliHelp {
146 pub name: String,
148
149 pub version: String,
151
152 pub about: String,
154
155 pub global_options: Vec<ArgumentHelp>,
157
158 pub subcommands: Vec<CommandHelp>,
160
161 pub exit_codes: Vec<ExitCodeHelp>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct CommandHelp {
168 pub name: String,
170
171 pub about: String,
173
174 pub arguments: Vec<ArgumentHelp>,
176
177 pub options: Vec<ArgumentHelp>,
179
180 #[serde(skip_serializing_if = "Vec::is_empty")]
182 pub subcommands: Vec<CommandHelp>,
183
184 #[serde(skip_serializing_if = "Vec::is_empty")]
186 pub examples: Vec<String>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ArgumentHelp {
192 pub name: String,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub short: Option<String>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub long: Option<String>,
202
203 pub help: String,
205
206 pub required: bool,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub default_value: Option<String>,
212
213 pub takes_value: bool,
215
216 #[serde(skip_serializing_if = "Vec::is_empty")]
218 pub possible_values: Vec<String>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct ExitCodeHelp {
224 pub code: i32,
226
227 pub name: String,
229
230 pub description: String,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct OutputSchema {
237 pub command: String,
239
240 pub schema_version: String,
242
243 pub format: String,
245
246 pub success: serde_json::Value,
248
249 pub error: serde_json::Value,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct CommandSummary {
256 pub name: String,
258
259 pub description: String,
261
262 pub has_subcommands: bool,
264}
265
266pub fn get_exit_codes() -> Vec<ExitCodeHelp> {
268 vec![
269 ExitCodeHelp {
270 code: 0,
271 name: "success".to_string(),
272 description: "Command completed successfully".to_string(),
273 },
274 ExitCodeHelp {
275 code: 1,
276 name: "error".to_string(),
277 description: "Command failed with an error".to_string(),
278 },
279 ExitCodeHelp {
280 code: 2,
281 name: "validation_failed".to_string(),
282 description: "Validation failed (schema or input invalid)".to_string(),
283 },
284 ]
285}
286
287impl CommandResult {
288 pub fn success(command: &str, data: Value) -> Self {
290 Self {
291 status: "success".to_string(),
292 command: command.to_string(),
293 data: Some(data),
294 message: None,
295 code: None,
296 errors: Vec::new(),
297 warnings: Vec::new(),
298 }
299 }
300
301 pub fn success_with_warnings(command: &str, data: Value, warnings: Vec<String>) -> Self {
303 Self {
304 status: "success".to_string(),
305 command: command.to_string(),
306 data: Some(data),
307 message: None,
308 code: None,
309 errors: Vec::new(),
310 warnings,
311 }
312 }
313
314 pub fn error(command: &str, message: &str, code: &str) -> Self {
316 Self {
317 status: "error".to_string(),
318 command: command.to_string(),
319 data: None,
320 message: Some(message.to_string()),
321 code: Some(code.to_string()),
322 errors: Vec::new(),
323 warnings: Vec::new(),
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_output_formatter_json_mode_success() {
334 let formatter = OutputFormatter::new(true, false);
335
336 let result = CommandResult::success(
337 "compile",
338 json!({
339 "files_compiled": 2,
340 "output_file": "schema.compiled.json"
341 }),
342 );
343
344 let output = formatter.format(&result);
345 assert!(!output.is_empty());
346
347 let parsed: serde_json::Value =
349 serde_json::from_str(&output).expect("Output must be valid JSON");
350 assert_eq!(parsed["status"], "success");
351 assert_eq!(parsed["command"], "compile");
352 }
353
354 #[test]
355 fn test_output_formatter_text_mode_success() {
356 let formatter = OutputFormatter::new(false, false);
357
358 let result = CommandResult::success("compile", json!({}));
359 let output = formatter.format(&result);
360
361 assert!(!output.is_empty());
362 assert!(output.contains("compile"));
363 assert!(output.contains("✓"));
364 }
365
366 #[test]
367 fn test_output_formatter_quiet_mode() {
368 let formatter = OutputFormatter::new(false, true);
369
370 let result = CommandResult::success("compile", json!({}));
371 let output = formatter.format(&result);
372
373 assert_eq!(output, "");
374 }
375
376 #[test]
377 fn test_output_formatter_json_mode_error() {
378 let formatter = OutputFormatter::new(true, false);
379
380 let result = CommandResult::error("compile", "Parse error", "PARSE_ERROR");
381
382 let output = formatter.format(&result);
383 assert!(!output.is_empty());
384
385 let parsed: serde_json::Value =
386 serde_json::from_str(&output).expect("Output must be valid JSON");
387 assert_eq!(parsed["status"], "error");
388 assert_eq!(parsed["command"], "compile");
389 assert_eq!(parsed["code"], "PARSE_ERROR");
390 }
391
392 #[test]
393 fn test_command_result_preserves_data() {
394 let data = json!({
395 "count": 42,
396 "nested": {
397 "value": "test"
398 }
399 });
400
401 let result = CommandResult::success("test", data.clone());
402
403 assert_eq!(result.data, Some(data));
405 }
406
407 #[test]
408 fn test_output_formatter_with_warnings() {
409 let formatter = OutputFormatter::new(true, false);
410
411 let result = CommandResult::success_with_warnings(
412 "compile",
413 json!({ "status": "ok" }),
414 vec!["Optimization opportunity: add index to User.id".to_string()],
415 );
416
417 let output = formatter.format(&result);
418 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Valid JSON");
419
420 assert_eq!(parsed["status"], "success");
421 assert!(parsed["warnings"].is_array());
422 }
423
424 #[test]
425 fn test_text_mode_shows_status() {
426 let formatter = OutputFormatter::new(false, false);
427
428 let result = CommandResult::success("compile", json!({}));
429 let output = formatter.format(&result);
430
431 assert!(output.to_lowercase().contains("success") || output.contains("✓"));
433 }
434
435 #[test]
436 fn test_text_mode_shows_error() {
437 let formatter = OutputFormatter::new(false, false);
438
439 let result = CommandResult::error("compile", "File not found", "FILE_NOT_FOUND");
440 let output = formatter.format(&result);
441
442 assert!(
443 output.to_lowercase().contains("error")
444 || output.contains("✗")
445 || output.contains("file")
446 );
447 }
448
449 #[test]
450 fn test_quiet_mode_suppresses_all_output() {
451 let formatter = OutputFormatter::new(false, true);
452
453 let success = CommandResult::success("compile", json!({}));
454 let error = CommandResult::error("validate", "Invalid", "INVALID");
455
456 assert_eq!(formatter.format(&success), "");
457 assert_eq!(formatter.format(&error), "");
458 }
459
460 #[test]
461 fn test_json_mode_ignores_quiet_flag() {
462 let formatter = OutputFormatter::new(true, true);
464
465 let result = CommandResult::success("compile", json!({}));
466 let output = formatter.format(&result);
467
468 let parsed: serde_json::Value =
470 serde_json::from_str(&output).expect("Should be valid JSON");
471 assert_eq!(parsed["status"], "success");
472 }
473}