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 #[must_use]
33 pub const fn is_json(&self) -> bool {
34 self.json_mode
35 }
36
37 pub fn format(&self, result: &CommandResult) -> String {
39 match (self.json_mode, self.quiet_mode) {
40 (true, _) => serde_json::to_string(result).unwrap_or_else(|_| {
42 json!({
43 "status": "error",
44 "command": "unknown",
45 "message": "Failed to serialize response"
46 })
47 .to_string()
48 }),
49 (false, true) => String::new(),
51 (false, false) => Self::format_text(result),
53 }
54 }
55
56 pub fn progress(&self, msg: &str) {
61 if !self.quiet_mode && !self.json_mode {
62 eprintln!("{msg}");
63 }
64 }
65
66 pub fn section(&self, title: &str) {
68 self.progress(&format!("==> {title}"));
69 }
70
71 fn format_text(result: &CommandResult) -> String {
72 match result.status.as_str() {
73 "success" => {
74 let mut output = format!("✓ {} succeeded", result.command);
75
76 if !result.warnings.is_empty() {
77 output.push_str("\n\nWarnings:");
78 for warning in &result.warnings {
79 output.push_str(&format!("\n • {warning}"));
80 }
81 }
82
83 output
84 },
85 "validation-failed" => {
86 let mut output = format!("✗ {} validation failed", result.command);
87
88 if !result.errors.is_empty() {
89 output.push_str("\n\nErrors:");
90 for error in &result.errors {
91 output.push_str(&format!("\n • {error}"));
92 }
93 }
94
95 output
96 },
97 "error" => {
98 let mut output = format!("✗ {} error", result.command);
99
100 if let Some(msg) = &result.message {
101 output.push_str(&format!("\n {msg}"));
102 }
103
104 if let Some(code) = &result.code {
105 output.push_str(&format!("\n Code: {code}"));
106 }
107
108 output
109 },
110 _ => format!("? {} - unknown status: {}", result.command, result.status),
111 }
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct CommandResult {
118 pub status: String,
120
121 pub command: String,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub data: Option<Value>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub message: Option<String>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub code: Option<String>,
135
136 #[serde(skip_serializing_if = "Vec::is_empty")]
138 pub errors: Vec<String>,
139
140 #[serde(skip_serializing_if = "Vec::is_empty")]
142 pub warnings: Vec<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct CliHelp {
152 pub name: String,
154
155 pub version: String,
157
158 pub about: String,
160
161 pub global_options: Vec<ArgumentHelp>,
163
164 pub subcommands: Vec<CommandHelp>,
166
167 pub exit_codes: Vec<ExitCodeHelp>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct CommandHelp {
174 pub name: String,
176
177 pub about: String,
179
180 pub arguments: Vec<ArgumentHelp>,
182
183 pub options: Vec<ArgumentHelp>,
185
186 #[serde(skip_serializing_if = "Vec::is_empty")]
188 pub subcommands: Vec<CommandHelp>,
189
190 #[serde(skip_serializing_if = "Vec::is_empty")]
192 pub examples: Vec<String>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ArgumentHelp {
198 pub name: String,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub short: Option<String>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub long: Option<String>,
208
209 pub help: String,
211
212 pub required: bool,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub default_value: Option<String>,
218
219 pub takes_value: bool,
221
222 #[serde(skip_serializing_if = "Vec::is_empty")]
224 pub possible_values: Vec<String>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct ExitCodeHelp {
230 pub code: i32,
232
233 pub name: String,
235
236 pub description: String,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct OutputSchema {
243 pub command: String,
245
246 pub schema_version: String,
248
249 pub format: String,
251
252 pub success: serde_json::Value,
254
255 pub error: serde_json::Value,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct CommandSummary {
262 pub name: String,
264
265 pub description: String,
267
268 pub has_subcommands: bool,
270}
271
272pub fn get_exit_codes() -> Vec<ExitCodeHelp> {
274 vec![
275 ExitCodeHelp {
276 code: 0,
277 name: "success".to_string(),
278 description: "Command completed successfully".to_string(),
279 },
280 ExitCodeHelp {
281 code: 1,
282 name: "error".to_string(),
283 description: "Command failed with an error".to_string(),
284 },
285 ExitCodeHelp {
286 code: 2,
287 name: "validation_failed".to_string(),
288 description: "Validation failed (schema or input invalid)".to_string(),
289 },
290 ]
291}
292
293impl CommandResult {
294 pub fn success(command: &str, data: Value) -> Self {
296 Self {
297 status: "success".to_string(),
298 command: command.to_string(),
299 data: Some(data),
300 message: None,
301 code: None,
302 errors: Vec::new(),
303 warnings: Vec::new(),
304 }
305 }
306
307 pub fn success_with_warnings(command: &str, data: Value, warnings: Vec<String>) -> Self {
309 Self {
310 status: "success".to_string(),
311 command: command.to_string(),
312 data: Some(data),
313 message: None,
314 code: None,
315 errors: Vec::new(),
316 warnings,
317 }
318 }
319
320 pub fn error(command: &str, message: &str, code: &str) -> Self {
322 Self {
323 status: "error".to_string(),
324 command: command.to_string(),
325 data: None,
326 message: Some(message.to_string()),
327 code: Some(code.to_string()),
328 errors: Vec::new(),
329 warnings: Vec::new(),
330 }
331 }
332
333 pub fn validation_failed(command: &str, errors: Vec<String>, code: &str) -> Self {
340 Self {
341 status: "validation-failed".to_string(),
342 command: command.to_string(),
343 data: None,
344 message: None,
345 code: Some(code.to_string()),
346 errors,
347 warnings: Vec::new(),
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests;