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 fn format_text(result: &CommandResult) -> String {
51 match result.status.as_str() {
52 "success" => {
53 let mut output = format!("✓ {} succeeded", result.command);
54
55 if !result.warnings.is_empty() {
56 output.push_str("\n\nWarnings:");
57 for warning in &result.warnings {
58 output.push_str(&format!("\n • {warning}"));
59 }
60 }
61
62 output
63 },
64 "validation-failed" => {
65 let mut output = format!("✗ {} validation failed", result.command);
66
67 if !result.errors.is_empty() {
68 output.push_str("\n\nErrors:");
69 for error in &result.errors {
70 output.push_str(&format!("\n • {error}"));
71 }
72 }
73
74 output
75 },
76 "error" => {
77 let mut output = format!("✗ {} error", result.command);
78
79 if let Some(msg) = &result.message {
80 output.push_str(&format!("\n {msg}"));
81 }
82
83 if let Some(code) = &result.code {
84 output.push_str(&format!("\n Code: {code}"));
85 }
86
87 output
88 },
89 _ => format!("? {} - unknown status: {}", result.command, result.status),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CommandResult {
97 pub status: String,
99
100 pub command: String,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub data: Option<Value>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub message: Option<String>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub code: Option<String>,
114
115 #[serde(skip_serializing_if = "Vec::is_empty")]
117 pub errors: Vec<String>,
118
119 #[serde(skip_serializing_if = "Vec::is_empty")]
121 pub warnings: Vec<String>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct CliHelp {
131 pub name: String,
133
134 pub version: String,
136
137 pub about: String,
139
140 pub global_options: Vec<ArgumentHelp>,
142
143 pub subcommands: Vec<CommandHelp>,
145
146 pub exit_codes: Vec<ExitCodeHelp>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct CommandHelp {
153 pub name: String,
155
156 pub about: String,
158
159 pub arguments: Vec<ArgumentHelp>,
161
162 pub options: Vec<ArgumentHelp>,
164
165 #[serde(skip_serializing_if = "Vec::is_empty")]
167 pub subcommands: Vec<CommandHelp>,
168
169 #[serde(skip_serializing_if = "Vec::is_empty")]
171 pub examples: Vec<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ArgumentHelp {
177 pub name: String,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub short: Option<String>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub long: Option<String>,
187
188 pub help: String,
190
191 pub required: bool,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub default_value: Option<String>,
197
198 pub takes_value: bool,
200
201 #[serde(skip_serializing_if = "Vec::is_empty")]
203 pub possible_values: Vec<String>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ExitCodeHelp {
209 pub code: i32,
211
212 pub name: String,
214
215 pub description: String,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct OutputSchema {
222 pub command: String,
224
225 pub schema_version: String,
227
228 pub format: String,
230
231 pub success: serde_json::Value,
233
234 pub error: serde_json::Value,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct CommandSummary {
241 pub name: String,
243
244 pub description: String,
246
247 pub has_subcommands: bool,
249}
250
251pub fn get_exit_codes() -> Vec<ExitCodeHelp> {
253 vec![
254 ExitCodeHelp {
255 code: 0,
256 name: "success".to_string(),
257 description: "Command completed successfully".to_string(),
258 },
259 ExitCodeHelp {
260 code: 1,
261 name: "error".to_string(),
262 description: "Command failed with an error".to_string(),
263 },
264 ExitCodeHelp {
265 code: 2,
266 name: "validation_failed".to_string(),
267 description: "Validation failed (schema or input invalid)".to_string(),
268 },
269 ]
270}
271
272impl CommandResult {
273 pub fn success(command: &str, data: Value) -> Self {
275 Self {
276 status: "success".to_string(),
277 command: command.to_string(),
278 data: Some(data),
279 message: None,
280 code: None,
281 errors: Vec::new(),
282 warnings: Vec::new(),
283 }
284 }
285
286 pub fn success_with_warnings(command: &str, data: Value, warnings: Vec<String>) -> Self {
288 Self {
289 status: "success".to_string(),
290 command: command.to_string(),
291 data: Some(data),
292 message: None,
293 code: None,
294 errors: Vec::new(),
295 warnings,
296 }
297 }
298
299 pub fn error(command: &str, message: &str, code: &str) -> Self {
301 Self {
302 status: "error".to_string(),
303 command: command.to_string(),
304 data: None,
305 message: Some(message.to_string()),
306 code: Some(code.to_string()),
307 errors: Vec::new(),
308 warnings: Vec::new(),
309 }
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_output_formatter_json_mode_success() {
319 let formatter = OutputFormatter::new(true, false);
320
321 let result = CommandResult::success(
322 "compile",
323 json!({
324 "files_compiled": 2,
325 "output_file": "schema.compiled.json"
326 }),
327 );
328
329 let output = formatter.format(&result);
330 assert!(!output.is_empty());
331
332 let parsed: serde_json::Value =
334 serde_json::from_str(&output).expect("Output must be valid JSON");
335 assert_eq!(parsed["status"], "success");
336 assert_eq!(parsed["command"], "compile");
337 }
338
339 #[test]
340 fn test_output_formatter_text_mode_success() {
341 let formatter = OutputFormatter::new(false, false);
342
343 let result = CommandResult::success("compile", json!({}));
344 let output = formatter.format(&result);
345
346 assert!(!output.is_empty());
347 assert!(output.contains("compile"));
348 assert!(output.contains("✓"));
349 }
350
351 #[test]
352 fn test_output_formatter_quiet_mode() {
353 let formatter = OutputFormatter::new(false, true);
354
355 let result = CommandResult::success("compile", json!({}));
356 let output = formatter.format(&result);
357
358 assert_eq!(output, "");
359 }
360
361 #[test]
362 fn test_output_formatter_json_mode_error() {
363 let formatter = OutputFormatter::new(true, false);
364
365 let result = CommandResult::error("compile", "Parse error", "PARSE_ERROR");
366
367 let output = formatter.format(&result);
368 assert!(!output.is_empty());
369
370 let parsed: serde_json::Value =
371 serde_json::from_str(&output).expect("Output must be valid JSON");
372 assert_eq!(parsed["status"], "error");
373 assert_eq!(parsed["command"], "compile");
374 assert_eq!(parsed["code"], "PARSE_ERROR");
375 }
376
377 #[test]
378 fn test_command_result_preserves_data() {
379 let data = json!({
380 "count": 42,
381 "nested": {
382 "value": "test"
383 }
384 });
385
386 let result = CommandResult::success("test", data.clone());
387
388 assert_eq!(result.data, Some(data));
390 }
391
392 #[test]
393 fn test_output_formatter_with_warnings() {
394 let formatter = OutputFormatter::new(true, false);
395
396 let result = CommandResult::success_with_warnings(
397 "compile",
398 json!({ "status": "ok" }),
399 vec!["Optimization opportunity: add index to User.id".to_string()],
400 );
401
402 let output = formatter.format(&result);
403 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Valid JSON");
404
405 assert_eq!(parsed["status"], "success");
406 assert!(parsed["warnings"].is_array());
407 }
408
409 #[test]
410 fn test_text_mode_shows_status() {
411 let formatter = OutputFormatter::new(false, false);
412
413 let result = CommandResult::success("compile", json!({}));
414 let output = formatter.format(&result);
415
416 assert!(output.to_lowercase().contains("success") || output.contains("✓"));
418 }
419
420 #[test]
421 fn test_text_mode_shows_error() {
422 let formatter = OutputFormatter::new(false, false);
423
424 let result = CommandResult::error("compile", "File not found", "FILE_NOT_FOUND");
425 let output = formatter.format(&result);
426
427 assert!(
428 output.to_lowercase().contains("error")
429 || output.contains("✗")
430 || output.contains("file")
431 );
432 }
433
434 #[test]
435 fn test_quiet_mode_suppresses_all_output() {
436 let formatter = OutputFormatter::new(false, true);
437
438 let success = CommandResult::success("compile", json!({}));
439 let error = CommandResult::error("validate", "Invalid", "INVALID");
440
441 assert_eq!(formatter.format(&success), "");
442 assert_eq!(formatter.format(&error), "");
443 }
444
445 #[test]
446 fn test_json_mode_ignores_quiet_flag() {
447 let formatter = OutputFormatter::new(true, true);
449
450 let result = CommandResult::success("compile", json!({}));
451 let output = formatter.format(&result);
452
453 let parsed: serde_json::Value =
455 serde_json::from_str(&output).expect("Should be valid JSON");
456 assert_eq!(parsed["status"], "success");
457 }
458}