Skip to main content

contract_cli/
output.rs

1use std::io::IsTerminal;
2
3use crate::error::AppError;
4
5#[derive(Clone, Copy, Debug)]
6pub enum Format {
7    Json,
8    Human,
9}
10
11impl Format {
12    pub fn detect(json_flag: bool) -> Self {
13        if json_flag || !std::io::stdout().is_terminal() {
14            Format::Json
15        } else {
16            Format::Human
17        }
18    }
19}
20
21#[derive(Clone, Copy)]
22pub struct Ctx {
23    pub format: Format,
24    pub quiet: bool,
25}
26
27impl Ctx {
28    pub fn new(json_flag: bool, quiet: bool) -> Self {
29        Self {
30            format: Format::detect(json_flag),
31            quiet,
32        }
33    }
34}
35
36/// Serialize without ever panicking or emitting invalid JSON. A payload that
37/// fails to serialize degrades to an error envelope rather than a crash.
38fn safe_json_string<T: serde::Serialize>(value: &T) -> String {
39    match serde_json::to_string_pretty(value) {
40        Ok(s) => s,
41        Err(e) => {
42            let fallback = serde_json::json!({
43                "version": "1",
44                "status": "error",
45                "error": {
46                    "code": "serialize",
47                    "message": e.to_string(),
48                    "suggestion": "Retry the command",
49                },
50            });
51            serde_json::to_string_pretty(&fallback).unwrap_or_else(|_| {
52                r#"{"version":"1","status":"error","error":{"code":"serialize","message":"serialization failed","suggestion":"Retry the command"}}"#.to_string()
53            })
54        }
55    }
56}
57
58pub fn print_success<T, F>(ctx: Ctx, data: &T, human: F)
59where
60    T: serde::Serialize,
61    F: FnOnce(&T),
62{
63    match ctx.format {
64        Format::Json => {
65            let envelope = serde_json::json!({
66                "version": "1",
67                "status": "success",
68                "data": data,
69            });
70            println!("{}", safe_json_string(&envelope));
71        }
72        Format::Human if !ctx.quiet => human(data),
73        Format::Human => {}
74    }
75}
76
77pub fn print_error(format: Format, err: &AppError) {
78    match format {
79        Format::Json => {
80            let envelope = serde_json::json!({
81                "version": "1",
82                "status": "error",
83                "error": {
84                    "code": err.error_code(),
85                    "message": err.to_string(),
86                    "suggestion": err.suggestion(),
87                },
88            });
89            eprintln!("{}", safe_json_string(&envelope));
90        }
91        Format::Human => {
92            eprintln!("error: {}", err);
93            let hint = err.suggestion();
94            if !hint.is_empty() {
95                eprintln!("  hint: {}", hint);
96            }
97        }
98    }
99}
100
101pub fn print_raw<T: serde::Serialize>(value: &T) {
102    println!("{}", safe_json_string(value));
103}
104
105/// Help / version text requested while piped: wrap in the success envelope so
106/// `contract --help | jq` parses. Informational requests always exit 0.
107pub fn print_help_envelope(text: &str) {
108    let envelope = serde_json::json!({
109        "version": "1",
110        "status": "success",
111        "data": { "usage": text },
112    });
113    println!("{}", safe_json_string(&envelope));
114}