use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Debug, Clone)]
pub struct OutputFormatter {
json_mode: bool,
quiet_mode: bool,
}
impl OutputFormatter {
pub const fn new(json_mode: bool, quiet_mode: bool) -> Self {
Self {
json_mode,
quiet_mode,
}
}
pub fn format(&self, result: &CommandResult) -> String {
match (self.json_mode, self.quiet_mode) {
(true, _) => serde_json::to_string(result).unwrap_or_else(|_| {
json!({
"status": "error",
"command": "unknown",
"message": "Failed to serialize response"
})
.to_string()
}),
(false, true) => String::new(),
(false, false) => Self::format_text(result),
}
}
pub fn progress(&self, msg: &str) {
if !self.quiet_mode && !self.json_mode {
eprintln!("{msg}");
}
}
pub fn section(&self, title: &str) {
self.progress(&format!("==> {title}"));
}
fn format_text(result: &CommandResult) -> String {
match result.status.as_str() {
"success" => {
let mut output = format!("✓ {} succeeded", result.command);
if !result.warnings.is_empty() {
output.push_str("\n\nWarnings:");
for warning in &result.warnings {
output.push_str(&format!("\n • {warning}"));
}
}
output
},
"validation-failed" => {
let mut output = format!("✗ {} validation failed", result.command);
if !result.errors.is_empty() {
output.push_str("\n\nErrors:");
for error in &result.errors {
output.push_str(&format!("\n • {error}"));
}
}
output
},
"error" => {
let mut output = format!("✗ {} error", result.command);
if let Some(msg) = &result.message {
output.push_str(&format!("\n {msg}"));
}
if let Some(code) = &result.code {
output.push_str(&format!("\n Code: {code}"));
}
output
},
_ => format!("? {} - unknown status: {}", result.command, result.status),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandResult {
pub status: String,
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliHelp {
pub name: String,
pub version: String,
pub about: String,
pub global_options: Vec<ArgumentHelp>,
pub subcommands: Vec<CommandHelp>,
pub exit_codes: Vec<ExitCodeHelp>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandHelp {
pub name: String,
pub about: String,
pub arguments: Vec<ArgumentHelp>,
pub options: Vec<ArgumentHelp>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub subcommands: Vec<CommandHelp>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgumentHelp {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub short: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub long: Option<String>,
pub help: String,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_value: Option<String>,
pub takes_value: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub possible_values: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExitCodeHelp {
pub code: i32,
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputSchema {
pub command: String,
pub schema_version: String,
pub format: String,
pub success: serde_json::Value,
pub error: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandSummary {
pub name: String,
pub description: String,
pub has_subcommands: bool,
}
pub fn get_exit_codes() -> Vec<ExitCodeHelp> {
vec![
ExitCodeHelp {
code: 0,
name: "success".to_string(),
description: "Command completed successfully".to_string(),
},
ExitCodeHelp {
code: 1,
name: "error".to_string(),
description: "Command failed with an error".to_string(),
},
ExitCodeHelp {
code: 2,
name: "validation_failed".to_string(),
description: "Validation failed (schema or input invalid)".to_string(),
},
]
}
impl CommandResult {
pub fn success(command: &str, data: Value) -> Self {
Self {
status: "success".to_string(),
command: command.to_string(),
data: Some(data),
message: None,
code: None,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn success_with_warnings(command: &str, data: Value, warnings: Vec<String>) -> Self {
Self {
status: "success".to_string(),
command: command.to_string(),
data: Some(data),
message: None,
code: None,
errors: Vec::new(),
warnings,
}
}
pub fn error(command: &str, message: &str, code: &str) -> Self {
Self {
status: "error".to_string(),
command: command.to_string(),
data: None,
message: Some(message.to_string()),
code: Some(code.to_string()),
errors: Vec::new(),
warnings: Vec::new(),
}
}
}
#[cfg(test)]
mod tests;