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),
}
}
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 {
use super::*;
#[test]
fn test_output_formatter_json_mode_success() {
let formatter = OutputFormatter::new(true, false);
let result = CommandResult::success(
"compile",
json!({
"files_compiled": 2,
"output_file": "schema.compiled.json"
}),
);
let output = formatter.format(&result);
assert!(!output.is_empty());
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("Output must be valid JSON");
assert_eq!(parsed["status"], "success");
assert_eq!(parsed["command"], "compile");
}
#[test]
fn test_output_formatter_text_mode_success() {
let formatter = OutputFormatter::new(false, false);
let result = CommandResult::success("compile", json!({}));
let output = formatter.format(&result);
assert!(!output.is_empty());
assert!(output.contains("compile"));
assert!(output.contains("✓"));
}
#[test]
fn test_output_formatter_quiet_mode() {
let formatter = OutputFormatter::new(false, true);
let result = CommandResult::success("compile", json!({}));
let output = formatter.format(&result);
assert_eq!(output, "");
}
#[test]
fn test_output_formatter_json_mode_error() {
let formatter = OutputFormatter::new(true, false);
let result = CommandResult::error("compile", "Parse error", "PARSE_ERROR");
let output = formatter.format(&result);
assert!(!output.is_empty());
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("Output must be valid JSON");
assert_eq!(parsed["status"], "error");
assert_eq!(parsed["command"], "compile");
assert_eq!(parsed["code"], "PARSE_ERROR");
}
#[test]
fn test_command_result_preserves_data() {
let data = json!({
"count": 42,
"nested": {
"value": "test"
}
});
let result = CommandResult::success("test", data.clone());
assert_eq!(result.data, Some(data));
}
#[test]
fn test_output_formatter_with_warnings() {
let formatter = OutputFormatter::new(true, false);
let result = CommandResult::success_with_warnings(
"compile",
json!({ "status": "ok" }),
vec!["Optimization opportunity: add index to User.id".to_string()],
);
let output = formatter.format(&result);
let parsed: serde_json::Value = serde_json::from_str(&output).expect("Valid JSON");
assert_eq!(parsed["status"], "success");
assert!(parsed["warnings"].is_array());
}
#[test]
fn test_text_mode_shows_status() {
let formatter = OutputFormatter::new(false, false);
let result = CommandResult::success("compile", json!({}));
let output = formatter.format(&result);
assert!(output.to_lowercase().contains("success") || output.contains("✓"));
}
#[test]
fn test_text_mode_shows_error() {
let formatter = OutputFormatter::new(false, false);
let result = CommandResult::error("compile", "File not found", "FILE_NOT_FOUND");
let output = formatter.format(&result);
assert!(
output.to_lowercase().contains("error")
|| output.contains("✗")
|| output.contains("file")
);
}
#[test]
fn test_quiet_mode_suppresses_all_output() {
let formatter = OutputFormatter::new(false, true);
let success = CommandResult::success("compile", json!({}));
let error = CommandResult::error("validate", "Invalid", "INVALID");
assert_eq!(formatter.format(&success), "");
assert_eq!(formatter.format(&error), "");
}
#[test]
fn test_json_mode_ignores_quiet_flag() {
let formatter = OutputFormatter::new(true, true);
let result = CommandResult::success("compile", json!({}));
let output = formatter.format(&result);
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("Should be valid JSON");
assert_eq!(parsed["status"], "success");
}
}