Skip to main content

yaml_subset/
lib.rs

1extern crate pest;
2
3mod path;
4mod utils;
5pub mod yaml;
6
7pub use path::YamlPath;
8pub use yaml::{DocumentResult, YamlResult};
9
10#[cfg(test)]
11mod tests {
12    use super::yaml::parse_yaml_file;
13    use super::yaml::{AliasedYaml, Yaml, YamlInsert};
14    use super::YamlPath;
15    use crate::yaml::{DoubleQuotedStringPart, MyVec, Pretty};
16
17    #[test]
18    fn basic() {
19        let inp = r#"---
20# test
21k: &anch test
22# hi
23v: &ok
24   - &ok2 test
25# comment
26   - *test
27v: test # maybe
28k: [5, 10, "e"]
29variable_groups: []
30double_quoted: "test
31
32now a space \n vs \\n
33"
34single_quoted: 'test escaping ''
35
36now a space \n vs \\n
37'
38l:
39 - 'sqrt( {d}^({r*n+m}m+{p}) * x^({r*a+p})  * y^{b*r+q} )'
40 - Read the values of the cyclometric numbers off of the circle. <br/> {gon_circle}
41
42# after
43"#;
44        let parsed = parse_yaml_file(inp);
45        insta::assert_debug_snapshot!(parsed);
46        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
47    }
48
49    #[test]
50    fn inline_hash() {
51        let inp = r#"---
52field: &test {}
53item: {}
54item2: { key: "value", other: [a, b]}
55other: # check
56  combine: { key: { key: [l, {a: "b"}]} }
57  item: {}
58"#;
59        let parsed = parse_yaml_file(inp);
60        insta::assert_debug_snapshot!(parsed);
61        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
62    }
63
64    #[test]
65    fn block_delimiters_in_text() {
66        let inp = r#"---
67content: When you press <em>Submit part</em>.
68abs: An absolute value is written as |x|
69"#;
70        let parsed = parse_yaml_file(inp);
71        insta::assert_debug_snapshot!(parsed);
72        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
73    }
74
75    #[test]
76    fn array_delimiters_in_text() {
77        let inp = r#"---
78default_value:  random(-9 .. 9 except [0, a])    
79"#;
80        let parsed = parse_yaml_file(inp);
81        insta::assert_debug_snapshot!(parsed);
82        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
83    }
84
85    #[test]
86    fn comments_before_start() {
87        let inp = r#"
88# a comment
89# another one
90
91---
92x: 5
93
94  "#;
95
96        let parsed = parse_yaml_file(inp);
97        insta::assert_debug_snapshot!(parsed);
98        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
99    }
100
101    #[test]
102    fn elab() {
103        let inp = r#"---
104# test
105k: &anch test
106# hi
107
108v: &ok
109   - &ok2 test
110# comment
111   - *test # an anchor
112v: test # maybe
113k: &kk [5, 10, "e"]
114# after
115y: - x: 5
116     z: &zfield 6 # yess
117
118   - n: 8
119
120   - j
121# between
122z: - *kk
123
124   - ke
125k: - - j
126     - k
127
128     - l
129   - - m
130     - z: k
131
132
133       t: - m: l # comment
134            p: 4 # comment
135          - m # after m
136# wow
137# something else
138"#;
139        let parsed = parse_yaml_file(inp);
140        insta::assert_debug_snapshot!(parsed);
141        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
142    }
143
144    #[test]
145    fn block_scalars() {
146        // see https://yaml-multiline.info/
147        let inp = r#"---
148        newlines: &newline |
149                  Several lines of text,
150                  with some "quotes" of various 'types',
151                  and also a blank line:
152                  
153                  and some text with
154                    extra indentation
155                  on the next line,
156                  plus another line at the end.
157                  
158        folded: &folded >
159                  Several lines of text,
160                  with some "quotes" of various 'types',
161                  and also a blank line:
162                  
163                  and some text with
164                    extra indentation
165                  on the next line,
166                  plus another line at the end.
167                  
168                  
169                  
170        test: 5"#;
171
172        let parsed = parse_yaml_file(inp);
173        insta::assert_debug_snapshot!(parsed);
174        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
175    }
176
177    #[test]
178    fn block_scalars_with_chomping() {
179        // see https://yaml-multiline.info/
180        let inp = r#"---
181        newlines: &newline |-
182                  Several lines of text,
183                  with some "quotes" of various 'types',
184                  and also a blank line:
185                  
186                  and some text with
187                    extra indentation
188                  on the next line,
189                  plus another line at the end.
190                  
191        folded: &folded >+
192                  Several lines of text,
193                  with some "quotes" of various 'types',
194                  and also a blank line:
195                  
196                  and some text with
197                    extra indentation
198                  on the next line,
199                  plus another line at the end.
200                  
201                  
202                  
203        test: 5"#;
204
205        let parsed = parse_yaml_file(inp);
206        insta::assert_debug_snapshot!(parsed);
207        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
208    }
209
210    #[test]
211    fn block_scalars_with_indent() {
212        // see https://yaml-multiline.info/
213        let inp = r#"---
214        newlines: &newline |10
215                    Several lines of text,
216                  with some "quotes" of various 'types',
217                  and also a blank line:
218                  
219                  and some text with
220                    extra indentation
221                  on the next line,
222                  plus another line at the end.
223                  
224        folded: &folded >10
225                    Several lines of text,
226                  with some "quotes" of various 'types',
227                  and also a blank line:
228                  
229                  and some text with
230                    extra indentation
231                  on the next line,
232                  plus another line at the end.
233                  
234                  
235                  
236        test: 5"#;
237
238        let parsed = parse_yaml_file(inp);
239        insta::assert_debug_snapshot!(parsed);
240        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
241    }
242
243    #[test]
244    fn block_scalars_with_indent_and_empty_lines() {
245        // see https://yaml-multiline.info/
246        let inp = r#"---
247        newlines: &newline |10
248                    Several lines of text,
249                  with some "quotes" of various 'types',
250                  and also a blank line:
251
252                  and some text with
253                    extra indentation
254                  on the next line,
255                  plus another line at the end.
256                  
257        folded: &folded >10
258                    Several lines of text,
259                  with some "quotes" of various 'types',
260                  and also a blank line:
261
262                  and some text with
263                    extra indentation
264                  on the next line,
265                  plus another line at the end.
266                  
267                  
268                  
269        test: 5"#;
270
271        let parsed = parse_yaml_file(inp);
272        insta::assert_debug_snapshot!(parsed);
273        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
274    }
275
276    #[test]
277    fn block_scalars_deeper() {
278        let inp = r#"---
279parts:
280  - 
281    type: gapfill
282    marks: 0
283    prompt:
284      content: test
285      other: |
286        8
287        10"#;
288
289        let parsed = parse_yaml_file(inp);
290        insta::assert_debug_snapshot!(parsed);
291        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
292    }
293
294    #[test]
295    fn commas_in_unquoted_string() {
296        let inp = r#"---
297inline_array: [test, 5, hi]
298s: &key test, 5, hi
299"#;
300        let parsed = parse_yaml_file(inp);
301        insta::assert_debug_snapshot!(parsed);
302        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
303    }
304
305    #[test]
306    fn alternative_hash_comment_between_first_and_second() {
307        let inp = r#"---
308parts:
309- type: choose_one
310  # prompt: "file:statement-wrong.html"
311  answer_data: tmp 
312  "#;
313
314        let parsed = parse_yaml_file(inp);
315        insta::assert_debug_snapshot!(parsed);
316        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
317    }
318
319    #[test]
320    fn comment_before_first_key() {
321        let inp = r#"---
322default_value:
323    content: 
324      # NOTE: the 'original' non-shuffled array advr is used, rather than x_advr !!!
325      # Better advice to be written ...? This one does not correctly extend to users of this template!!
326      nl: "Gebruik het merkwaardig product {if(a*b>0, advr[0], advr[1])}"
327      en: "Use the special product {if(a*b>0, advr[0], advr[1])}""#;
328        let parsed = parse_yaml_file(inp);
329        insta::assert_debug_snapshot!(parsed);
330        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
331    }
332
333    #[test]
334    fn insert_hash_top() {
335        let inp = r#"---
336inline_array: [test, 5, hi]
337s: &key test, 5, hi
338item:
339 key: value
340"#;
341        let mut parsed = parse_yaml_file(inp).unwrap();
342        let path: YamlPath = "new".parse().unwrap();
343        let data = AliasedYaml {
344            alias: None,
345            value: Yaml::UnquotedString("value".to_string()),
346        };
347        assert_eq!(1, parsed.insert_into_hash(&path, &data, false));
348
349        let out = r#"---
350inline_array: [test, 5, hi]
351s: &key test, 5, hi
352item:
353 key: value
354new: value
355"#;
356
357        let parsed_out = parse_yaml_file(out).unwrap();
358        assert_eq!(parsed_out, parsed);
359    }
360
361    #[test]
362    fn insert_hash() {
363        let inp = r#"---
364inline_array: [test, 5, hi]
365s: &key test, 5, hi
366item:
367 key: value
368"#;
369        let mut parsed = parse_yaml_file(inp).unwrap();
370        let path: YamlPath = "item.key2".parse().unwrap();
371        let data = AliasedYaml {
372            alias: None,
373            value: Yaml::DoubleQuotedString(vec![DoubleQuotedStringPart::String(
374                "test".to_string(),
375            )]),
376        };
377        assert_eq!(1, parsed.insert_into_hash(&path, &data, false));
378
379        let out = r#"---
380inline_array: [test, 5, hi]
381s: &key test, 5, hi
382item:
383 key: value
384 key2: "test"
385"#;
386
387        let parsed_out = parse_yaml_file(out).unwrap();
388        assert_eq!(parsed_out, parsed);
389
390        let path: YamlPath = "item.key2".parse().unwrap();
391        let data = AliasedYaml {
392            alias: None,
393            value: Yaml::DoubleQuotedString(vec![DoubleQuotedStringPart::String(
394                "test2".to_string(),
395            )]),
396        };
397        assert_eq!(0, parsed.insert_into_hash(&path, &data, false));
398        assert_eq!(parsed_out, parsed);
399        assert_eq!(1, parsed.insert_into_hash(&path, &data, true));
400        let out = r#"---
401inline_array: [test, 5, hi]
402s: &key test, 5, hi
403item:
404 key: value
405 key2: "test2"
406"#;
407
408        let parsed_out = parse_yaml_file(out).unwrap();
409        assert_eq!(parsed_out, parsed);
410    }
411
412    #[test]
413    fn rename_field() {
414        let inp = r#"---
415inline_array: [test, 5, hi]
416s: &key test, 5, hi
417item:
418 key: value
419"#;
420        let mut parsed = parse_yaml_file(inp).unwrap();
421        let path: YamlPath = "item.key".parse().unwrap();
422        assert_eq!(1, parsed.rename_field(&path, "newkey".to_string()));
423
424        let out = r#"---
425inline_array: [test, 5, hi]
426s: &key test, 5, hi
427item:
428 newkey: value
429"#;
430
431        let parsed_out = parse_yaml_file(out).unwrap();
432        assert_eq!(parsed_out, parsed);
433    }
434
435    #[test]
436    fn rename_field_list_in_list() {
437        let inp = r#"---
438parts:
439  - type: gapfill
440    gaps:
441      - type: jme
442        old_name: value
443      - type: jme
444        old_name: value
445  - type: random 
446    gaps:
447      - type: jme
448        old_name: value
449      - type: jme
450        old_name: value
451  - type: gapfill
452    gaps:
453      - type: jme
454        old_name: value
455      - type: jme
456        old_name: value
457"#;
458        let mut parsed = parse_yaml_file(inp).unwrap();
459        let path: YamlPath = "parts[*]|type=gapfill.gaps[*].old_name".parse().unwrap();
460        assert_eq!(4, parsed.rename_field(&path, "new_name".to_string()));
461
462        let out = r#"---
463parts:
464  - type: gapfill
465    gaps:
466      - type: jme
467        new_name: value
468      - type: jme
469        new_name: value
470  - type: random 
471    gaps:
472      - type: jme
473        old_name: value
474      - type: jme
475        old_name: value
476  - type: gapfill
477    gaps:
478      - type: jme
479        new_name: value
480      - type: jme
481        new_name: value
482"#;
483
484        let parsed_out = parse_yaml_file(out).unwrap();
485        assert_eq!(parsed_out, parsed);
486    }
487
488    #[test]
489    fn to_object() {
490        let inp = r#"---
491inline_array: [test, 5, hi]
492s: &key test, 5, hi
493item:
494 key: value
495"#;
496        let mut parsed = parse_yaml_file(inp).unwrap();
497        let path: YamlPath = "item.key".parse().unwrap();
498        assert_eq!(1, parsed.to_object(&path, "subkey".to_string()));
499
500        let out = r#"---
501inline_array: [test, 5, hi]
502s: &key test, 5, hi
503item:
504 key: 
505   subkey: value
506"#;
507
508        let parsed_out = parse_yaml_file(out).unwrap();
509        assert_eq!(parsed_out, parsed);
510    }
511    #[test]
512    fn to_object_with_condition() {
513        let inp = r#"---
514inline_array: [test, 5, hi]
515s: &key test, 5, hi
516item:
517 key: value
518"#;
519        let mut parsed = parse_yaml_file(inp).unwrap();
520        let path: YamlPath = "item.key|key=value".parse().unwrap();
521        assert_eq!(1, parsed.to_object(&path, "subkey".to_string()));
522
523        let out = r#"---
524inline_array: [test, 5, hi]
525s: &key test, 5, hi
526item:
527 key: 
528   subkey: value
529"#;
530
531        let parsed_out = parse_yaml_file(out).unwrap();
532        assert_eq!(parsed_out, parsed);
533
534        let mut parsed = parse_yaml_file(inp).unwrap();
535        let path: YamlPath = "item.key|key=other".parse().unwrap();
536        assert_eq!(0, parsed.to_object(&path, "subkey".to_string()));
537        assert_eq!(parse_yaml_file(inp).unwrap(), parsed);
538    }
539    #[test]
540    fn remove_from_hash() {
541        let inp = r#"---
542inline_array: [test, 5, hi]
543s: &key test, 5, hi
544item:
545 key: value
546"#;
547        let mut parsed = parse_yaml_file(inp).unwrap();
548        let path: YamlPath = "item".parse().unwrap();
549        assert_eq!(1, parsed.remove_from_hash(&path));
550
551        let out = r#"---
552inline_array: [test, 5, hi]
553s: &key test, 5, hi
554"#;
555
556        let parsed_out = parse_yaml_file(out).unwrap();
557        assert_eq!(parsed_out, parsed);
558    }
559    #[test]
560    fn remove_from_hash_deeper() {
561        let inp = r#"---
562inline_array: [test, 5, hi]
563s: &key test, 5, hi
564item:
565 key:
566  item: 5
567  item2: 6
568"#;
569        let mut parsed = parse_yaml_file(inp).unwrap();
570        let path: YamlPath = "item.key.item".parse().unwrap();
571        assert_eq!(1, parsed.remove_from_hash(&path));
572        let out = r#"---
573inline_array: [test, 5, hi]
574s: &key test, 5, hi
575item:
576  key:
577    item2: 6
578"#;
579
580        let parsed_out = parse_yaml_file(out).unwrap();
581        assert_eq!(parsed_out, parsed);
582    }
583    #[test]
584    fn remove_from_hash_deeper_with_condition() {
585        let inp = r#"---
586type: test
587inline_array: [test, 5, hi]
588s: &key test, 5, hi
589item:
590 key:
591  item: 5
592  item2: 6
593"#;
594        let mut parsed = parse_yaml_file(inp).unwrap();
595        let path: YamlPath = "item|type=test.key.item".parse().unwrap();
596        assert_eq!(1, parsed.remove_from_hash(&path));
597        let out = r#"---
598type: test
599inline_array: [test, 5, hi]
600s: &key test, 5, hi
601item:
602  key:
603    item2: 6
604"#;
605
606        let parsed_out = parse_yaml_file(out).unwrap();
607        assert_eq!(parsed_out, parsed);
608
609        let mut parsed = parse_yaml_file(inp).unwrap();
610        let path: YamlPath = "item|type=other.key.item".parse().unwrap();
611        assert_eq!(0, parsed.remove_from_hash(&path));
612        assert_eq!(parse_yaml_file(inp).unwrap(), parsed);
613    }
614    #[test]
615    fn move_to_subfield_in_root() {
616        let inp = r#"---
617a: vala
618b: valb
619c: valc
620d: vald
621e: vale
622"#;
623
624        let mut parsed = parse_yaml_file(inp).unwrap();
625        let path: YamlPath = "".parse().unwrap();
626        assert_eq!(
627            1,
628            parsed.move_to_subfield(
629                &path,
630                "sub".to_string(),
631                vec!["e".to_string(), "b".to_string(), "d".to_string()]
632            )
633        );
634        let out = r#"---
635a: vala
636c: valc
637sub:
638    e: vale
639    b: valb
640    d: vald
641"#;
642
643        let parsed_out = parse_yaml_file(out).unwrap();
644        assert_eq!(parsed_out, parsed);
645    }
646    #[test]
647    fn move_to_subfield() {
648        let inp = r#"---
649k:
650  a: vala
651  b: valb
652  c: valc
653  d: vald
654  e: vale
655"#;
656
657        let mut parsed = parse_yaml_file(inp).unwrap();
658        let path: YamlPath = "k".parse().unwrap();
659        assert_eq!(
660            1,
661            parsed.move_to_subfield(
662                &path,
663                "sub".to_string(),
664                vec!["e".to_string(), "b".to_string(), "d".to_string()]
665            )
666        );
667        let out = r#"---
668k:
669  a: vala
670  c: valc
671  sub:
672    e: vale
673    b: valb
674    d: vald
675"#;
676
677        let parsed_out = parse_yaml_file(out).unwrap();
678        assert_eq!(parsed_out, parsed);
679    }
680    #[test]
681    fn move_to_existing_subfield() {
682        let inp = r#"---
683k:
684  a: vala
685  b: valb
686  c: valc
687  d: vald
688  e: vale
689  l:
690    z: valz
691"#;
692
693        let mut parsed = parse_yaml_file(inp).unwrap();
694        let path: YamlPath = "k".parse().unwrap();
695        assert_eq!(
696            1,
697            parsed.move_to_subfield(
698                &path,
699                "l".to_string(),
700                vec![
701                    "e".to_string(),
702                    "b".to_string(),
703                    "d".to_string(),
704                    "kk".to_string()
705                ]
706            )
707        );
708        let out = r#"---
709k:
710  a: vala
711  c: valc
712  l:
713    z: valz
714    e: vale
715    b: valb
716    d: vald
717"#;
718
719        let parsed_out = parse_yaml_file(out).unwrap();
720        assert_eq!(parsed_out, parsed);
721    }
722    #[test]
723    fn move_to_subfield_list() {
724        let inp = r#"---
725k:
726  - item1: test0
727    result:
728      yes: three
729  - item1: test1
730"#;
731
732        let mut parsed = parse_yaml_file(inp).unwrap();
733        let path: YamlPath = "k[*]".parse().unwrap();
734        assert_eq!(
735            2,
736            parsed.move_to_subfield(&path, "result".to_string(), vec!["item1".to_string(),])
737        );
738        let out = r#"---
739k:
740  - result:
741      yes: three
742      item1: test0
743  - result:
744      item1: test1
745"#;
746
747        let parsed_out = parse_yaml_file(out).unwrap();
748        assert_eq!(parsed_out, parsed);
749    }
750    #[test]
751    fn move_to_subfield_list_item() {
752        let inp = r#"---
753k:
754  - item1: test0
755    result:
756      yes: three
757  - item1: test1
758"#;
759
760        let mut parsed = parse_yaml_file(inp).unwrap();
761        let path: YamlPath = "k[0]".parse().unwrap();
762        assert_eq!(
763            1,
764            parsed.move_to_subfield(&path, "result".to_string(), vec!["item1".to_string(),])
765        );
766        let out = r#"---
767k:
768  - result:
769      yes: three
770      item1: test0
771  - item1: test1
772"#;
773
774        let parsed_out = parse_yaml_file(out).unwrap();
775        assert_eq!(parsed_out, parsed);
776    }
777    #[test]
778    fn move_to_subfield_list_filter() {
779        let inp = r#"---
780k:
781  - item1: test0
782    result:
783      yes: three
784  - item1: test1
785"#;
786
787        let mut parsed = parse_yaml_file(inp).unwrap();
788        let path: YamlPath = "k[*]|item1=test1".parse().unwrap();
789        assert_eq!(
790            1,
791            parsed.move_to_subfield(&path, "result".to_string(), vec!["item1".to_string(),])
792        );
793        let out = r#"---
794k:
795  - item1: test0
796    result:
797      yes: three      
798  - result:
799      item1: test1
800"#;
801
802        let parsed_out = parse_yaml_file(out).unwrap();
803        assert_eq!(parsed_out, parsed);
804    }
805    #[test]
806    fn neg_number_in_list() {
807        let inp = r#"---
808vset_range:
809  - -10
810  - 10
811vset_range_points: 20
812"#;
813
814        let parsed = parse_yaml_file(inp);
815        insta::assert_debug_snapshot!(parsed);
816        insta::assert_display_snapshot!(parsed.unwrap().format().unwrap());
817    }
818    #[test]
819    fn move_to_map_with_field_as_key() {
820        let inp = r#"---
821k:
822    - item1: 
823        variables:
824            # this specifies the test
825            test: value
826            ke:
827              # comments
828              definition: result
829              group: keep
830            # this one will be moved
831            other:
832              definition: result
833              group: different
834            k: 
835              definition: value
836              group: ""
837            main:
838              definiton: field
839              group: other
840            other2:
841              definition: result2
842              group: different
843    - item2:
844        variables:
845            # this specifies the test
846            test: value
847        grouped_variables: anything
848"#;
849
850        let mut parsed = parse_yaml_file(inp).unwrap();
851        let path: YamlPath = "k[*].item1".parse().unwrap();
852        assert_eq!(
853            1,
854            parsed.move_to_map_with_field_as_key(
855                &path,
856                "variables".to_string(),
857                "group".to_string(),
858                "grouped_variables".to_string(),
859                vec!["keep".to_string()]
860            )
861        );
862        let out = r#"---
863k:
864- item1: 
865        variables:
866            # this specifies the test
867            test: value
868            ke:
869              # comments
870              definition: result
871            k: 
872              definition: value
873        grouped_variables:
874            different:
875                # this one will be moved
876                other:
877                    definition: result
878                other2:
879                    definition: result2
880            other:
881                main:
882                    definiton: field
883- item2:
884    variables:
885        # this specifies the test
886        test: value
887    grouped_variables: anything
888"#;
889        println!("{}", parsed.format().unwrap());
890        let parsed_out = parse_yaml_file(out).unwrap();
891        assert_eq!(parsed_out, parsed);
892    }
893
894    #[test]
895    fn find() {
896        let inp = r#"---
897k:
898  - item1: test0
899    result:
900      yes: three
901  - item1: test1
902"#;
903
904        let mut parsed = parse_yaml_file(inp).unwrap();
905        let path: YamlPath = "k[*].item1".parse().unwrap();
906        assert_eq!(
907            MyVec(vec![
908                Yaml::UnquotedString("test0".to_string()),
909                Yaml::UnquotedString("test1".to_string()),
910            ]),
911            parsed.find_values(&path)
912        );
913    }
914
915    #[test]
916    fn pretty_long_double_quoted_string() {
917        let inp = r#"---
918default_value: "import math\ndef schuine_zijde(a,b):\n  pytha = math.sqrt(a**2+b**2)\n  return pytha\nprint(schuine_zijde(6,7))"   
919"#;
920        let parsed = parse_yaml_file(inp);
921        insta::assert_debug_snapshot!(parsed);
922        insta::assert_display_snapshot!(parsed.unwrap().pretty().format().unwrap());
923    }
924
925    #[test]
926    fn pretty_long_double_quoted_string_deeper_level() {
927        let inp = r#"---
928submission_id: 0195d1a3-9c41-7203-aad8-29603034fd9c
929datetime_rfc3339: "2025-03-26T09:48:35.393+01:00"
930type_with_data:
931  Programming:
932    content:
933      code: "import math\ndef schuine_zijde(a,b):\n  c = math.sqrt(a.a + b.b)\n  return c""#;
934
935        let parsed = parse_yaml_file(inp);
936        insta::assert_debug_snapshot!(parsed);
937        insta::assert_display_snapshot!(parsed.unwrap().pretty().format().unwrap());
938    }
939
940    // Single-quoted string that is safe to write unquoted in block context → becomes unquoted.
941    #[test]
942    fn pretty_dequotes_single_quoted_in_block() {
943        let inp = "---\nk: 'hello world'\n";
944        let pretty = parse_yaml_file(inp).unwrap().pretty().format().unwrap();
945        assert_eq!("---\nk: hello world", pretty);
946    }
947
948    // Single-quoted string with a comma stays quoted in block context (comma is an inline-breaking
949    // char so it must stay quoted to survive inside inline arrays).
950    #[test]
951    fn pretty_keeps_quotes_for_comma_in_block() {
952        let inp = "---\nk: 'a, b'\n";
953        let pretty = parse_yaml_file(inp).unwrap().pretty().format().unwrap();
954        assert_eq!("---\nk: 'a, b'", pretty);
955    }
956
957    // A single-quoted string that is too long (>90 chars) AND contains a comma must be folded,
958    // not left as a long quoted line.
959    #[test]
960    fn pretty_folds_long_single_quoted_with_comma() {
961        let long = "a, ".repeat(35); // "a, a, a, …" — has commas, > 90 chars
962        let inp = format!("---\nk: '{}'\n", long.trim_end_matches(", "));
963        let pretty = parse_yaml_file(&inp).unwrap().pretty().format().unwrap();
964        assert!(pretty.contains(">-") || pretty.contains("|-"), "expected folded/literal block scalar, got:\n{}", pretty);
965    }
966
967    // A long single-quoted string without inline-breaking chars must also be folded.
968    #[test]
969    fn pretty_folds_long_single_quoted_no_breaking_chars() {
970        let long = "word ".repeat(25); // > 90 chars, no commas/braces
971        let inp = format!("---\nk: '{}'\n", long.trim_end());
972        let pretty = parse_yaml_file(&inp).unwrap().pretty().format().unwrap();
973        assert!(pretty.contains(">-") || pretty.contains("|-") || pretty.contains(">"), "expected folded/literal block scalar, got:\n{}", pretty);
974    }
975
976    // An unquoted string that contains a brace (inline-breaking) and appears inside an inline
977    // array must be single-quoted so the inline array stays parseable.
978    #[test]
979    fn pretty_requotes_brace_in_inline_array() {
980        // After round-tripping through the serializer the string may arrive as UnquotedString;
981        // pretty() must re-quote it when it lands inside an inline array.
982        let inp = "---\nitems:\n  - ['$\\\\binom{1}{1}$', hello]\n";
983        let pretty = parse_yaml_file(inp).unwrap().pretty().format().unwrap();
984        // The brace-containing element must be quoted inside the inline array.
985        assert!(pretty.contains("'$\\\\binom{1}{1}$'") || pretty.contains("\"$\\\\binom{1}{1}$\""),
986            "brace string should be quoted in inline array, got:\n{}", pretty);
987    }
988
989    // Minimal reproduction: flatten + tag + untagged enum with nested struct.
990    // Ensures that fields after the solution (feedback, points) are NOT nested
991    // inside the inner tuple map.
992    #[cfg(feature = "serde")]
993    #[test]
994    fn serialize_flatten_tag_nested_map() {
995        use crate::yaml::serializer::YamlSerializer;
996        use crate::yaml::Pretty;
997        use serde::Serialize;
998
999        #[derive(Serialize)]
1000        struct Outer {
1001            metadata: String,
1002            #[serde(flatten)]
1003            content: Tagged,
1004        }
1005
1006        #[derive(Serialize)]
1007        #[serde(tag = "type", rename_all = "snake_case")]
1008        enum Tagged {
1009            Input(Inner),
1010        }
1011
1012        #[derive(Serialize)]
1013        struct Inner {
1014            #[serde(flatten)]
1015            kind: Kind,
1016            points: i16,
1017        }
1018
1019        #[derive(Serialize)]
1020        #[serde(tag = "kind", rename_all = "snake_case")]
1021        enum Kind {
1022            PythonExpression(PythonExpr),
1023        }
1024
1025        #[derive(Serialize)]
1026        struct PythonExpr {
1027            solution: Value,
1028            feedback: Option<String>,
1029        }
1030
1031        #[derive(Serialize)]
1032        #[serde(untagged)]
1033        enum Value {
1034            Tuple(Tuple),
1035            Int(i64),
1036        }
1037
1038        #[derive(Serialize)]
1039        struct Tuple {
1040            tuple_items: Vec<Value>,
1041        }
1042
1043        let outer = Outer {
1044            metadata: "test".to_string(),
1045            content: Tagged::Input(Inner {
1046                kind: Kind::PythonExpression(PythonExpr {
1047                    solution: Value::Tuple(Tuple {
1048                        tuple_items: vec![
1049                            Value::Int(10),
1050                            Value::Tuple(Tuple {
1051                                tuple_items: vec![Value::Int(8), Value::Int(5)],
1052                            }),
1053                        ],
1054                    }),
1055                    feedback: None,
1056                }),
1057                points: 100,
1058            }),
1059        };
1060
1061        let yaml = outer.serialize(YamlSerializer).unwrap();
1062        let doc = yaml.pretty();
1063        use crate::yaml::{AliasedYaml, DocumentData};
1064        let document = crate::yaml::Document {
1065            leading_comments: vec![],
1066            items: vec![DocumentData::Yaml(doc)],
1067        };
1068        let output = document.format().unwrap();
1069        println!("YAML output:\n{}", output);
1070
1071        // `feedback` and `points` must be siblings of `solution`, not nested inside tuple_items.
1072        assert!(
1073            !output.contains("- 5\n      feedback") && !output.contains("- 5\n              feedback"),
1074            "feedback must not be nested inside tuple_items:\n{}", output
1075        );
1076        assert!(output.contains("\nfeedback:") || output.contains("\npoints:") || output.contains("  feedback:") || output.contains("  points:"),
1077            "feedback/points should appear at the input level:\n{}", output);
1078    }
1079}