1use std::io::Write;
5
6use serde::Serialize;
7use serde_json::Value;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
15pub enum OutputError {
16 #[error("invalid output mode: cannot combine --json and --plain")]
17 ConflictingFlags,
18
19 #[error("serialize value: {0}")]
20 Serialize(#[from] serde_json::Error),
21
22 #[error("write output: {0}")]
23 Io(#[from] std::io::Error),
24}
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub enum OutputMode {
32 #[default]
33 Text,
34 Json,
35 Plain, }
37
38#[derive(Debug, Clone, Default)]
43pub struct OutputConfig {
44 pub mode: OutputMode,
45 pub results_only: bool,
46 pub select_fields: Vec<String>,
47}
48
49impl OutputConfig {
50 pub fn from_flags(json: bool, plain: bool) -> Result<Self, String> {
53 if json && plain {
54 return Err("invalid output mode (cannot combine --json and --plain)".to_string());
55 }
56 let mode = if json {
57 OutputMode::Json
58 } else if plain {
59 OutputMode::Plain
60 } else {
61 OutputMode::Text
62 };
63 Ok(OutputConfig {
64 mode,
65 results_only: false,
66 select_fields: Vec::new(),
67 })
68 }
69
70 pub fn is_json(&self) -> bool {
71 self.mode == OutputMode::Json
72 }
73
74 pub fn is_plain(&self) -> bool {
75 self.mode == OutputMode::Plain
76 }
77}
78
79pub fn write_json<W: Write>(
85 w: &mut W,
86 value: &impl Serialize,
87 config: &OutputConfig,
88) -> Result<(), OutputError> {
89 let mut v: Value = serde_json::to_value(value)?;
91
92 if config.results_only {
93 v = unwrap_primary(v);
94 }
95
96 if !config.select_fields.is_empty() {
97 v = select_fields(v, &config.select_fields);
98 }
99
100 let s = serde_json::to_string_pretty(&v)?;
102 w.write_all(s.as_bytes())?;
103 w.write_all(b"\n")?;
105 Ok(())
106}
107
108fn unwrap_primary(v: Value) -> Value {
121 let m = match v {
122 Value::Object(ref map) => map.clone(),
123 other => return other,
124 };
125
126 if let Some(results) = m.get("results") {
128 return results.clone();
129 }
130
131 const META: &[&str] = &[
133 "nextPageToken",
134 "next_cursor",
135 "has_more",
136 "count",
137 "query",
138 "dry_run",
139 "dryRun",
140 "op",
141 "action",
142 "note",
143 "notes",
144 ];
145
146 let candidates: Vec<&str> = m
147 .keys()
148 .filter(|k| !META.contains(&k.as_str()))
149 .map(|k| k.as_str())
150 .collect();
151
152 if candidates.len() == 1 {
154 return m[candidates[0]].clone();
155 }
156
157 for k in &candidates {
159 if m[*k].is_array() {
160 return m[*k].clone();
161 }
162 }
163
164 const KNOWN: &[&str] = &[
166 "files",
167 "threads",
168 "messages",
169 "labels",
170 "events",
171 "calendars",
172 "courses",
173 "topics",
174 "announcements",
175 "materials",
176 "coursework",
177 "submissions",
178 "invitations",
179 "guardians",
180 "notes",
181 "contacts",
182 "people",
183 "tasks",
184 "lists",
185 "groups",
186 "members",
187 "drives",
188 "rules",
189 "colors",
190 "spaces",
191 "request",
192 ];
193 for k in KNOWN {
194 if let Some(val) = m.get(*k) {
195 return val.clone();
196 }
197 }
198
199 v
201}
202
203fn select_fields(v: Value, fields: &[String]) -> Value {
210 match v {
211 Value::Array(arr) => {
212 let projected = arr
213 .into_iter()
214 .map(|item| select_fields_from_item(item, fields))
215 .collect();
216 Value::Array(projected)
217 }
218 other => select_fields_from_item(other, fields),
219 }
220}
221
222fn select_fields_from_item(v: Value, fields: &[String]) -> Value {
223 let m = match v {
224 Value::Object(map) => map,
225 other => return other,
226 };
227
228 let mut out = serde_json::Map::new();
229 for f in fields {
230 let tmp = Value::Object(m.clone());
232 if let Some(val) = get_at_path(&tmp, f) {
233 out.insert(f.clone(), val);
234 }
235 }
236 Value::Object(out)
237}
238
239fn get_at_path(v: &Value, path: &str) -> Option<Value> {
246 let path = path.trim();
247 if path.is_empty() {
248 return None;
249 }
250
251 let mut cur = v;
252 let mut _owned: Value;
254
255 let segs: Vec<&str> = path.split('.').collect();
256 let last = segs.len() - 1;
257
258 for (i, seg) in segs.iter().enumerate() {
259 let seg = seg.trim();
260 if seg.is_empty() {
261 return None;
262 }
263
264 match cur {
265 Value::Object(map) => {
266 let next = map.get(seg)?;
267 if i == last {
268 return Some(next.clone());
269 }
270 _owned = next.clone();
271 cur = &_owned;
272 }
273 Value::Array(arr) => {
274 let idx: usize = seg.parse().ok()?;
275 let next = arr.get(idx)?;
276 if i == last {
277 return Some(next.clone());
278 }
279 _owned = next.clone();
280 cur = &_owned;
281 }
282 _ => return None,
283 }
284 }
285
286 None
287}
288
289pub fn write_table<W: Write>(w: &mut W, rows: &[Vec<String>]) -> Result<(), OutputError> {
297 if rows.is_empty() {
298 return Ok(());
299 }
300
301 let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
303 let mut widths = vec![0usize; num_cols];
304 for row in rows {
305 for (i, cell) in row.iter().enumerate() {
306 if i < num_cols {
307 widths[i] = widths[i].max(cell.len());
308 }
309 }
310 }
311
312 for row in rows {
313 let last_col = row.len().saturating_sub(1);
314 for (i, cell) in row.iter().enumerate() {
315 if i == last_col {
316 write!(w, "{}", cell)?;
318 } else {
319 write!(w, "{:<width$} ", cell, width = widths[i])?;
321 }
322 }
323 writeln!(w)?;
324 }
325 Ok(())
326}
327
328pub fn write_tsv<W: Write>(w: &mut W, rows: &[Vec<String>]) -> Result<(), OutputError> {
330 for row in rows {
331 let line = row.join("\t");
332 writeln!(w, "{}", line)?;
333 }
334 Ok(())
335}
336
337#[cfg(test)]
342mod tests {
343 use super::*;
344 use serde_json::json;
345
346 #[test]
351 fn test_output_config_from_flags_json() {
352 let cfg = OutputConfig::from_flags(true, false).unwrap();
353 assert_eq!(cfg.mode, OutputMode::Json);
354 assert!(cfg.is_json());
355 assert!(!cfg.is_plain());
356 }
357
358 #[test]
359 fn test_output_config_from_flags_plain() {
360 let cfg = OutputConfig::from_flags(false, true).unwrap();
361 assert_eq!(cfg.mode, OutputMode::Plain);
362 assert!(!cfg.is_json());
363 assert!(cfg.is_plain());
364 }
365
366 #[test]
367 fn test_output_config_from_flags_default() {
368 let cfg = OutputConfig::from_flags(false, false).unwrap();
369 assert_eq!(cfg.mode, OutputMode::Text);
370 assert!(!cfg.is_json());
371 assert!(!cfg.is_plain());
372 }
373
374 #[test]
375 fn test_output_config_from_flags_both_error() {
376 let err = OutputConfig::from_flags(true, true).unwrap_err();
377 assert!(err.contains("cannot combine"));
378 }
379
380 #[test]
385 fn test_unwrap_primary_results_key() {
386 let v = json!({
387 "results": [1, 2, 3],
388 "nextPageToken": "abc"
389 });
390 let out = unwrap_primary(v);
391 assert_eq!(out, json!([1, 2, 3]));
392 }
393
394 #[test]
395 fn test_unwrap_primary_single_candidate() {
396 let v = json!({
397 "messages": [{"id": "1"}, {"id": "2"}],
398 "nextPageToken": "tok"
399 });
400 let out = unwrap_primary(v);
401 assert_eq!(out, json!([{"id": "1"}, {"id": "2"}]));
402 }
403
404 #[test]
405 fn test_unwrap_primary_array_preference() {
406 let v = json!({
409 "label": "hello",
410 "items": [1, 2, 3]
411 });
412 let out = unwrap_primary(v);
413 assert_eq!(out, json!([1, 2, 3]));
414 }
415
416 #[test]
417 fn test_unwrap_primary_passthrough() {
418 let v = json!([10, 20, 30]);
420 let out = unwrap_primary(v.clone());
421 assert_eq!(out, v);
422
423 let v2 = json!("just a string");
424 let out2 = unwrap_primary(v2.clone());
425 assert_eq!(out2, v2);
426 }
427
428 #[test]
433 fn test_select_fields_flat() {
434 let v = json!({"id": "1", "name": "Alice", "email": "alice@example.com"});
435 let fields = vec!["id".to_string(), "name".to_string()];
436 let out = select_fields(v, &fields);
437 assert_eq!(out, json!({"id": "1", "name": "Alice"}));
438 }
439
440 #[test]
441 fn test_select_fields_array() {
442 let v = json!([
443 {"id": "1", "name": "Alice", "email": "a@b.com"},
444 {"id": "2", "name": "Bob", "email": "b@b.com"}
445 ]);
446 let fields = vec!["id".to_string(), "name".to_string()];
447 let out = select_fields(v, &fields);
448 assert_eq!(
449 out,
450 json!([
451 {"id": "1", "name": "Alice"},
452 {"id": "2", "name": "Bob"}
453 ])
454 );
455 }
456
457 #[test]
462 fn test_get_at_path_nested() {
463 let v = json!({"user": {"name": "Alice"}});
464 let result = get_at_path(&v, "user.name");
465 assert_eq!(result, Some(json!("Alice")));
466 }
467
468 #[test]
469 fn test_get_at_path_missing() {
470 let v = json!({"user": {"name": "Alice"}});
471 let result = get_at_path(&v, "user.email");
472 assert_eq!(result, None);
473 }
474
475 #[test]
480 fn test_write_json_basic() {
481 let cfg = OutputConfig::default();
482 let value = json!({"hello": "world"});
483 let mut buf = Vec::new();
484 write_json(&mut buf, &value, &cfg).unwrap();
485 let output = String::from_utf8(buf).unwrap();
486 assert!(output.contains("\"hello\""));
488 assert!(output.contains("\"world\""));
489 assert!(output.ends_with('\n'));
490 let _: Value = serde_json::from_str(output.trim()).unwrap();
492 }
493
494 #[test]
495 fn test_write_json_results_only() {
496 let cfg = OutputConfig {
497 mode: OutputMode::Json,
498 results_only: true,
499 select_fields: Vec::new(),
500 };
501 let value = json!({
502 "results": [{"id": "1"}, {"id": "2"}],
503 "nextPageToken": "token"
504 });
505 let mut buf = Vec::new();
506 write_json(&mut buf, &value, &cfg).unwrap();
507 let output = String::from_utf8(buf).unwrap();
508 let parsed: Value = serde_json::from_str(output.trim()).unwrap();
509 assert!(parsed.is_array());
511 assert_eq!(parsed.as_array().unwrap().len(), 2);
512 }
513}