Skip to main content

gemini_cli/auth/
output.rs

1use std::io;
2
3pub const AUTH_SCHEMA_VERSION: &str = "gemini-cli.auth.v1";
4
5#[derive(Clone, Debug)]
6pub enum JsonValue {
7    Null,
8    Bool(bool),
9    Number(i64),
10    String(String),
11    Array(Vec<JsonValue>),
12    Object(Vec<(String, JsonValue)>),
13}
14
15impl JsonValue {
16    pub fn to_json_string(&self) -> String {
17        let mut out = String::new();
18        self.write_json(&mut out);
19        out
20    }
21
22    fn write_json(&self, out: &mut String) {
23        match self {
24            JsonValue::Null => out.push_str("null"),
25            JsonValue::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
26            JsonValue::Number(value) => out.push_str(&value.to_string()),
27            JsonValue::String(value) => {
28                out.push('"');
29                out.push_str(&escape_json(value));
30                out.push('"');
31            }
32            JsonValue::Array(values) => {
33                out.push('[');
34                for (index, value) in values.iter().enumerate() {
35                    if index > 0 {
36                        out.push(',');
37                    }
38                    value.write_json(out);
39                }
40                out.push(']');
41            }
42            JsonValue::Object(fields) => {
43                out.push('{');
44                for (index, (key, value)) in fields.iter().enumerate() {
45                    if index > 0 {
46                        out.push(',');
47                    }
48                    out.push('"');
49                    out.push_str(&escape_json(key));
50                    out.push_str("\":");
51                    value.write_json(out);
52                }
53                out.push('}');
54            }
55        }
56    }
57}
58
59pub fn s(value: impl Into<String>) -> JsonValue {
60    JsonValue::String(value.into())
61}
62
63pub fn b(value: bool) -> JsonValue {
64    JsonValue::Bool(value)
65}
66
67pub fn n(value: i64) -> JsonValue {
68    JsonValue::Number(value)
69}
70
71pub fn null() -> JsonValue {
72    JsonValue::Null
73}
74
75pub fn arr(values: Vec<JsonValue>) -> JsonValue {
76    JsonValue::Array(values)
77}
78
79pub fn obj(fields: Vec<(&str, JsonValue)>) -> JsonValue {
80    JsonValue::Object(
81        fields
82            .into_iter()
83            .map(|(key, value)| (key.to_string(), value))
84            .collect(),
85    )
86}
87
88pub fn obj_dynamic(fields: Vec<(String, JsonValue)>) -> JsonValue {
89    JsonValue::Object(fields)
90}
91
92pub fn emit_result(command: &str, result: JsonValue) -> io::Result<()> {
93    println!("{}", render_result(command, result));
94    Ok(())
95}
96
97pub fn emit_error(
98    command: &str,
99    code: &str,
100    message: impl Into<String>,
101    details: Option<JsonValue>,
102) -> io::Result<()> {
103    println!("{}", render_error(command, code, message, details));
104    Ok(())
105}
106
107pub fn render_result(command: &str, result: JsonValue) -> String {
108    obj(vec![
109        ("schema_version", s(AUTH_SCHEMA_VERSION)),
110        ("command", s(command)),
111        ("ok", b(true)),
112        ("result", result),
113    ])
114    .to_json_string()
115}
116
117pub fn render_error(
118    command: &str,
119    code: &str,
120    message: impl Into<String>,
121    details: Option<JsonValue>,
122) -> String {
123    let mut error_fields = vec![
124        ("code".to_string(), s(code)),
125        ("message".to_string(), s(message)),
126    ];
127    if let Some(details) = details {
128        error_fields.push(("details".to_string(), details));
129    }
130
131    obj_dynamic(vec![
132        ("schema_version".to_string(), s(AUTH_SCHEMA_VERSION)),
133        ("command".to_string(), s(command)),
134        ("ok".to_string(), b(false)),
135        ("error".to_string(), JsonValue::Object(error_fields)),
136    ])
137    .to_json_string()
138}
139
140fn escape_json(raw: &str) -> String {
141    let mut escaped = String::with_capacity(raw.len());
142    for ch in raw.chars() {
143        match ch {
144            '"' => escaped.push_str("\\\""),
145            '\\' => escaped.push_str("\\\\"),
146            '\u{08}' => escaped.push_str("\\b"),
147            '\u{0C}' => escaped.push_str("\\f"),
148            '\n' => escaped.push_str("\\n"),
149            '\r' => escaped.push_str("\\r"),
150            '\t' => escaped.push_str("\\t"),
151            ch if ch.is_control() => escaped.push_str(&format!("\\u{:04x}", ch as u32)),
152            ch => escaped.push(ch),
153        }
154    }
155    escaped
156}
157
158#[cfg(test)]
159mod tests {
160    use super::{obj, render_error, render_result};
161
162    #[test]
163    fn render_result_contains_schema_and_command() {
164        let rendered = render_result("auth login", obj(vec![("completed", super::b(true))]));
165        assert!(rendered.contains("\"schema_version\":\"gemini-cli.auth.v1\""));
166        assert!(rendered.contains("\"command\":\"auth login\""));
167        assert!(rendered.contains("\"ok\":true"));
168    }
169
170    #[test]
171    fn render_error_omits_details_when_absent() {
172        let rendered = render_error("auth save", "invalid-usage", "bad", None);
173        assert!(rendered.contains("\"ok\":false"));
174        assert!(!rendered.contains("\"details\""));
175    }
176
177    #[test]
178    fn render_error_escapes_strings() {
179        let rendered = render_error("auth use", "invalid", "a\"b", None);
180        assert!(rendered.contains("a\\\"b"));
181    }
182}