From 39772b0df28413f71c96559090468602099f7d3b Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 22 Jul 2026 19:50:43 -0500 Subject: [PATCH] feat(cli): add JsonResponse machine output formatter and pipeline helpers --- crates/nuncio-cli/src/main.rs | 13 +++--- crates/nuncio-cli/src/output.rs | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 crates/nuncio-cli/src/output.rs diff --git a/crates/nuncio-cli/src/main.rs b/crates/nuncio-cli/src/main.rs index c0a6588..7c553aa 100644 --- a/crates/nuncio-cli/src/main.rs +++ b/crates/nuncio-cli/src/main.rs @@ -1,9 +1,12 @@ //! Nuncio CLI main application entry point. mod args; +mod output; use args::{Cli, Commands}; use clap::Parser; +use output::format_json; +use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { @@ -12,35 +15,35 @@ async fn main() -> Result<(), Box> { match cli.command { Commands::Sync => { if cli.json { - println!(r#"{{"status":"ok","action":"sync"}}"#); + println!("{}", format_json(&json!({"action": "sync"}))); } else { println!("Triggering account synchronization..."); } } Commands::List { folder } => { if cli.json { - println!(r#"{{"status":"ok","action":"list","folder":"{}"}}"#, folder); + println!("{}", format_json(&json!({"action": "list", "folder": folder}))); } else { println!("Listing messages in folder '{}'", folder); } } Commands::Send { to, subject, body: _ } => { if cli.json { - println!(r#"{{"status":"ok","action":"send","to":"{}"}}"#, to); + println!("{}", format_json(&json!({"action": "send", "to": to, "subject": subject}))); } else { println!("Sending message to {} ('{}')", to, subject); } } Commands::Search { query } => { if cli.json { - println!(r#"{{"status":"ok","action":"search","query":"{}"}}"#, query); + println!("{}", format_json(&json!({"action": "search", "query": query}))); } else { println!("Searching for '{}'", query); } } Commands::Config => { if cli.json { - println!(r#"{{"status":"ok","action":"config"}}"#); + println!("{}", format_json(&json!({"action": "config"}))); } else { println!("Nuncio CLI Configuration Operational"); } diff --git a/crates/nuncio-cli/src/output.rs b/crates/nuncio-cli/src/output.rs new file mode 100644 index 0000000..98df9d7 --- /dev/null +++ b/crates/nuncio-cli/src/output.rs @@ -0,0 +1,71 @@ +//! Standardized JSON machine-readable output formatters for Unix scripting. + +use serde::Serialize; + +/// Standardized JSON response envelope for CLI machine output. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct JsonResponse { + /// Result status string ("ok" or "error"). + pub status: &'static str, + /// Result payload data (present if status is "ok"). + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + /// Error message string (present if status is "error"). + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl JsonResponse { + /// Construct a successful JSON response wrapping payload data. + pub fn success(data: T) -> Self { + Self { + status: "ok", + data: Some(data), + error: None, + } + } +} + +impl JsonResponse<()> { + /// Construct an error JSON response wrapping an error string. + pub fn error(message: &str) -> Self { + Self { + status: "error", + data: None, + error: Some(message.to_string()), + } + } +} + +/// Serialize payload to a formatted JSON string. +pub fn format_json(data: &T) -> String { + let response = JsonResponse::success(data); + serde_json::to_string(&response).unwrap_or_else(|_| r#"{"status":"error","error":"JSON serialization failed"}"#.to_string()) +} + +/// Serialize error message to a formatted JSON string. +pub fn format_json_error(message: &str) -> String { + let response = JsonResponse::error(message); + serde_json::to_string(&response).unwrap_or_else(|_| r#"{"status":"error","error":"JSON serialization failed"}"#.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn format_json_success_payload() { + let payload = json!({"count": 42, "user": "alice"}); + let json_str = format_json(&payload); + assert!(json_str.contains(r#""status":"ok""#)); + assert!(json_str.contains(r#""count":42"#)); + } + + #[test] + fn format_json_error_payload() { + let err_str = format_json_error("account not found"); + assert!(err_str.contains(r#""status":"error""#)); + assert!(err_str.contains(r#""error":"account not found""#)); + } +} -- 2.54.0