Skip to main content

pick/
lib.rs

1pub mod cli;
2pub mod detector;
3pub mod error;
4pub mod formats;
5pub mod output;
6pub mod selector;
7pub mod streaming;
8
9use cli::{Cli, InputFormat};
10use error::PickError;
11use selector::{Expression, execute};
12use serde_json::Value;
13
14pub fn run(cli: &Cli, input: &str) -> Result<String, PickError> {
15    if input.trim().is_empty() {
16        return Err(PickError::NoInput);
17    }
18
19    let selector_str = cli.selector.as_deref().unwrap_or("");
20    let expression = Expression::parse(selector_str)?;
21
22    // Determine format
23    let format = match cli.input {
24        InputFormat::Auto => detector::detect_format(input),
25        ref f => f.clone(),
26    };
27
28    // Parse and extract
29    let results = match parse_and_execute(input, &format, &expression, selector_str) {
30        Ok(r) => r,
31        Err(e) => {
32            if let Some(ref default) = cli.default {
33                return Ok(default.clone());
34            }
35            return Err(e);
36        }
37    };
38
39    // Handle empty results with --default
40    if results.is_empty() {
41        if let Some(ref default) = cli.default {
42            return Ok(default.clone());
43        }
44        return Err(PickError::KeyNotFound(selector_str.to_string()));
45    }
46
47    // --exists: just check, output nothing
48    if cli.exists {
49        return Ok(String::new());
50    }
51
52    // --count: output match count
53    if cli.count {
54        return Ok(results.len().to_string());
55    }
56
57    // --first: only first result
58    let results = if cli.first {
59        vec![results.into_iter().next().unwrap()]
60    } else {
61        results
62    };
63
64    Ok(output::format_output(
65        &results,
66        cli.json,
67        cli.lines,
68        &cli.output,
69    ))
70}
71
72fn parse_and_execute(
73    input: &str,
74    format: &InputFormat,
75    expression: &Expression,
76    selector_str: &str,
77) -> Result<Vec<Value>, PickError> {
78    // Text format has a special fallback path
79    if *format == InputFormat::Text {
80        return parse_and_extract_text(input, expression, selector_str);
81    }
82
83    let value = parse_input(input, format)?;
84    execute(&value, expression)
85}
86
87fn parse_and_extract_text(
88    input: &str,
89    expression: &Expression,
90    selector_str: &str,
91) -> Result<Vec<Value>, PickError> {
92    let value = formats::text::parse(input)?;
93
94    // Try normal extraction first
95    if let Ok(results) = execute(&value, expression)
96        && !results.is_empty()
97    {
98        return Ok(results);
99    }
100
101    // Fallback: search for the full selector string in the text
102    if !selector_str.is_empty()
103        && let Some(found) = formats::text::search_text(input, selector_str)
104    {
105        return Ok(vec![found]);
106    }
107
108    Err(PickError::KeyNotFound(selector_str.to_string()))
109}
110
111fn parse_input(input: &str, format: &InputFormat) -> Result<Value, PickError> {
112    match format {
113        InputFormat::Json => formats::json::parse(input),
114        InputFormat::Yaml => formats::yaml::parse(input),
115        InputFormat::Toml => formats::toml_format::parse(input),
116        InputFormat::Env => formats::env::parse(input),
117        InputFormat::Headers => formats::headers::parse(input),
118        InputFormat::Logfmt => formats::logfmt::parse(input),
119        InputFormat::Csv => formats::csv_format::parse(input),
120        InputFormat::Text => formats::text::parse(input),
121        InputFormat::Auto => {
122            // Should not reach here; detect_format handles this
123            Err(PickError::UnknownFormat)
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use cli::OutputFormat;
132
133    fn make_cli(selector: Option<&str>) -> Cli {
134        Cli {
135            selector: selector.map(String::from),
136            input: InputFormat::Auto,
137            output: OutputFormat::Auto,
138            file: None,
139            json: false,
140            raw: false,
141            first: false,
142            lines: false,
143            default: None,
144            quiet: false,
145            exists: false,
146            count: false,
147            stream: false,
148        }
149    }
150
151    #[test]
152    fn run_json_simple() {
153        let cli = make_cli(Some("name"));
154        let result = run(&cli, r#"{"name": "Alice"}"#).unwrap();
155        assert_eq!(result, "Alice");
156    }
157
158    #[test]
159    fn run_json_nested() {
160        let cli = make_cli(Some("user.email"));
161        let result = run(&cli, r#"{"user": {"email": "a@b.com"}}"#).unwrap();
162        assert_eq!(result, "a@b.com");
163    }
164
165    #[test]
166    fn run_json_array_index() {
167        let cli = make_cli(Some("items[0]"));
168        let result = run(&cli, r#"{"items": ["first", "second"]}"#).unwrap();
169        assert_eq!(result, "first");
170    }
171
172    #[test]
173    fn run_json_wildcard() {
174        let cli = make_cli(Some("items[*].name"));
175        let result = run(&cli, r#"{"items": [{"name": "a"}, {"name": "b"}]}"#).unwrap();
176        assert_eq!(result, "a\nb");
177    }
178
179    #[test]
180    fn run_yaml() {
181        let mut cli = make_cli(Some("name"));
182        cli.input = InputFormat::Yaml;
183        let result = run(&cli, "name: Alice\nage: 30").unwrap();
184        assert_eq!(result, "Alice");
185    }
186
187    #[test]
188    fn run_toml() {
189        let mut cli = make_cli(Some("package.name"));
190        cli.input = InputFormat::Toml;
191        let result = run(&cli, "[package]\nname = \"pick\"").unwrap();
192        assert_eq!(result, "pick");
193    }
194
195    #[test]
196    fn run_env() {
197        let mut cli = make_cli(Some("PORT"));
198        cli.input = InputFormat::Env;
199        let result = run(&cli, "DATABASE_URL=pg://localhost\nPORT=3000").unwrap();
200        assert_eq!(result, "3000");
201    }
202
203    #[test]
204    fn run_headers() {
205        let mut cli = make_cli(Some("content-type"));
206        cli.input = InputFormat::Headers;
207        let result = run(&cli, "Content-Type: application/json\nX-Request-Id: abc").unwrap();
208        assert_eq!(result, "application/json");
209    }
210
211    #[test]
212    fn run_logfmt() {
213        let mut cli = make_cli(Some("msg"));
214        cli.input = InputFormat::Logfmt;
215        let result = run(&cli, "level=info msg=hello status=200").unwrap();
216        assert_eq!(result, "hello");
217    }
218
219    #[test]
220    fn run_csv() {
221        let mut cli = make_cli(Some("[0].name"));
222        cli.input = InputFormat::Csv;
223        let result = run(&cli, "name,age\nAlice,30\nBob,25").unwrap();
224        assert_eq!(result, "Alice");
225    }
226
227    #[test]
228    fn run_no_selector_returns_whole() {
229        let cli = make_cli(None);
230        let result = run(&cli, r#"{"a": 1}"#).unwrap();
231        assert!(result.contains("\"a\""));
232    }
233
234    #[test]
235    fn run_empty_input() {
236        let cli = make_cli(Some("x"));
237        assert!(run(&cli, "").is_err());
238        assert!(run(&cli, "   ").is_err());
239    }
240
241    #[test]
242    fn run_key_not_found() {
243        let cli = make_cli(Some("missing"));
244        assert!(run(&cli, r#"{"a": 1}"#).is_err());
245    }
246
247    #[test]
248    fn run_default_on_missing() {
249        let mut cli = make_cli(Some("missing"));
250        cli.default = Some("fallback".into());
251        let result = run(&cli, r#"{"a": 1}"#).unwrap();
252        assert_eq!(result, "fallback");
253    }
254
255    #[test]
256    fn run_exists_found() {
257        let mut cli = make_cli(Some("a"));
258        cli.exists = true;
259        let result = run(&cli, r#"{"a": 1}"#).unwrap();
260        assert_eq!(result, "");
261    }
262
263    #[test]
264    fn run_exists_not_found() {
265        let mut cli = make_cli(Some("b"));
266        cli.exists = true;
267        assert!(run(&cli, r#"{"a": 1}"#).is_err());
268    }
269
270    #[test]
271    fn run_count() {
272        let mut cli = make_cli(Some("items[*]"));
273        cli.count = true;
274        let result = run(&cli, r#"{"items": [1, 2, 3]}"#).unwrap();
275        assert_eq!(result, "3");
276    }
277
278    #[test]
279    fn run_first() {
280        let mut cli = make_cli(Some("items[*]"));
281        cli.first = true;
282        let result = run(&cli, r#"{"items": [1, 2, 3]}"#).unwrap();
283        assert_eq!(result, "1");
284    }
285
286    #[test]
287    fn run_json_output() {
288        let mut cli = make_cli(Some("name"));
289        cli.json = true;
290        let result = run(&cli, r#"{"name": "Alice"}"#).unwrap();
291        assert_eq!(result, "\"Alice\"");
292    }
293
294    #[test]
295    fn run_lines_output() {
296        let mut cli = make_cli(Some("items"));
297        cli.lines = true;
298        let result = run(&cli, r#"{"items": ["a", "b", "c"]}"#).unwrap();
299        assert_eq!(result, "a\nb\nc");
300    }
301
302    #[test]
303    fn run_text_kv_fallback() {
304        let mut cli = make_cli(Some("name"));
305        cli.input = InputFormat::Text;
306        let result = run(&cli, "name=Alice\nage=30").unwrap();
307        assert_eq!(result, "Alice");
308    }
309
310    #[test]
311    fn run_text_search_fallback() {
312        let mut cli = make_cli(Some("error"));
313        cli.input = InputFormat::Text;
314        let result = run(&cli, "info: all good\nerror: something failed").unwrap();
315        assert_eq!(result, "something failed");
316    }
317
318    #[test]
319    fn run_auto_detect_json() {
320        let cli = make_cli(Some("x"));
321        let result = run(&cli, r#"{"x": 42}"#).unwrap();
322        assert_eq!(result, "42");
323    }
324
325    #[test]
326    fn run_auto_detect_env() {
327        let cli = make_cli(Some("PORT"));
328        let result = run(&cli, "PORT=3000\nHOST=localhost").unwrap();
329        assert_eq!(result, "3000");
330    }
331
332    #[test]
333    fn run_negative_index() {
334        let cli = make_cli(Some("[*][-1]"));
335        let result = run(&cli, "[[1,2],[3,4]]").unwrap();
336        assert_eq!(result, "2\n4");
337    }
338
339    // ── Phase 1 integration: slicing ──
340
341    #[test]
342    fn run_slice() {
343        let cli = make_cli(Some("items[1:3]"));
344        let result = run(&cli, r#"{"items": [10, 20, 30, 40, 50]}"#).unwrap();
345        assert_eq!(result, "20\n30");
346    }
347
348    // ── Phase 1 integration: builtins ──
349
350    #[test]
351    fn run_keys() {
352        let cli = make_cli(Some("keys()"));
353        let result = run(&cli, r#"{"b": 2, "a": 1}"#).unwrap();
354        assert!(result.contains("\"a\""));
355        assert!(result.contains("\"b\""));
356    }
357
358    #[test]
359    fn run_length() {
360        let cli = make_cli(Some("items.length()"));
361        let result = run(&cli, r#"{"items": [1, 2, 3]}"#).unwrap();
362        assert_eq!(result, "3");
363    }
364
365    // ── Phase 1 integration: recursive descent ──
366
367    #[test]
368    fn run_recursive() {
369        let cli = make_cli(Some("..name"));
370        let result = run(&cli, r#"{"a": {"name": "deep"}}"#).unwrap();
371        assert_eq!(result, "deep");
372    }
373
374    // ── Phase 1 integration: multi-selector ──
375
376    #[test]
377    fn run_multi_selector() {
378        let cli = make_cli(Some("name, age"));
379        let result = run(&cli, r#"{"name": "Alice", "age": 30}"#).unwrap();
380        assert_eq!(result, "Alice\n30");
381    }
382
383    // ── Phase 2 integration: pipeline ──
384
385    #[test]
386    fn run_pipeline_select() {
387        let cli = make_cli(Some("items[*] | select(.price > 100) | name"));
388        let input = r#"{"items": [{"name": "a", "price": 50}, {"name": "b", "price": 200}]}"#;
389        let result = run(&cli, input).unwrap();
390        assert_eq!(result, "b");
391    }
392
393    #[test]
394    fn run_pipeline_builtin() {
395        let cli = make_cli(Some("data | keys()"));
396        let result = run(&cli, r#"{"data": {"x": 1, "y": 2}}"#).unwrap();
397        assert!(result.contains("\"x\""));
398        assert!(result.contains("\"y\""));
399    }
400
401    // ── Phase 2 integration: regex ──
402
403    #[test]
404    fn run_pipeline_regex() {
405        let cli = make_cli(Some("items[*] | select(.name ~ \"^a\") | name"));
406        let input = r#"{"items": [{"name": "apple"}, {"name": "banana"}, {"name": "avocado"}]}"#;
407        let result = run(&cli, input).unwrap();
408        assert_eq!(result, "apple\navocado");
409    }
410
411    // ── Phase 3 integration: set/del ──
412
413    #[test]
414    fn run_set() {
415        let mut cli = make_cli(Some("set(.name, \"Bob\")"));
416        cli.json = true;
417        let result = run(&cli, r#"{"name": "Alice", "age": 30}"#).unwrap();
418        assert!(result.contains("\"Bob\""));
419        assert!(result.contains("30"));
420    }
421
422    #[test]
423    fn run_del() {
424        let mut cli = make_cli(Some("del(.temp)"));
425        cli.json = true;
426        let result = run(&cli, r#"{"name": "Alice", "temp": "x"}"#).unwrap();
427        assert!(result.contains("Alice"));
428        assert!(!result.contains("temp"));
429    }
430
431    // ── Phase 3 integration: format-aware output ──
432
433    #[test]
434    fn run_output_yaml() {
435        let mut cli = make_cli(None);
436        cli.output = OutputFormat::Yaml;
437        let result = run(&cli, r#"{"name": "Alice"}"#).unwrap();
438        assert!(result.contains("name:"));
439    }
440
441    #[test]
442    fn run_output_toml() {
443        let mut cli = make_cli(None);
444        cli.output = OutputFormat::Toml;
445        let result = run(&cli, r#"{"name": "Alice"}"#).unwrap();
446        assert!(result.contains("name = "));
447    }
448
449    // ══════════════════════════════════════════════
450    // Additional coverage tests
451    // ══════════════════════════════════════════════
452
453    // ── Phase 1: Slice edge cases ──
454
455    #[test]
456    fn run_slice_from_start() {
457        let cli = make_cli(Some("items[:2]"));
458        let result = run(&cli, r#"{"items": [10, 20, 30, 40]}"#).unwrap();
459        assert_eq!(result, "10\n20");
460    }
461
462    #[test]
463    fn run_slice_to_end() {
464        let cli = make_cli(Some("items[2:]"));
465        let result = run(&cli, r#"{"items": [10, 20, 30, 40]}"#).unwrap();
466        assert_eq!(result, "30\n40");
467    }
468
469    #[test]
470    fn run_slice_negative() {
471        let cli = make_cli(Some("items[-2:]"));
472        let result = run(&cli, r#"{"items": [10, 20, 30, 40]}"#).unwrap();
473        assert_eq!(result, "30\n40");
474    }
475
476    #[test]
477    fn run_slice_all() {
478        let cli = make_cli(Some("items[:]"));
479        let result = run(&cli, r#"{"items": [1, 2, 3]}"#).unwrap();
480        assert_eq!(result, "1\n2\n3");
481    }
482
483    #[test]
484    fn run_slice_empty_result() {
485        let cli = make_cli(Some("items[10:20]"));
486        let result = run(&cli, r#"{"items": [1, 2, 3]}"#);
487        assert!(result.is_err());
488    }
489
490    #[test]
491    fn run_slice_deeply_nested() {
492        let cli = make_cli(Some("data[0].items[1:3]"));
493        let result = run(&cli, r#"{"data": [{"items": [10, 20, 30, 40]}]}"#).unwrap();
494        assert_eq!(result, "20\n30");
495    }
496
497    // ── Phase 1: Builtin edge cases ──
498
499    #[test]
500    fn run_values() {
501        let cli = make_cli(Some("values()"));
502        let result = run(&cli, r#"{"a": 1, "b": 2}"#).unwrap();
503        assert!(result.contains("1"));
504        assert!(result.contains("2"));
505    }
506
507    #[test]
508    fn run_length_string() {
509        let cli = make_cli(Some("name.length()"));
510        let result = run(&cli, r#"{"name": "Alice"}"#).unwrap();
511        assert_eq!(result, "5");
512    }
513
514    #[test]
515    fn run_length_object() {
516        let cli = make_cli(Some("length()"));
517        let result = run(&cli, r#"{"a": 1, "b": 2, "c": 3}"#).unwrap();
518        assert_eq!(result, "3");
519    }
520
521    #[test]
522    fn run_keys_on_array() {
523        let cli = make_cli(Some("keys()"));
524        let result = run(&cli, "[10, 20, 30]").unwrap();
525        assert!(result.contains("0"));
526        assert!(result.contains("1"));
527        assert!(result.contains("2"));
528    }
529
530    // ── Phase 1: Recursive descent edge cases ──
531
532    #[test]
533    fn run_recursive_multiple_matches() {
534        let cli = make_cli(Some("..id"));
535        let result = run(&cli, r#"{"a": {"id": 1}, "b": {"id": 2}}"#).unwrap();
536        assert!(result.contains("1"));
537        assert!(result.contains("2"));
538    }
539
540    #[test]
541    fn run_recursive_deep() {
542        let cli = make_cli(Some("..target"));
543        let result = run(&cli, r#"{"a": {"b": {"c": {"target": 42}}}}"#).unwrap();
544        assert_eq!(result, "42");
545    }
546
547    #[test]
548    fn run_recursive_not_found() {
549        let cli = make_cli(Some("..missing"));
550        assert!(run(&cli, r#"{"a": 1, "b": 2}"#).is_err());
551    }
552
553    // ── Phase 1: Multi-selector edge cases ──
554
555    #[test]
556    fn run_multi_selector_three() {
557        let cli = make_cli(Some("a, b, c"));
558        let result = run(&cli, r#"{"a": 1, "b": 2, "c": 3}"#).unwrap();
559        assert_eq!(result, "1\n2\n3");
560    }
561
562    #[test]
563    fn run_multi_selector_partial_missing() {
564        let cli = make_cli(Some("name, missing, age"));
565        let result = run(&cli, r#"{"name": "Alice", "age": 30}"#).unwrap();
566        assert_eq!(result, "Alice\n30");
567    }
568
569    #[test]
570    fn run_multi_selector_all_missing() {
571        let cli = make_cli(Some("x, y"));
572        let result = run(&cli, r#"{"a": 1}"#);
573        assert!(result.is_err());
574    }
575
576    // ── Phase 2: Pipeline edge cases ──
577
578    #[test]
579    fn run_pipeline_three_stages() {
580        let cli = make_cli(Some("items[*] | select(.active) | name"));
581        let input = r#"{"items": [{"name": "a", "active": true}, {"name": "b", "active": false}, {"name": "c", "active": true}]}"#;
582        let result = run(&cli, input).unwrap();
583        assert_eq!(result, "a\nc");
584    }
585
586    #[test]
587    fn run_pipeline_four_stages() {
588        let cli = make_cli(Some("items[*] | select(.active) | name | length()"));
589        let input = r#"{"items": [{"name": "ab", "active": true}, {"name": "cde", "active": false}, {"name": "fgh", "active": true}]}"#;
590        let result = run(&cli, input).unwrap();
591        assert_eq!(result, "2\n3");
592    }
593
594    #[test]
595    fn run_pipeline_keys_then_length() {
596        let cli = make_cli(Some("keys() | length()"));
597        let result = run(&cli, r#"{"a": 1, "b": 2, "c": 3}"#).unwrap();
598        assert_eq!(result, "3");
599    }
600
601    #[test]
602    fn run_pipeline_values_then_length() {
603        let cli = make_cli(Some("values() | length()"));
604        let result = run(&cli, r#"{"a": 1, "b": 2}"#).unwrap();
605        assert_eq!(result, "2");
606    }
607
608    // ── Phase 2: Select edge cases ──
609
610    #[test]
611    fn run_select_lt() {
612        let cli = make_cli(Some("[*] | select(. < 10)"));
613        let result = run(&cli, "[1, 5, 10, 15, 20]").unwrap();
614        assert_eq!(result, "1\n5");
615    }
616
617    #[test]
618    fn run_select_gte() {
619        let cli = make_cli(Some("[*] | select(. >= 10)"));
620        let result = run(&cli, "[1, 5, 10, 15, 20]").unwrap();
621        assert_eq!(result, "10\n15\n20");
622    }
623
624    #[test]
625    fn run_select_ne() {
626        let cli = make_cli(Some("[*] | select(.status != \"deleted\") | name"));
627        let input = r#"[{"name": "a", "status": "active"}, {"name": "b", "status": "deleted"}, {"name": "c", "status": "active"}]"#;
628        let result = run(&cli, input).unwrap();
629        assert_eq!(result, "a\nc");
630    }
631
632    #[test]
633    fn run_select_or() {
634        let cli = make_cli(Some(
635            "[*] | select(.price > 100 or .featured == true) | name",
636        ));
637        let input = r#"[{"name": "a", "price": 5, "featured": true}, {"name": "b", "price": 50, "featured": false}, {"name": "c", "price": 500, "featured": false}]"#;
638        let result = run(&cli, input).unwrap();
639        assert_eq!(result, "a\nc");
640    }
641
642    #[test]
643    fn run_select_not() {
644        let cli = make_cli(Some("[*] | select(not .active) | name"));
645        let input = r#"[{"name": "a", "active": true}, {"name": "b", "active": false}]"#;
646        let result = run(&cli, input).unwrap();
647        assert_eq!(result, "b");
648    }
649
650    #[test]
651    fn run_select_all_filtered_out() {
652        let cli = make_cli(Some("[*] | select(. > 100)"));
653        let result = run(&cli, "[1, 2, 3]");
654        assert!(result.is_err());
655    }
656
657    #[test]
658    fn run_select_eq_null() {
659        let cli = make_cli(Some("[*] | select(.email == null) | name"));
660        let input = r#"[{"name": "a", "email": null}, {"name": "b", "email": "b@x.com"}]"#;
661        let result = run(&cli, input).unwrap();
662        assert_eq!(result, "a");
663    }
664
665    #[test]
666    fn run_select_eq_bool() {
667        let cli = make_cli(Some("[*] | select(.done == true) | name"));
668        let input = r#"[{"name": "a", "done": true}, {"name": "b", "done": false}]"#;
669        let result = run(&cli, input).unwrap();
670        assert_eq!(result, "a");
671    }
672
673    #[test]
674    fn run_select_regex_case_insensitive() {
675        let cli = make_cli(Some("[*] | select(. ~ \"(?i)^hello$\")"));
676        let result = run(&cli, r#"["Hello", "hello", "HELLO", "world"]"#).unwrap();
677        assert_eq!(result, "Hello\nhello\nHELLO");
678    }
679
680    // ── Phase 3: set/del edge cases ──
681
682    #[test]
683    fn run_set_nested() {
684        let mut cli = make_cli(Some("set(.user.name, \"Bob\")"));
685        cli.json = true;
686        let result = run(&cli, r#"{"user": {"name": "Alice", "age": 30}}"#).unwrap();
687        assert!(result.contains("\"Bob\""));
688        assert!(result.contains("30"));
689    }
690
691    #[test]
692    fn run_set_new_key() {
693        let mut cli = make_cli(Some("set(.b, 2)"));
694        cli.json = true;
695        let result = run(&cli, r#"{"a": 1}"#).unwrap();
696        assert!(result.contains("\"a\": 1"));
697        assert!(result.contains("\"b\": 2"));
698    }
699
700    #[test]
701    fn run_set_number() {
702        let cli = make_cli(Some("set(.count, 42) | count"));
703        let result = run(&cli, r#"{"count": 0}"#).unwrap();
704        assert_eq!(result, "42");
705    }
706
707    #[test]
708    fn run_set_bool() {
709        let cli = make_cli(Some("set(.active, true) | active"));
710        let result = run(&cli, r#"{"active": false}"#).unwrap();
711        assert_eq!(result, "true");
712    }
713
714    #[test]
715    fn run_set_null() {
716        let cli = make_cli(Some("set(.temp, null) | temp"));
717        let result = run(&cli, r#"{"temp": "data"}"#).unwrap();
718        assert_eq!(result, "null");
719    }
720
721    #[test]
722    fn run_del_nested() {
723        let mut cli = make_cli(Some("del(.user.temp)"));
724        cli.json = true;
725        let result = run(&cli, r#"{"user": {"name": "Alice", "temp": "x"}}"#).unwrap();
726        assert!(result.contains("Alice"));
727        assert!(!result.contains("temp"));
728    }
729
730    #[test]
731    fn run_del_array_element() {
732        let mut cli = make_cli(Some("del(.items[1])"));
733        cli.json = true;
734        let result = run(&cli, r#"{"items": [1, 2, 3]}"#).unwrap();
735        assert!(result.contains("1"));
736        assert!(result.contains("3"));
737        assert!(!result.contains("  2")); // "2" not as standalone array element
738    }
739
740    #[test]
741    fn run_set_then_del() {
742        let mut cli = make_cli(Some("set(.c, 3) | del(.a)"));
743        cli.json = true;
744        let result = run(&cli, r#"{"a": 1, "b": 2}"#).unwrap();
745        assert!(result.contains("\"b\": 2"));
746        assert!(result.contains("\"c\": 3"));
747        assert!(!result.contains("\"a\""));
748    }
749
750    #[test]
751    fn run_multiple_set() {
752        let cli = make_cli(Some("set(.x, 1) | set(.y, 2) | keys() | length()"));
753        let result = run(&cli, r#"{"a": 0}"#).unwrap();
754        // keys: a, x, y → 3
755        assert_eq!(result, "3");
756    }
757
758    #[test]
759    fn run_multiple_del() {
760        let mut cli = make_cli(Some("del(.a) | del(.b)"));
761        cli.json = true;
762        let result = run(&cli, r#"{"a": 1, "b": 2, "c": 3}"#).unwrap();
763        assert!(result.contains("\"c\": 3"));
764        assert!(!result.contains("\"a\""));
765        assert!(!result.contains("\"b\""));
766    }
767
768    // ── Phase 3: Format output edge cases ──
769
770    #[test]
771    fn run_output_json_explicit() {
772        let mut cli = make_cli(Some("name"));
773        cli.output = OutputFormat::Json;
774        let result = run(&cli, r#"{"name": "Alice"}"#).unwrap();
775        assert_eq!(result, "\"Alice\"");
776    }
777
778    #[test]
779    fn run_output_yaml_nested() {
780        let mut cli = make_cli(Some("user"));
781        cli.output = OutputFormat::Yaml;
782        let result = run(&cli, r#"{"user": {"name": "Alice", "age": 30}}"#).unwrap();
783        assert!(result.contains("name:"));
784        assert!(result.contains("Alice"));
785    }
786
787    // ── Cross-phase combinations ──
788
789    #[test]
790    fn run_slice_then_select() {
791        let cli = make_cli(Some("items[1:4] | select(.price > 100) | name"));
792        let input = r#"{"items": [{"name": "a", "price": 10}, {"name": "b", "price": 200}, {"name": "c", "price": 50}, {"name": "d", "price": 300}]}"#;
793        let result = run(&cli, input).unwrap();
794        assert_eq!(result, "b\nd");
795    }
796
797    #[test]
798    fn run_recursive_then_select() {
799        let cli = make_cli(Some("..items[*] | select(.active) | name"));
800        let input = r#"{"data": {"items": [{"name": "a", "active": true}, {"name": "b", "active": false}]}}"#;
801        let result = run(&cli, input).unwrap();
802        assert_eq!(result, "a");
803    }
804
805    #[test]
806    fn run_wildcard_then_length() {
807        let cli = make_cli(Some("items[*].name | length()"));
808        let input = r#"{"items": [{"name": "ab"}, {"name": "cde"}]}"#;
809        let result = run(&cli, input).unwrap();
810        assert_eq!(result, "2\n3");
811    }
812
813    #[test]
814    fn run_select_then_set() {
815        let mut cli = make_cli(Some("[*] | select(.active) | set(.selected, true)"));
816        cli.json = true;
817        let input = r#"[{"name": "a", "active": true}, {"name": "b", "active": false}]"#;
818        let result = run(&cli, input).unwrap();
819        assert!(result.contains("selected"));
820        assert!(result.contains("\"a\""));
821    }
822
823    // ── Flags with pipeline features ──
824
825    #[test]
826    fn run_count_with_select() {
827        let mut cli = make_cli(Some("[*] | select(. > 10)"));
828        cli.count = true;
829        let result = run(&cli, "[1, 5, 15, 20, 25]").unwrap();
830        assert_eq!(result, "3");
831    }
832
833    #[test]
834    fn run_first_with_select() {
835        let mut cli = make_cli(Some("[*] | select(. > 10)"));
836        cli.first = true;
837        let result = run(&cli, "[1, 5, 15, 20, 25]").unwrap();
838        assert_eq!(result, "15");
839    }
840
841    #[test]
842    fn run_exists_with_pipeline() {
843        let mut cli = make_cli(Some("items[*] | select(.active)"));
844        cli.exists = true;
845        let input = r#"{"items": [{"active": true}]}"#;
846        let result = run(&cli, input).unwrap();
847        assert_eq!(result, "");
848    }
849
850    #[test]
851    fn run_default_with_pipeline() {
852        let mut cli = make_cli(Some("[*] | select(. > 100)"));
853        cli.default = Some("none".into());
854        let result = run(&cli, "[1, 2, 3]").unwrap();
855        assert_eq!(result, "none");
856    }
857
858    // ── Unicode handling ──
859
860    #[test]
861    fn run_unicode_key() {
862        let cli = make_cli(Some("\"名前\""));
863        let result = run(&cli, r#"{"名前": "太郎"}"#).unwrap();
864        assert_eq!(result, "太郎");
865    }
866
867    #[test]
868    fn run_unicode_value() {
869        let cli = make_cli(Some("emoji"));
870        let result = run(&cli, r#"{"emoji": "🎉🎊🎈"}"#).unwrap();
871        assert_eq!(result, "🎉🎊🎈");
872    }
873
874    // ── Default on parse error ──
875
876    #[test]
877    fn run_default_on_parse_error() {
878        let mut cli = make_cli(Some("name"));
879        cli.default = Some("fallback".into());
880        cli.input = InputFormat::Json;
881        let result = run(&cli, "not valid json").unwrap();
882        assert_eq!(result, "fallback");
883    }
884}