gemini_cli/auth/
output.rs1use 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}