1use std::io::Write;
16
17use anyhow::{Context, Result};
18use clap::ValueEnum;
19use serde::Serialize;
20
21#[derive(Clone, Debug, Default, ValueEnum)]
23pub enum OutputFormat {
24 #[default]
26 Table,
27 Json,
29 Yaml,
31 Yamls,
33 Jsonl,
35}
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
43#[value(rename_all = "lowercase")]
44pub enum TableOrJson {
45 #[default]
47 Table,
48 Json,
50}
51
52pub trait JsonlSerialize {
58 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()>;
60}
61
62pub fn write_items_jsonl<'a, I, T>(items: I, out: &mut dyn Write) -> Result<()>
64where
65 I: IntoIterator<Item = &'a T>,
66 T: Serialize + 'a,
67{
68 for item in items {
69 let line = serde_json::to_string(item).context("Failed to serialize as JSON")?;
70 writeln!(out, "{line}").context("Failed to write JSONL line")?;
71 }
72 Ok(())
73}
74
75pub fn write_scalar_jsonl<T: Serialize>(item: &T, out: &mut dyn Write) -> Result<()> {
77 let line = serde_json::to_string(item).context("Failed to serialize as JSON")?;
78 writeln!(out, "{line}").context("Failed to write JSONL line")?;
79 Ok(())
80}
81
82impl<T: Serialize> JsonlSerialize for Vec<T> {
83 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
84 write_items_jsonl(self.iter(), out)
85 }
86}
87
88pub fn write_output<T: Serialize + JsonlSerialize>(
93 data: &T,
94 format: &OutputFormat,
95 out: &mut dyn Write,
96) -> Result<bool> {
97 match format {
98 OutputFormat::Table => Ok(false),
99 OutputFormat::Json => {
100 let rendered =
101 serde_json::to_string_pretty(data).context("Failed to serialize as JSON")?;
102 writeln!(out, "{rendered}").context("Failed to write JSON output")?;
103 Ok(true)
104 }
105 OutputFormat::Yaml => {
106 let rendered = serde_yaml::to_string(data).context("Failed to serialize as YAML")?;
107 write!(out, "{rendered}").context("Failed to write YAML output")?;
108 Ok(true)
109 }
110 OutputFormat::Yamls => {
111 let rendered = format_yaml_stream(data)?;
112 write!(out, "{rendered}").context("Failed to write YAML stream output")?;
113 Ok(true)
114 }
115 OutputFormat::Jsonl => {
116 data.write_jsonl(out)?;
117 Ok(true)
118 }
119 }
120}
121
122fn yaml_stream_doc(value: &serde_yaml::Value) -> Result<String> {
124 let s = serde_yaml::to_string(value).context("Failed to serialize YAML stream item")?;
125 Ok(format!("---\n{s}"))
126}
127
128fn format_yaml_stream<T: Serialize>(data: &T) -> Result<String> {
134 match serde_yaml::to_value(data).context("Failed to serialize as YAML stream")? {
135 serde_yaml::Value::Sequence(items) => items.iter().map(yaml_stream_doc).collect(),
136 other => yaml_stream_doc(&other),
137 }
138}
139
140pub fn output_as<T: Serialize + JsonlSerialize>(data: &T, format: &OutputFormat) -> Result<bool> {
144 let stdout = std::io::stdout();
145 let mut handle = stdout.lock();
146 write_output(data, format, &mut handle)
147}
148
149#[cfg(test)]
150#[allow(clippy::unwrap_used)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn output_default_is_table() {
156 assert!(matches!(OutputFormat::default(), OutputFormat::Table));
157 }
158
159 #[test]
160 fn output_json_variant() {
161 assert!(matches!(OutputFormat::Json, OutputFormat::Json));
162 }
163
164 #[test]
165 fn output_yaml_variant() {
166 assert!(matches!(OutputFormat::Yaml, OutputFormat::Yaml));
167 }
168
169 #[test]
170 fn output_yamls_variant() {
171 assert!(matches!(OutputFormat::Yamls, OutputFormat::Yamls));
172 }
173
174 #[test]
175 fn output_jsonl_variant() {
176 assert!(matches!(OutputFormat::Jsonl, OutputFormat::Jsonl));
177 }
178
179 #[test]
180 fn output_debug_format() {
181 assert_eq!(format!("{:?}", OutputFormat::Jsonl), "Jsonl");
182 }
183
184 #[test]
185 fn output_clone() {
186 let format = OutputFormat::Jsonl;
187 let cloned = format;
188 assert!(matches!(cloned, OutputFormat::Jsonl));
189 }
190
191 #[test]
194 fn output_as_table_returns_false() {
195 let data = vec![1, 2, 3];
196 assert!(!output_as(&data, &OutputFormat::Table).unwrap());
197 }
198
199 #[test]
200 fn output_as_json_returns_true() {
201 let data = vec![1, 2, 3];
202 assert!(output_as(&data, &OutputFormat::Json).unwrap());
203 }
204
205 #[test]
206 fn output_as_yaml_returns_true() {
207 let data = vec![1, 2, 3];
208 assert!(output_as(&data, &OutputFormat::Yaml).unwrap());
209 }
210
211 #[test]
212 fn output_as_yamls_returns_true() {
213 let data = vec![1, 2, 3];
214 assert!(output_as(&data, &OutputFormat::Yamls).unwrap());
215 }
216
217 #[test]
218 fn output_as_jsonl_returns_true() {
219 let data = vec![1, 2, 3];
220 assert!(output_as(&data, &OutputFormat::Jsonl).unwrap());
221 }
222
223 #[test]
226 fn vec_jsonl_empty_emits_nothing() {
227 let data: Vec<i32> = vec![];
228 let mut buf = Vec::new();
229 data.write_jsonl(&mut buf).unwrap();
230 assert_eq!(buf, b"");
231 }
232
233 #[test]
234 fn vec_jsonl_emits_one_line_per_item() {
235 let data = vec![1_i32, 2, 3];
236 let mut buf = Vec::new();
237 data.write_jsonl(&mut buf).unwrap();
238 assert_eq!(String::from_utf8(buf).unwrap(), "1\n2\n3\n");
239 }
240
241 #[test]
242 fn vec_jsonl_emits_compact_objects() {
243 #[derive(Serialize)]
244 struct Item {
245 key: &'static str,
246 val: u32,
247 }
248 let data = vec![Item { key: "a", val: 1 }, Item { key: "b", val: 2 }];
249 let mut buf = Vec::new();
250 data.write_jsonl(&mut buf).unwrap();
251 let out = String::from_utf8(buf).unwrap();
252 assert_eq!(
253 out,
254 "{\"key\":\"a\",\"val\":1}\n{\"key\":\"b\",\"val\":2}\n"
255 );
256 }
257
258 #[test]
259 fn write_items_jsonl_over_slice() {
260 let data = [10_i32, 20];
261 let mut buf = Vec::new();
262 write_items_jsonl(data.iter(), &mut buf).unwrap();
263 assert_eq!(String::from_utf8(buf).unwrap(), "10\n20\n");
264 }
265
266 #[test]
267 fn write_scalar_jsonl_emits_one_line() {
268 #[derive(Serialize)]
269 struct Scalar {
270 name: &'static str,
271 }
272 let item = Scalar { name: "solo" };
273 let mut buf = Vec::new();
274 write_scalar_jsonl(&item, &mut buf).unwrap();
275 assert_eq!(String::from_utf8(buf).unwrap(), "{\"name\":\"solo\"}\n");
276 }
277
278 #[derive(serde::Serialize)]
281 struct Issue {
282 key: &'static str,
283 summary: &'static str,
284 }
285
286 #[test]
287 fn yaml_stream_emits_one_doc_per_sequence_item() {
288 let data = vec![
289 Issue {
290 key: "PROJ-1",
291 summary: "Fix login",
292 },
293 Issue {
294 key: "PROJ-2",
295 summary: "Add feature",
296 },
297 ];
298 let out = format_yaml_stream(&data).unwrap();
299 assert_eq!(
300 out,
301 "---\nkey: PROJ-1\nsummary: Fix login\n---\nkey: PROJ-2\nsummary: Add feature\n"
302 );
303 }
304
305 #[test]
306 fn yaml_stream_empty_sequence_emits_nothing() {
307 let data: Vec<Issue> = vec![];
308 let out = format_yaml_stream(&data).unwrap();
309 assert_eq!(out, "");
310 }
311
312 #[test]
313 fn yaml_stream_single_item_sequence() {
314 let data = vec![Issue {
315 key: "PROJ-1",
316 summary: "Fix login",
317 }];
318 let out = format_yaml_stream(&data).unwrap();
319 assert_eq!(out, "---\nkey: PROJ-1\nsummary: Fix login\n");
320 }
321
322 #[test]
323 fn yaml_stream_non_sequence_emits_single_doc() {
324 let data = Issue {
325 key: "PROJ-1",
326 summary: "Fix login",
327 };
328 let out = format_yaml_stream(&data).unwrap();
329 assert_eq!(out, "---\nkey: PROJ-1\nsummary: Fix login\n");
330 }
331
332 #[test]
333 fn yaml_stream_scalar_emits_single_doc() {
334 let data: i32 = 42;
335 let out = format_yaml_stream(&data).unwrap();
336 assert_eq!(out, "---\n42\n");
337 }
338
339 #[test]
340 fn yaml_stream_nested_sequences_treat_outer_only() {
341 let data = vec![vec![1, 2], vec![3, 4]];
342 let out = format_yaml_stream(&data).unwrap();
343 assert_eq!(out, "---\n- 1\n- 2\n---\n- 3\n- 4\n");
344 }
345
346 #[test]
347 fn yaml_stream_round_trips_via_safe_load_all() {
348 use serde::Deserialize;
349
350 let data = vec![
351 Issue {
352 key: "PROJ-1",
353 summary: "Fix login",
354 },
355 Issue {
356 key: "PROJ-2",
357 summary: "Add feature",
358 },
359 ];
360 let out = format_yaml_stream(&data).unwrap();
361
362 let docs: Vec<serde_yaml::Value> = serde_yaml::Deserializer::from_str(&out)
363 .map(serde_yaml::Value::deserialize)
364 .collect::<Result<_, _>>()
365 .unwrap();
366
367 assert_eq!(docs.len(), 2);
368 assert_eq!(docs[0]["key"], serde_yaml::Value::from("PROJ-1"));
369 assert_eq!(docs[1]["key"], serde_yaml::Value::from("PROJ-2"));
370 }
371
372 #[test]
375 fn write_output_table_returns_false_and_writes_nothing() {
376 let data = vec![1_i32, 2];
377 let mut buf = Vec::new();
378 let wrote = write_output(&data, &OutputFormat::Table, &mut buf).unwrap();
379 assert!(!wrote);
380 assert!(buf.is_empty());
381 }
382
383 #[test]
384 fn write_output_json_emits_pretty_array() {
385 let data = vec![1_i32, 2, 3];
386 let mut buf = Vec::new();
387 let wrote = write_output(&data, &OutputFormat::Json, &mut buf).unwrap();
388 assert!(wrote);
389 let out = String::from_utf8(buf).unwrap();
390 assert!(out.starts_with('['));
391 assert!(out.contains(" 1,\n"));
392 assert!(out.ends_with("]\n"));
393 }
394
395 #[test]
396 fn write_output_yaml_emits_list() {
397 let data = vec![1_i32, 2];
398 let mut buf = Vec::new();
399 let wrote = write_output(&data, &OutputFormat::Yaml, &mut buf).unwrap();
400 assert!(wrote);
401 let out = String::from_utf8(buf).unwrap();
402 assert_eq!(out, "- 1\n- 2\n");
403 }
404
405 #[test]
406 fn write_output_yamls_emits_yaml_stream() {
407 let data = vec![1_i32, 2];
408 let mut buf = Vec::new();
409 let wrote = write_output(&data, &OutputFormat::Yamls, &mut buf).unwrap();
410 assert!(wrote);
411 let out = String::from_utf8(buf).unwrap();
412 assert_eq!(out, "---\n1\n---\n2\n");
413 }
414
415 #[test]
416 fn write_output_jsonl_emits_one_line_per_item() {
417 let data = vec![1_i32, 2, 3];
418 let mut buf = Vec::new();
419 let wrote = write_output(&data, &OutputFormat::Jsonl, &mut buf).unwrap();
420 assert!(wrote);
421 assert_eq!(String::from_utf8(buf).unwrap(), "1\n2\n3\n");
422 }
423
424 struct FailingWriter;
425
426 impl Write for FailingWriter {
427 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
428 Err(std::io::Error::other("boom"))
429 }
430 fn flush(&mut self) -> std::io::Result<()> {
431 Err(std::io::Error::other("boom"))
432 }
433 }
434
435 #[test]
436 fn write_output_propagates_write_errors() {
437 let data = vec![1_i32];
438 let mut writer = FailingWriter;
439
440 assert!(write_output(&data, &OutputFormat::Json, &mut writer).is_err());
441 assert!(write_output(&data, &OutputFormat::Yaml, &mut writer).is_err());
442 assert!(write_output(&data, &OutputFormat::Yamls, &mut writer).is_err());
443 assert!(write_output(&data, &OutputFormat::Jsonl, &mut writer).is_err());
444 assert!(writer.write(b"x").is_err());
445 assert!(writer.flush().is_err());
446 }
447
448 struct FailingSerialize;
449
450 impl Serialize for FailingSerialize {
451 fn serialize<S>(&self, _serializer: S) -> std::result::Result<S::Ok, S::Error>
452 where
453 S: serde::Serializer,
454 {
455 Err(serde::ser::Error::custom("serialize failed"))
456 }
457 }
458
459 impl JsonlSerialize for FailingSerialize {
460 fn write_jsonl(&self, _out: &mut dyn Write) -> Result<()> {
461 Ok(())
462 }
463 }
464
465 #[test]
466 fn write_output_propagates_json_serialize_errors() {
467 let mut buf = Vec::new();
468 let err = write_output(&FailingSerialize, &OutputFormat::Json, &mut buf).unwrap_err();
469 assert!(err.to_string().contains("Failed to serialize as JSON"));
470 }
471
472 #[test]
473 fn write_output_propagates_yaml_serialize_errors() {
474 let mut buf = Vec::new();
475 let err = write_output(&FailingSerialize, &OutputFormat::Yaml, &mut buf).unwrap_err();
476 assert!(err.to_string().contains("Failed to serialize as YAML"));
477 }
478
479 #[test]
480 fn failing_serialize_jsonl_impl_is_a_noop() {
481 let mut buf = Vec::new();
482 FailingSerialize.write_jsonl(&mut buf).unwrap();
483 assert!(buf.is_empty());
484 }
485}