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 =
220            format_output(&[json!(["a", "b", "c"])], false, true, &OutputFormat::Auto);
221        assert_eq!(output, "a\nb\nc");
222    }
223
224    #[test]
225    fn format_lines_multiple() {
226        let output =
227            format_output(&[json!("x"), json!("y")], false, true, &OutputFormat::Auto);
228        assert_eq!(output, "x\ny");
229    }
230
231    #[test]
232    fn format_empty() {
233        assert_eq!(
234            format_output(&[], false, false, &OutputFormat::Auto),
235            ""
236        );
237    }
238
239    #[test]
240    fn format_empty_string() {
241        assert_eq!(
242            format_output(&[json!("")], false, false, &OutputFormat::Auto),
243            ""
244        );
245    }
246
247    #[test]
248    fn format_string_with_newlines() {
249        assert_eq!(
250            format_output(&[json!("line1\nline2")], false, false, &OutputFormat::Auto),
251            "line1\nline2"
252        );
253    }
254
255    // ── Format-aware output ──
256
257    #[test]
258    fn format_output_yaml() {
259        let output =
260            format_output(&[json!({"name": "Alice"})], false, false, &OutputFormat::Yaml);
261        assert!(output.contains("name:"));
262        assert!(output.contains("Alice"));
263    }
264
265    #[test]
266    fn format_output_toml() {
267        let output =
268            format_output(&[json!({"name": "Alice"})], false, false, &OutputFormat::Toml);
269        assert!(output.contains("name"));
270        assert!(output.contains("Alice"));
271    }
272
273    #[test]
274    fn format_output_json_explicit() {
275        let output =
276            format_output(&[json!({"name": "Alice"})], false, false, &OutputFormat::Json);
277        assert!(output.contains("\"name\""));
278        assert!(output.contains("\"Alice\""));
279    }
280
281    #[test]
282    fn format_yaml_scalar() {
283        let output = format_output(&[json!("hello")], false, false, &OutputFormat::Yaml);
284        assert!(output.contains("hello"));
285    }
286
287    #[test]
288    fn format_yaml_array() {
289        let output =
290            format_output(&[json!([1, 2, 3])], false, false, &OutputFormat::Yaml);
291        assert!(output.contains("- 1"));
292    }
293
294    #[test]
295    fn format_toml_non_table_fallback() {
296        // TOML can't represent a scalar root; falls back to plain
297        let output = format_output(&[json!("hello")], false, false, &OutputFormat::Toml);
298        assert_eq!(output, "hello");
299    }
300
301    #[test]
302    fn format_toml_nested() {
303        let output = format_output(
304            &[json!({"server": {"port": 8080}})],
305            false,
306            false,
307            &OutputFormat::Toml,
308        );
309        assert!(output.contains("[server]"));
310        assert!(output.contains("port = 8080"));
311    }
312
313    // ══════════════════════════════════════════════
314    // Additional coverage tests
315    // ══════════════════════════════════════════════
316
317    // ── Multiple results to different formats ──
318
319    #[test]
320    fn format_multiple_as_yaml() {
321        let output = format_output(
322            &[json!({"a": 1}), json!({"b": 2})],
323            false,
324            false,
325            &OutputFormat::Yaml,
326        );
327        assert!(output.contains("a:"));
328        assert!(output.contains("b:"));
329    }
330
331    #[test]
332    fn format_multiple_as_toml() {
333        let output = format_output(
334            &[json!({"a": 1}), json!({"b": 2})],
335            false,
336            false,
337            &OutputFormat::Toml,
338        );
339        // Wraps in "results" table
340        assert!(output.contains("results"));
341    }
342
343    #[test]
344    fn format_multiple_as_json() {
345        let output = format_output(
346            &[json!("a"), json!("b"), json!("c")],
347            false,
348            false,
349            &OutputFormat::Json,
350        );
351        assert!(output.contains('['));
352        assert!(output.contains("\"a\""));
353        assert!(output.contains("\"b\""));
354        assert!(output.contains("\"c\""));
355    }
356
357    // ── YAML edge cases ──
358
359    #[test]
360    fn format_yaml_nested_object() {
361        let output = format_output(
362            &[json!({"server": {"host": "localhost", "port": 8080}})],
363            false,
364            false,
365            &OutputFormat::Yaml,
366        );
367        assert!(output.contains("server:"));
368        assert!(output.contains("host:"));
369        assert!(output.contains("localhost"));
370    }
371
372    #[test]
373    fn format_yaml_array_of_objects() {
374        let output = format_output(
375            &[json!([{"name": "Alice"}, {"name": "Bob"}])],
376            false,
377            false,
378            &OutputFormat::Yaml,
379        );
380        assert!(output.contains("name: Alice"));
381        assert!(output.contains("name: Bob"));
382    }
383
384    #[test]
385    fn format_yaml_null() {
386        let output = format_output(&[json!(null)], false, false, &OutputFormat::Yaml);
387        assert!(output.contains("null"));
388    }
389
390    #[test]
391    fn format_yaml_boolean() {
392        let output = format_output(&[json!(true)], false, false, &OutputFormat::Yaml);
393        assert!(output.contains("true"));
394    }
395
396    #[test]
397    fn format_yaml_number() {
398        let output = format_output(&[json!(42)], false, false, &OutputFormat::Yaml);
399        assert!(output.contains("42"));
400    }
401
402    // ── TOML edge cases ──
403
404    #[test]
405    fn format_toml_array_fallback() {
406        // TOML can't represent array root; wraps in "results"
407        let output = format_output(
408            &[json!([1, 2, 3])],
409            false,
410            false,
411            &OutputFormat::Toml,
412        );
413        // Falls back to plain since array is not a table
414        assert!(output.contains("1"));
415    }
416
417    #[test]
418    fn format_toml_boolean() {
419        let output = format_output(
420            &[json!({"flag": true})],
421            false,
422            false,
423            &OutputFormat::Toml,
424        );
425        assert!(output.contains("flag = true"));
426    }
427
428    #[test]
429    fn format_toml_string_with_quotes() {
430        let output = format_output(
431            &[json!({"name": "Alice"})],
432            false,
433            false,
434            &OutputFormat::Toml,
435        );
436        assert!(output.contains("name = \"Alice\""));
437    }
438
439    #[test]
440    fn format_toml_integer() {
441        let output = format_output(
442            &[json!({"count": 42})],
443            false,
444            false,
445            &OutputFormat::Toml,
446        );
447        assert!(output.contains("count = 42"));
448    }
449
450    // ── JSON output edge cases ──
451
452    #[test]
453    fn format_json_null() {
454        let output = format_output(&[json!(null)], true, false, &OutputFormat::Auto);
455        assert_eq!(output, "null");
456    }
457
458    #[test]
459    fn format_json_bool() {
460        let output = format_output(&[json!(true)], true, false, &OutputFormat::Auto);
461        assert_eq!(output, "true");
462    }
463
464    #[test]
465    fn format_json_object() {
466        let output = format_output(&[json!({"a": 1})], true, false, &OutputFormat::Auto);
467        assert!(output.contains("\"a\": 1"));
468    }
469
470    #[test]
471    fn format_json_array() {
472        let output = format_output(&[json!([1, 2])], true, false, &OutputFormat::Auto);
473        assert!(output.contains("1"));
474        assert!(output.contains("2"));
475    }
476
477    // ── Lines output edge cases ──
478
479    #[test]
480    fn format_lines_nested_arrays() {
481        let output = format_output(
482            &[json!(["a", "b"]), json!(["c", "d"])],
483            false,
484            true,
485            &OutputFormat::Auto,
486        );
487        assert_eq!(output, "a\nb\nc\nd");
488    }
489
490    #[test]
491    fn format_lines_mixed_types() {
492        let output = format_output(
493            &[json!("str"), json!(42), json!(true), json!(null)],
494            false,
495            true,
496            &OutputFormat::Auto,
497        );
498        assert_eq!(output, "str\n42\ntrue\nnull");
499    }
500
501    #[test]
502    fn format_lines_single_value() {
503        let output = format_output(&[json!("hello")], false, true, &OutputFormat::Auto);
504        assert_eq!(output, "hello");
505    }
506
507    #[test]
508    fn format_lines_empty() {
509        let output = format_output(&[], false, true, &OutputFormat::Auto);
510        assert_eq!(output, "");
511    }
512
513    // ── JSON flag overrides output format ──
514
515    #[test]
516    fn format_json_flag_overrides_yaml() {
517        // --json should win even if --output yaml is set
518        let output = format_output(
519            &[json!({"name": "Alice"})],
520            true,
521            false,
522            &OutputFormat::Yaml,
523        );
524        assert!(output.contains("\"name\""));
525        assert!(output.contains("\"Alice\""));
526    }
527
528    #[test]
529    fn format_json_flag_overrides_toml() {
530        let output = format_output(
531            &[json!({"name": "Alice"})],
532            true,
533            false,
534            &OutputFormat::Toml,
535        );
536        assert!(output.contains("\"name\""));
537    }
538
539    // ── Plain output edge cases ──
540
541    #[test]
542    fn format_negative_number() {
543        assert_eq!(
544            format_output(&[json!(-5)], false, false, &OutputFormat::Auto),
545            "-5"
546        );
547    }
548
549    #[test]
550    fn format_large_number() {
551        assert_eq!(
552            format_output(&[json!(999999999)], false, false, &OutputFormat::Auto),
553            "999999999"
554        );
555    }
556
557    #[test]
558    fn format_deeply_nested_object() {
559        let val = json!({"a": {"b": {"c": 1}}});
560        let output = format_output(&[val], false, false, &OutputFormat::Auto);
561        assert!(output.contains("\"a\""));
562        assert!(output.contains("\"b\""));
563        assert!(output.contains("\"c\""));
564    }
565
566    #[test]
567    fn format_unicode_string() {
568        assert_eq!(
569            format_output(&[json!("hello 🌍")], false, false, &OutputFormat::Auto),
570            "hello 🌍"
571        );
572    }
573}