feat(cli): add JsonResponse machine output formatter and pipeline helpers (GH-21) #50

Merged
KofTwentyTwo merged 1 commits from feature/GH-21-json-machine-output-pipeline into main 2026-07-23 00:50:56 +00:00
2 changed files with 79 additions and 5 deletions

No files matched your search

+8 -5
View File
@@ -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<dyn std::error::Error>> {
@@ -12,35 +15,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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");
}
+71
View File
@@ -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<T: Serialize> {
/// 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<T>,
/// Error message string (present if status is "error").
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl<T: Serialize> JsonResponse<T> {
/// 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<T: Serialize>(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""#));
}
}