Skip to main content

pick/
output.rs

1use crate::cli::OutputFormat;
2use serde_json::Value;
3
4pub fn format_output(
5    results: &[Value],
6    as_json: bool,
7    as_lines: bool,
8    output_format: &OutputFormat,
9) -> String {
10    if results.is_empty() {
11        return String::new();
12    }
13
14    // Explicit --json flag always wins
15    if as_json {
16        return format_as_json(results);
17    }
18
19    // Output format override (--output yaml/toml/json)
20    match output_format {
21        OutputFormat::Json => return format_as_json(results),
22        OutputFormat::Yaml => return format_as_yaml(results),
23        OutputFormat::Toml => return format_as_toml(results),
24        OutputFormat::Auto => {} // fall through to default formatting
25    }
26
27    if as_lines {
28        // Flatten: if results contain arrays, expand them
29        let mut all_values = Vec::new();
30        for r in results {
31            if let Value::Array(arr) = r {
32                all_values.extend(arr.iter().cloned());
33            } else {
34                all_values.push(r.clone());
35            }
36        }
37        return all_values
38            .iter()
39            .map(format_value_plain)
40            .collect::<Vec<_>>()
41            .join("\n");
42    }
43
44    if results.len() == 1 {
45        return format_value_plain(&results[0]);
46    }
47
48    // Multiple results: one per line
49    results
50        .iter()
51        .map(format_value_plain)
52        .collect::<Vec<_>>()
53        .join("\n")
54}
55
56fn format_value_plain(value: &Value) -> String {
57    match value {
58        Value::Null => "null".to_string(),
59        Value::Bool(b) => b.to_string(),
60        Value::Number(n) => n.to_string(),
61        Value::String(s) => s.clone(),
62        // Complex types rendered as compact JSON
63        Value::Array(_) | Value::Object(_) => serde_json::to_string_pretty(value).unwrap(),
64    }
65}
66
67fn format_as_json(results: &[Value]) -> String {
68    if results.len() == 1 {
69        return serde_json::to_string_pretty(&results[0]).unwrap();
70    }
71    let arr = Value::Array(results.to_vec());
72    serde_json::to_string_pretty(&arr).unwrap()
73}
74
75fn format_as_yaml(results: &[Value]) -> String {
76    if results.len() == 1 {
77        let mut s = serde_yaml::to_string(&results[0]).unwrap_or_default();
78        // serde_yaml adds trailing newline; strip it for consistency
79        if s.ends_with('\n') {
80            s.pop();
81        }
82        // serde_yaml adds "---\n" prefix; strip for clean output
83        if let Some(stripped) = s.strip_prefix("---\n") {
84            return stripped.to_string();
85        }
86        return s;
87    }
88    let arr = Value::Array(results.to_vec());
89    let mut s = serde_yaml::to_string(&arr).unwrap_or_default();
90    if s.ends_with('\n') {
91        s.pop();
92    }
93    if let Some(stripped) = s.strip_prefix("---\n") {
94        return stripped.to_string();
95    }
96    s
97}
98
99fn format_as_toml(results: &[Value]) -> String {
100    if results.len() == 1 {
101        return toml_from_json(&results[0]);
102    }
103    // TOML requires a top-level table; wrap array in a key
104    let wrapper = serde_json::json!({"results": results});
105    toml_from_json(&wrapper)
106}
107
108/// Convert a serde_json::Value to a TOML string.
109/// TOML requires the root to be a table. Non-table roots are wrapped.
110fn toml_from_json(value: &Value) -> String {
111    match value {
112        Value::Object(_) => {
113            // Convert via serde: json -> toml::Value -> string
114            let toml_val: Result<toml::Value, _> = serde_json::from_value(value.clone());
115            match toml_val {
116                Ok(tv) => {
117                    let mut s = toml::to_string_pretty(&tv).unwrap_or_default();
118                    if s.ends_with('\n') {
119                        s.pop();
120                    }
121                    s
122                }
123                Err(_) => serde_json::to_string_pretty(value).unwrap(),
124            }
125        }
126        // TOML cannot represent non-table roots; fall back to JSON
127        _ => format_value_plain(value),
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use serde_json::json;
135
136    #[test]
137    fn format_single_string() {
138        assert_eq!(
139            format_output(&[json!("hello")], false, false, &OutputFormat::Auto),
140            "hello"
141        );
142    }
143
144    #[test]
145    fn format_single_number() {
146        assert_eq!(
147            format_output(&[json!(42)], false, false, &OutputFormat::Auto),
148            "42"
149        );
150    }
151
152    #[test]
153    fn format_single_bool() {
154        assert_eq!(
155            format_output(&[json!(true)], false, false, &OutputFormat::Auto),
156            "true"
157        );
158    }
159
160    #[test]
161    fn format_single_null() {
162        assert_eq!(
163            format_output(&[json!(null)], false, false, &OutputFormat::Auto),
164            "null"
165        );
166    }
167
168    #[test]
169    fn format_single_float() {
170        let output = format_output(&[json!(3.14)], false, false, &OutputFormat::Auto);
171        assert!(output.starts_with("3.14"));
172    }
173
174    #[test]
175    fn format_object_plain() {
176        let output = format_output(&[json!({"a": 1})], false, false, &OutputFormat::Auto);
177        assert!(output.contains("\"a\""));
178        assert!(output.contains("1"));
179    }
180
181    #[test]
182    fn format_array_plain() {
183        let output = format_output(&[json!([1, 2, 3])], false, false, &OutputFormat::Auto);
184        assert!(output.contains("1"));
185    }
186
187    #[test]
188    fn format_multiple_results() {
189        let output = format_output(
190            &[json!("a"), json!("b"), json!("c")],
191            false,
192            false,
193            &OutputFormat::Auto,
194        );
195        assert_eq!(output, "a\nb\nc");
196    }
197
198    #[test]
199    fn format_json_single() {
200        let output = format_output(&[json!("hello")], true, false, &OutputFormat::Auto);
201        assert_eq!(output, "\"hello\"");
202    }
203
204    #[test]
205    fn format_json_number() {
206        let output = format_output(&[json!(42)], true, false, &OutputFormat::Auto);
207        assert_eq!(output, "42");
208    }
209
210    #[test]
211    fn format_json_multiple() {
212        let output = format_output(&[json!("a"), json!("b")], true, false, &OutputFormat::Auto);
213        assert!(output.contains('['));
214        assert!(output.contains("\"a\""));
215    }
216
217    #[test]
218    fn format_lines_array() {
219        let output = format_output(&[json!(["a", "b", "c"])], false, true, &OutputFormat::Auto);
220        assert_eq!(output, "a\nb\nc");
221    }
222
223    #[test]
224    fn format_lines_multiple() {
225        let output = format_output(&[json!("x"), json!("y")], false, true, &OutputFormat::Auto);
226        assert_eq!(output, "x\ny");
227    }
228
229    #[test]
230    fn format_empty() {
231        assert_eq!(format_output(&[], false, false, &OutputFormat::Auto), "");
232    }
233
234    #[test]
235    fn format_empty_string() {
236        assert_eq!(
237            format_output(&[json!("")], false, false, &OutputFormat::Auto),
238            ""
239        );
240    }
241
242    #[test]
243    fn format_string_with_newlines() {
244        assert_eq!(
245            format_output(&[json!("line1\nline2")], false, false, &OutputFormat::Auto),
246            "line1\nline2"
247        );
248    }
249
250    // ── Format-aware output ──
251
252    #[test]
253    fn format_output_yaml() {
254        let output = format_output(
255            &[json!({"name": "Alice"})],
256            false,
257            false,
258            &OutputFormat::Yaml,
259        );
260        assert!(output.contains("name:"));
261        assert!(output.contains("Alice"));
262    }
263
264    #[test]
265    fn format_output_toml() {
266        let output = format_output(
267            &[json!({"name": "Alice"})],
268            false,
269            false,
270            &OutputFormat::Toml,
271        );
272        assert!(output.contains("name"));
273        assert!(output.contains("Alice"));
274    }
275
276    #[test]
277    fn format_output_json_explicit() {
278        let output = format_output(
279            &[json!({"name": "Alice"})],
280            false,
281            false,
282            &OutputFormat::Json,
283        );
284        assert!(output.contains("\"name\""));
285        assert!(output.contains("\"Alice\""));
286    }
287
288    #[test]
289    fn format_yaml_scalar() {
290        let output = format_output(&[json!("hello")], false, false, &OutputFormat::Yaml);
291        assert!(output.contains("hello"));
292    }
293
294    #[test]
295    fn format_yaml_array() {
296        let output = format_output(&[json!([1, 2, 3])], false, false, &OutputFormat::Yaml);
297        assert!(output.contains("- 1"));
298    }
299
300    #[test]
301    fn format_toml_non_table_fallback() {
302        // TOML can't represent a scalar root; falls back to plain
303        let output = format_output(&[json!("hello")], false, false, &OutputFormat::Toml);
304        assert_eq!(output, "hello");
305    }
306
307    #[test]
308    fn format_toml_nested() {
309        let output = format_output(
310            &[json!({"server": {"port": 8080}})],
311            false,
312            false,
313            &OutputFormat::Toml,
314        );
315        assert!(output.contains("[server]"));
316        assert!(output.contains("port = 8080"));
317    }
318
319    // ══════════════════════════════════════════════
320    // Additional coverage tests
321    // ══════════════════════════════════════════════
322
323    // ── Multiple results to different formats ──
324
325    #[test]
326    fn format_multiple_as_yaml() {
327        let output = format_output(
328            &[json!({"a": 1}), json!({"b": 2})],
329            false,
330            false,
331            &OutputFormat::Yaml,
332        );
333        assert!(output.contains("a:"));
334        assert!(output.contains("b:"));
335    }
336
337    #[test]
338    fn format_multiple_as_toml() {
339        let output = format_output(
340            &[json!({"a": 1}), json!({"b": 2})],
341            false,
342            false,
343            &OutputFormat::Toml,
344        );
345        // Wraps in "results" table
346        assert!(output.contains("results"));
347    }
348
349    #[test]
350    fn format_multiple_as_json() {
351        let output = format_output(
352            &[json!("a"), json!("b"), json!("c")],
353            false,
354            false,
355            &OutputFormat::Json,
356        );
357        assert!(output.contains('['));
358        assert!(output.contains("\"a\""));
359        assert!(output.contains("\"b\""));
360        assert!(output.contains("\"c\""));
361    }
362
363    // ── YAML edge cases ──
364
365    #[test]
366    fn format_yaml_nested_object() {
367        let output = format_output(
368            &[json!({"server": {"host": "localhost", "port": 8080}})],
369            false,
370            false,
371            &OutputFormat::Yaml,
372        );
373        assert!(output.contains("server:"));
374        assert!(output.contains("host:"));
375        assert!(output.contains("localhost"));
376    }
377
378    #[test]
379    fn format_yaml_array_of_objects() {
380        let output = format_output(
381            &[json!([{"name": "Alice"}, {"name": "Bob"}])],
382            false,
383            false,
384            &OutputFormat::Yaml,
385        );
386        assert!(output.contains("name: Alice"));
387        assert!(output.contains("name: Bob"));
388    }
389
390    #[test]
391    fn format_yaml_null() {
392        let output = format_output(&[json!(null)], false, false, &OutputFormat::Yaml);
393        assert!(output.contains("null"));
394    }
395
396    #[test]
397    fn format_yaml_boolean() {
398        let output = format_output(&[json!(true)], false, false, &OutputFormat::Yaml);
399        assert!(output.contains("true"));
400    }
401
402    #[test]
403    fn format_yaml_number() {
404        let output = format_output(&[json!(42)], false, false, &OutputFormat::Yaml);
405        assert!(output.contains("42"));
406    }
407
408    // ── TOML edge cases ──
409
410    #[test]
411    fn format_toml_array_fallback() {
412        // TOML can't represent array root; wraps in "results"
413        let output = format_output(&[json!([1, 2, 3])], false, false, &OutputFormat::Toml);
414        // Falls back to plain since array is not a table
415        assert!(output.contains("1"));
416    }
417
418    #[test]
419    fn format_toml_boolean() {
420        let output = format_output(&[json!({"flag": true})], false, false, &OutputFormat::Toml);
421        assert!(output.contains("flag = true"));
422    }
423
424    #[test]
425    fn format_toml_string_with_quotes() {
426        let output = format_output(
427            &[json!({"name": "Alice"})],
428            false,
429            false,
430            &OutputFormat::Toml,
431        );
432        assert!(output.contains("name = \"Alice\""));
433    }
434
435    #[test]
436    fn format_toml_integer() {
437        let output = format_output(&[json!({"count": 42})], false, false, &OutputFormat::Toml);
438        assert!(output.contains("count = 42"));
439    }
440
441    // ── JSON output edge cases ──
442
443    #[test]
444    fn format_json_null() {
445        let output = format_output(&[json!(null)], true, false, &OutputFormat::Auto);
446        assert_eq!(output, "null");
447    }
448
449    #[test]
450    fn format_json_bool() {
451        let output = format_output(&[json!(true)], true, false, &OutputFormat::Auto);
452        assert_eq!(output, "true");
453    }
454
455    #[test]
456    fn format_json_object() {
457        let output = format_output(&[json!({"a": 1})], true, false, &OutputFormat::Auto);
458        assert!(output.contains("\"a\": 1"));
459    }
460
461    #[test]
462    fn format_json_array() {
463        let output = format_output(&[json!([1, 2])], true, false, &OutputFormat::Auto);
464        assert!(output.contains("1"));
465        assert!(output.contains("2"));
466    }
467
468    // ── Lines output edge cases ──
469
470    #[test]
471    fn format_lines_nested_arrays() {
472        let output = format_output(
473            &[json!(["a", "b"]), json!(["c", "d"])],
474            false,
475            true,
476            &OutputFormat::Auto,
477        );
478        assert_eq!(output, "a\nb\nc\nd");
479    }
480
481    #[test]
482    fn format_lines_mixed_types() {
483        let output = format_output(
484            &[json!("str"), json!(42), json!(true), json!(null)],
485            false,
486            true,
487            &OutputFormat::Auto,
488        );
489        assert_eq!(output, "str\n42\ntrue\nnull");
490    }
491
492    #[test]
493    fn format_lines_single_value() {
494        let output = format_output(&[json!("hello")], false, true, &OutputFormat::Auto);
495        assert_eq!(output, "hello");
496    }
497
498    #[test]
499    fn format_lines_empty() {
500        let output = format_output(&[], false, true, &OutputFormat::Auto);
501        assert_eq!(output, "");
502    }
503
504    // ── JSON flag overrides output format ──
505
506    #[test]
507    fn format_json_flag_overrides_yaml() {
508        // --json should win even if --output yaml is set
509        let output = format_output(
510            &[json!({"name": "Alice"})],
511            true,
512            false,
513            &OutputFormat::Yaml,
514        );
515        assert!(output.contains("\"name\""));
516        assert!(output.contains("\"Alice\""));
517    }
518
519    #[test]
520    fn format_json_flag_overrides_toml() {
521        let output = format_output(
522            &[json!({"name": "Alice"})],
523            true,
524            false,
525            &OutputFormat::Toml,
526        );
527        assert!(output.contains("\"name\""));
528    }
529
530    // ── Plain output edge cases ──
531
532    #[test]
533    fn format_negative_number() {
534        assert_eq!(
535            format_output(&[json!(-5)], false, false, &OutputFormat::Auto),
536            "-5"
537        );
538    }
539
540    #[test]
541    fn format_large_number() {
542        assert_eq!(
543            format_output(&[json!(999999999)], false, false, &OutputFormat::Auto),
544            "999999999"
545        );
546    }
547
548    #[test]
549    fn format_deeply_nested_object() {
550        let val = json!({"a": {"b": {"c": 1}}});
551        let output = format_output(&[val], false, false, &OutputFormat::Auto);
552        assert!(output.contains("\"a\""));
553        assert!(output.contains("\"b\""));
554        assert!(output.contains("\"c\""));
555    }
556
557    #[test]
558    fn format_unicode_string() {
559        assert_eq!(
560            format_output(&[json!("hello 🌍")], false, false, &OutputFormat::Auto),
561            "hello 🌍"
562        );
563    }
564}