styx_format/
value_format.rs

1//! Format `styx_tree::Value` to Styx text.
2
3use styx_tree::{Entry, Object, Payload, Sequence, Value};
4
5use crate::{FormatOptions, StyxWriter};
6
7/// Format a Value as a Styx document string.
8///
9/// The value is treated as the root of a document, so if it's an Object,
10/// it will be formatted without braces (implicit root object).
11pub fn format_value(value: &Value, options: FormatOptions) -> String {
12    let mut formatter = ValueFormatter::new(options);
13    formatter.format_root(value);
14    formatter.finish_document()
15}
16
17/// Format a Value as a Styx document string with default options.
18pub fn format_value_default(value: &Value) -> String {
19    format_value(value, FormatOptions::default())
20}
21
22/// Format an Object directly (with braces), not as a root document.
23///
24/// This is useful for code actions that need to format a single object
25/// while respecting its separator style.
26pub fn format_object_braced(obj: &Object, options: FormatOptions) -> String {
27    let mut formatter = ValueFormatter::new(options);
28    formatter.format_object(obj);
29    formatter.finish()
30}
31
32struct ValueFormatter {
33    writer: StyxWriter,
34}
35
36impl ValueFormatter {
37    fn new(options: FormatOptions) -> Self {
38        Self {
39            writer: StyxWriter::with_options(options),
40        }
41    }
42
43    /// Finish and return output without trailing newline.
44    fn finish(self) -> String {
45        self.writer.finish_string()
46    }
47
48    /// Finish and return output with trailing newline (for documents).
49    fn finish_document(self) -> String {
50        String::from_utf8(self.writer.finish_document())
51            .expect("Styx output should always be valid UTF-8")
52    }
53
54    fn format_root(&mut self, value: &Value) {
55        // Root is typically an untagged object
56        if value.tag.is_none()
57            && let Some(Payload::Object(obj)) = &value.payload
58        {
59            // Root object - no braces
60            self.writer.begin_struct(true);
61            self.format_object_entries(obj);
62            self.writer.end_struct().ok();
63            return;
64        }
65        // Non-object root or tagged root - just format the value
66        self.format_value(value);
67    }
68
69    fn format_value(&mut self, value: &Value) {
70        let has_tag = value.tag.is_some();
71
72        // Write tag if present
73        if let Some(tag) = &value.tag {
74            self.writer.write_tag(&tag.name);
75        }
76
77        // Write payload if present
78        match &value.payload {
79            None => {
80                // No payload - if no tag either, this is unit (@)
81                if !has_tag {
82                    self.writer.write_str("@");
83                }
84                // If there's a tag but no payload, tag was already written
85            }
86            Some(Payload::Scalar(s)) => {
87                // If tagged, wrap scalar in parens: @tag(scalar)
88                if has_tag {
89                    self.writer.begin_seq_after_tag();
90                    self.writer.write_scalar(&s.text);
91                    self.writer.end_seq().ok();
92                } else {
93                    self.writer.write_scalar(&s.text);
94                }
95            }
96            Some(Payload::Sequence(seq)) => {
97                // If tagged, sequence attaches directly: @tag(...)
98                self.format_sequence_inner(seq, has_tag);
99            }
100            Some(Payload::Object(obj)) => {
101                // If tagged, object attaches directly: @tag{...}
102                self.format_object_inner(obj, has_tag);
103            }
104        }
105    }
106
107    fn format_sequence_inner(&mut self, seq: &Sequence, after_tag: bool) {
108        if after_tag {
109            self.writer.begin_seq_after_tag();
110        } else {
111            self.writer.begin_seq();
112        }
113        for item in &seq.items {
114            self.format_value(item);
115        }
116        self.writer.end_seq().ok();
117    }
118
119    fn format_object(&mut self, obj: &Object) {
120        self.format_object_inner(obj, false);
121    }
122
123    fn format_object_inner(&mut self, obj: &Object, after_tag: bool) {
124        // Preserve the original separator style - if it was newline-separated, keep it multiline
125        let force_multiline = matches!(obj.separator, styx_parse::Separator::Newline);
126        if after_tag {
127            self.writer.begin_struct_after_tag(force_multiline);
128        } else {
129            self.writer
130                .begin_struct_with_options(false, force_multiline);
131        }
132        self.format_object_entries(obj);
133        self.writer.end_struct().ok();
134    }
135
136    fn format_object_entries(&mut self, obj: &Object) {
137        for entry in &obj.entries {
138            self.format_entry(entry);
139        }
140    }
141
142    fn format_entry(&mut self, entry: &Entry) {
143        // Format the key (which is itself a Value - scalar or unit, optionally tagged)
144        let key_str = self.format_key(&entry.key);
145
146        // Write doc comment + key together, or just key
147        if let Some(doc) = &entry.doc_comment {
148            self.writer.write_doc_comment_and_key_raw(doc, &key_str);
149        } else {
150            self.writer.field_key_raw(&key_str).ok();
151        }
152
153        self.format_value(&entry.value);
154    }
155
156    /// Format a key value to string.
157    /// Keys are scalars or unit, optionally tagged.
158    fn format_key(&self, key: &Value) -> String {
159        let mut result = String::new();
160
161        // Tag prefix if present
162        if let Some(tag) = &key.tag {
163            result.push('@');
164            result.push_str(&tag.name);
165        }
166
167        // Payload (scalar text or unit)
168        match &key.payload {
169            None => {
170                // Unit - if no tag, write @
171                if key.tag.is_none() {
172                    result.push('@');
173                }
174                // If tagged with no payload, tag is already written (e.g., @schema)
175            }
176            Some(Payload::Scalar(s)) => {
177                // Always check if the text can be bare, regardless of original ScalarKind
178                if crate::scalar::can_be_bare(&s.text) {
179                    result.push_str(&s.text);
180                } else {
181                    result.push('"');
182                    result.push_str(&crate::scalar::escape_quoted(&s.text));
183                    result.push('"');
184                }
185            }
186            Some(Payload::Sequence(_) | Payload::Object(_)) => {
187                panic!("object key cannot be a sequence or object: {:?}", key);
188            }
189        }
190
191        result
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use styx_parse::{ScalarKind, Separator};
199    use styx_tree::{Object, Payload, Scalar, Sequence, Tag};
200
201    fn scalar(text: &str) -> Value {
202        Value {
203            tag: None,
204            payload: Some(Payload::Scalar(Scalar {
205                text: text.to_string(),
206                kind: ScalarKind::Bare,
207                span: None,
208            })),
209            span: None,
210        }
211    }
212
213    fn tagged(name: &str) -> Value {
214        Value {
215            tag: Some(Tag {
216                name: name.to_string(),
217                span: None,
218            }),
219            payload: None,
220            span: None,
221        }
222    }
223
224    fn entry(key: &str, value: Value) -> Entry {
225        Entry {
226            key: scalar(key),
227            value,
228            doc_comment: None,
229        }
230    }
231
232    fn entry_with_doc(key: &str, value: Value, doc: &str) -> Entry {
233        Entry {
234            key: scalar(key),
235            value,
236            doc_comment: Some(doc.to_string()),
237        }
238    }
239
240    fn obj_value(entries: Vec<Entry>) -> Value {
241        Value {
242            tag: None,
243            payload: Some(Payload::Object(Object {
244                entries,
245                separator: Separator::Newline,
246                span: None,
247            })),
248            span: None,
249        }
250    }
251
252    fn seq_value(items: Vec<Value>) -> Value {
253        Value {
254            tag: None,
255            payload: Some(Payload::Sequence(Sequence { items, span: None })),
256            span: None,
257        }
258    }
259
260    #[test]
261    fn test_format_simple_object() {
262        let obj = obj_value(vec![
263            entry("name", scalar("Alice")),
264            entry("age", scalar("30")),
265        ]);
266
267        let result = format_value_default(&obj);
268        insta::assert_snapshot!(result);
269    }
270
271    #[test]
272    fn test_format_nested_object() {
273        let inner = Value {
274            tag: None,
275            payload: Some(Payload::Object(Object {
276                entries: vec![entry("name", scalar("Alice")), entry("age", scalar("30"))],
277                separator: Separator::Comma,
278                span: None,
279            })),
280            span: None,
281        };
282
283        let obj = obj_value(vec![entry("user", inner)]);
284
285        let result = format_value_default(&obj);
286        insta::assert_snapshot!(result);
287    }
288
289    #[test]
290    fn test_format_tagged() {
291        let obj = obj_value(vec![entry("type", tagged("string"))]);
292
293        let result = format_value_default(&obj);
294        insta::assert_snapshot!(result);
295    }
296
297    #[test]
298    fn test_format_sequence() {
299        let seq = seq_value(vec![scalar("a"), scalar("b"), scalar("c")]);
300
301        let obj = obj_value(vec![entry("items", seq)]);
302
303        let result = format_value_default(&obj);
304        insta::assert_snapshot!(result);
305    }
306
307    #[test]
308    fn test_format_with_doc_comments() {
309        let obj = obj_value(vec![
310            entry_with_doc("name", scalar("Alice"), "The user's name"),
311            entry_with_doc("age", scalar("30"), "Age in years"),
312        ]);
313
314        let result = format_value_default(&obj);
315        insta::assert_snapshot!(result);
316    }
317
318    #[test]
319    fn test_format_unit() {
320        let obj = obj_value(vec![entry("flag", Value::unit())]);
321
322        let result = format_value_default(&obj);
323        insta::assert_snapshot!(result);
324    }
325
326    // =========================================================================
327    // Edge case tests for formatting
328    // =========================================================================
329
330    /// Helper to create a newline-separated object value
331    fn obj_multiline(entries: Vec<Entry>) -> Value {
332        Value {
333            tag: None,
334            payload: Some(Payload::Object(Object {
335                entries,
336                separator: Separator::Newline,
337                span: None,
338            })),
339            span: None,
340        }
341    }
342
343    /// Helper to create a comma-separated (inline) object value
344    fn obj_inline(entries: Vec<Entry>) -> Value {
345        Value {
346            tag: None,
347            payload: Some(Payload::Object(Object {
348                entries,
349                separator: Separator::Comma,
350                span: None,
351            })),
352            span: None,
353        }
354    }
355
356    /// Helper to create a tagged value with object payload
357    fn tagged_obj(tag_name: &str, entries: Vec<Entry>, separator: Separator) -> Value {
358        Value {
359            tag: Some(Tag {
360                name: tag_name.to_string(),
361                span: None,
362            }),
363            payload: Some(Payload::Object(Object {
364                entries,
365                separator,
366                span: None,
367            })),
368            span: None,
369        }
370    }
371
372    /// Helper to create a tagged value with a single scalar payload
373    fn tagged_scalar(tag_name: &str, text: &str) -> Value {
374        Value {
375            tag: Some(Tag {
376                name: tag_name.to_string(),
377                span: None,
378            }),
379            payload: Some(Payload::Scalar(Scalar {
380                text: text.to_string(),
381                kind: ScalarKind::Bare,
382                span: None,
383            })),
384            span: None,
385        }
386    }
387
388    /// Helper to create a unit entry (@ key)
389    fn unit_entry(value: Value) -> Entry {
390        Entry {
391            key: Value::unit(),
392            value,
393            doc_comment: None,
394        }
395    }
396
397    /// Helper to create a schema declaration entry (@schema key)
398    fn schema_entry(value: Value) -> Entry {
399        Entry {
400            key: Value::tag("schema"),
401            value,
402            doc_comment: None,
403        }
404    }
405
406    // --- Edge Case 1: Schema declaration with blank line after ---
407    #[test]
408    fn test_edge_case_01_schema_declaration_blank_line() {
409        // @schema schema.styx followed by other fields should have blank line
410        let obj = obj_multiline(vec![
411            schema_entry(scalar("schema.styx")),
412            entry("name", scalar("test")),
413            entry("port", scalar("8080")),
414        ]);
415
416        let result = format_value_default(&obj);
417        insta::assert_snapshot!(result);
418    }
419
420    // --- Edge Case 2: Nested multiline objects preserve structure ---
421    #[test]
422    fn test_edge_case_02_nested_multiline_objects() {
423        let inner = obj_multiline(vec![
424            entry("host", scalar("localhost")),
425            entry("port", scalar("8080")),
426        ]);
427        let obj = obj_multiline(vec![entry("name", scalar("myapp")), entry("server", inner)]);
428
429        let result = format_value_default(&obj);
430        insta::assert_snapshot!(result);
431    }
432
433    // --- Edge Case 3: Deeply nested multiline objects (3 levels) ---
434    #[test]
435    fn test_edge_case_03_deeply_nested_multiline() {
436        let level3 = obj_multiline(vec![
437            entry("cert", scalar("/path/to/cert")),
438            entry("key", scalar("/path/to/key")),
439        ]);
440        let level2 = obj_multiline(vec![
441            entry("host", scalar("localhost")),
442            entry("tls", level3),
443        ]);
444        let obj = obj_multiline(vec![
445            entry("name", scalar("myapp")),
446            entry("server", level2),
447        ]);
448
449        let result = format_value_default(&obj);
450        insta::assert_snapshot!(result);
451    }
452
453    // --- Edge Case 4: Mixed inline and multiline ---
454    #[test]
455    fn test_edge_case_04_mixed_inline_multiline() {
456        // Outer is multiline, inner is inline
457        let inner = obj_inline(vec![entry("x", scalar("1")), entry("y", scalar("2"))]);
458        let obj = obj_multiline(vec![entry("name", scalar("point")), entry("coords", inner)]);
459
460        let result = format_value_default(&obj);
461        insta::assert_snapshot!(result);
462    }
463
464    // --- Edge Case 5: Tagged object with multiline content ---
465    #[test]
466    fn test_edge_case_05_tagged_multiline_object() {
467        let obj = obj_multiline(vec![entry(
468            "type",
469            tagged_obj(
470                "object",
471                vec![entry("name", tagged("string")), entry("age", tagged("int"))],
472                Separator::Newline,
473            ),
474        )]);
475
476        let result = format_value_default(&obj);
477        insta::assert_snapshot!(result);
478    }
479
480    // --- Edge Case 6: Tagged object with inline content ---
481    #[test]
482    fn test_edge_case_06_tagged_inline_object() {
483        let obj = obj_multiline(vec![entry(
484            "point",
485            tagged_obj(
486                "point",
487                vec![entry("x", scalar("1")), entry("y", scalar("2"))],
488                Separator::Comma,
489            ),
490        )]);
491
492        let result = format_value_default(&obj);
493        insta::assert_snapshot!(result);
494    }
495
496    // --- Edge Case 7: Schema-like structure with @object tags ---
497    #[test]
498    fn test_edge_case_07_schema_structure() {
499        // meta { ... }
500        // schema { @ @object{ ... } }
501        let meta = obj_multiline(vec![
502            entry("id", scalar("https://example.com/schema")),
503            entry("version", scalar("1.0")),
504        ]);
505        let schema_obj = tagged_obj(
506            "object",
507            vec![
508                entry("name", tagged("string")),
509                entry("port", tagged("int")),
510            ],
511            Separator::Newline,
512        );
513        let schema = obj_multiline(vec![unit_entry(schema_obj)]);
514        let root = obj_multiline(vec![entry("meta", meta), entry("schema", schema)]);
515
516        let result = format_value_default(&root);
517        insta::assert_snapshot!(result);
518    }
519
520    // --- Edge Case 8: Optional wrapped types ---
521    #[test]
522    fn test_edge_case_08_optional_types() {
523        let obj = obj_multiline(vec![
524            entry("required", tagged("string")),
525            entry("optional", tagged_scalar("optional", "@bool")),
526        ]);
527
528        let result = format_value_default(&obj);
529        insta::assert_snapshot!(result);
530    }
531
532    // --- Edge Case 9: Empty object ---
533    #[test]
534    fn test_edge_case_09_empty_object() {
535        let obj = obj_multiline(vec![entry("empty", obj_multiline(vec![]))]);
536
537        let result = format_value_default(&obj);
538        insta::assert_snapshot!(result);
539    }
540
541    // --- Edge Case 10: Empty inline object ---
542    #[test]
543    fn test_edge_case_10_empty_inline_object() {
544        let obj = obj_multiline(vec![entry("empty", obj_inline(vec![]))]);
545
546        let result = format_value_default(&obj);
547        insta::assert_snapshot!(result);
548    }
549
550    // --- Edge Case 11: Sequence of objects ---
551    #[test]
552    fn test_edge_case_11_sequence_of_objects() {
553        let item1 = obj_inline(vec![entry("name", scalar("Alice"))]);
554        let item2 = obj_inline(vec![entry("name", scalar("Bob"))]);
555        let seq = Value {
556            tag: None,
557            payload: Some(Payload::Sequence(Sequence {
558                items: vec![item1, item2],
559                span: None,
560            })),
561            span: None,
562        };
563        let obj = obj_multiline(vec![entry("users", seq)]);
564
565        let result = format_value_default(&obj);
566        insta::assert_snapshot!(result);
567    }
568
569    // --- Edge Case 12: Quoted strings that need escaping ---
570    #[test]
571    fn test_edge_case_12_quoted_strings() {
572        let obj = obj_multiline(vec![
573            entry("message", scalar(r#""Hello, World!""#)),
574            entry("path", scalar("/path/with spaces/file.txt")),
575        ]);
576
577        let result = format_value_default(&obj);
578        insta::assert_snapshot!(result);
579    }
580
581    // --- Edge Case 13: Keys that need quoting ---
582    #[test]
583    fn test_edge_case_13_quoted_keys() {
584        let obj = obj_multiline(vec![
585            entry("normal-key", scalar("value1")),
586            entry("key with spaces", scalar("value2")),
587            entry("123numeric", scalar("value3")),
588        ]);
589
590        let result = format_value_default(&obj);
591        insta::assert_snapshot!(result);
592    }
593
594    // --- Edge Case 14: Schema declaration (now using @schema tag) ---
595    #[test]
596    fn test_edge_case_14_schema_declaration() {
597        let obj = obj_multiline(vec![
598            schema_entry(scalar("first.styx")),
599            entry("name", scalar("test")),
600        ]);
601
602        let result = format_value_default(&obj);
603        insta::assert_snapshot!(result);
604    }
605
606    // --- Edge Case 15: Nested sequences ---
607    #[test]
608    fn test_edge_case_15_nested_sequences() {
609        let inner_seq = seq_value(vec![scalar("a"), scalar("b")]);
610        let outer_seq = Value {
611            tag: None,
612            payload: Some(Payload::Sequence(Sequence {
613                items: vec![inner_seq, seq_value(vec![scalar("c"), scalar("d")])],
614                span: None,
615            })),
616            span: None,
617        };
618        let obj = obj_multiline(vec![entry("matrix", outer_seq)]);
619
620        let result = format_value_default(&obj);
621        insta::assert_snapshot!(result);
622    }
623
624    // --- Edge Case 16: Tagged sequence ---
625    #[test]
626    fn test_edge_case_16_tagged_sequence() {
627        let tagged_seq = Value {
628            tag: Some(Tag {
629                name: "seq".to_string(),
630                span: None,
631            }),
632            payload: Some(Payload::Sequence(Sequence {
633                items: vec![tagged("string")],
634                span: None,
635            })),
636            span: None,
637        };
638        let obj = obj_multiline(vec![entry("items", tagged_seq)]);
639
640        let result = format_value_default(&obj);
641        insta::assert_snapshot!(result);
642    }
643
644    // --- Edge Case 17: Doc comments on nested entries ---
645    #[test]
646    fn test_edge_case_17_nested_doc_comments() {
647        let inner = Value {
648            tag: None,
649            payload: Some(Payload::Object(Object {
650                entries: vec![
651                    entry_with_doc("host", scalar("localhost"), "The server hostname"),
652                    entry_with_doc("port", scalar("8080"), "The server port"),
653                ],
654                separator: Separator::Newline,
655                span: None,
656            })),
657            span: None,
658        };
659        let obj = obj_multiline(vec![entry_with_doc(
660            "server",
661            inner,
662            "Server configuration",
663        )]);
664
665        let result = format_value_default(&obj);
666        insta::assert_snapshot!(result);
667    }
668
669    // --- Edge Case 18: Very long inline object should stay inline if marked ---
670    #[test]
671    fn test_edge_case_18_long_inline_stays_inline() {
672        let inner = obj_inline(vec![
673            entry("field1", scalar("value1")),
674            entry("field2", scalar("value2")),
675            entry("field3", scalar("value3")),
676            entry("field4", scalar("value4")),
677        ]);
678        let obj = obj_multiline(vec![entry("data", inner)]);
679
680        let result = format_value_default(&obj);
681        insta::assert_snapshot!(result);
682    }
683
684    // --- Edge Case 19: Multiline with single field ---
685    #[test]
686    fn test_edge_case_19_multiline_single_field() {
687        let inner = obj_multiline(vec![entry("only", scalar("one"))]);
688        let obj = obj_multiline(vec![entry("wrapper", inner)]);
689
690        let result = format_value_default(&obj);
691        insta::assert_snapshot!(result);
692    }
693
694    // --- Edge Case 20: Full schema file simulation ---
695    #[test]
696    fn test_edge_case_20_full_schema_simulation() {
697        // Simulates: meta { id ..., version ..., description ... }
698        //            schema { @ @object{ name @string, server @object{ host @string, port @int } } }
699        let meta = obj_multiline(vec![
700            entry("id", scalar("https://example.com/config")),
701            entry("version", scalar("2024-01-01")),
702            entry("description", scalar("\"A test schema\"")),
703        ]);
704
705        let _server_fields = obj_multiline(vec![
706            entry("host", tagged("string")),
707            entry("port", tagged("int")),
708        ]);
709        let server_schema = tagged_obj(
710            "object",
711            vec![
712                entry("host", tagged("string")),
713                entry("port", tagged("int")),
714            ],
715            Separator::Newline,
716        );
717
718        let root_schema = tagged_obj(
719            "object",
720            vec![
721                entry("name", tagged("string")),
722                entry("server", server_schema),
723            ],
724            Separator::Newline,
725        );
726
727        let schema = obj_multiline(vec![unit_entry(root_schema)]);
728
729        let root = obj_multiline(vec![entry("meta", meta), entry("schema", schema)]);
730
731        let result = format_value_default(&root);
732        insta::assert_snapshot!(result);
733    }
734
735    // =========================================================================
736    // Blank line behavior tests - testing against CST formatter as source of truth
737    // =========================================================================
738
739    /// Test that ValueFormatter output matches CST formatter for the same input.
740    /// This is the key idempotency property we need.
741    fn assert_matches_cst_formatter(value: &Value, description: &str) {
742        let value_output = format_value_default(value);
743        let cst_output = crate::format_source(&value_output, crate::FormatOptions::default());
744        assert_eq!(
745            value_output, cst_output,
746            "{}: ValueFormatter output should match CST formatter.\n\
747             ValueFormatter produced:\n{}\n\
748             CST formatter would produce:\n{}",
749            description, value_output, cst_output
750        );
751    }
752
753    #[test]
754    fn blank_line_01_two_scalars_at_root() {
755        // Two scalar entries at root - no blank line needed
756        let obj = obj_multiline(vec![
757            entry("name", scalar("Alice")),
758            entry("age", scalar("30")),
759        ]);
760        assert_matches_cst_formatter(&obj, "two scalars at root");
761    }
762
763    #[test]
764    fn blank_line_02_three_scalars_at_root() {
765        let obj = obj_multiline(vec![
766            entry("a", scalar("1")),
767            entry("b", scalar("2")),
768            entry("c", scalar("3")),
769        ]);
770        assert_matches_cst_formatter(&obj, "three scalars at root");
771    }
772
773    #[test]
774    fn blank_line_03_scalar_then_block() {
775        // Scalar followed by block object - needs blank line before block
776        let block = obj_multiline(vec![
777            entry("host", scalar("localhost")),
778            entry("port", scalar("8080")),
779        ]);
780        let obj = obj_multiline(vec![entry("name", scalar("myapp")), entry("server", block)]);
781        assert_matches_cst_formatter(&obj, "scalar then block");
782    }
783
784    #[test]
785    fn blank_line_04_block_then_scalar() {
786        // Block followed by scalar - needs blank line after block
787        let block = obj_multiline(vec![entry("host", scalar("localhost"))]);
788        let obj = obj_multiline(vec![entry("server", block), entry("name", scalar("myapp"))]);
789        assert_matches_cst_formatter(&obj, "block then scalar");
790    }
791
792    #[test]
793    fn blank_line_05_two_blocks() {
794        // Two block objects - needs blank line between them
795        let block1 = obj_multiline(vec![entry("a", scalar("1"))]);
796        let block2 = obj_multiline(vec![entry("b", scalar("2"))]);
797        let obj = obj_multiline(vec![entry("first", block1), entry("second", block2)]);
798        assert_matches_cst_formatter(&obj, "two blocks");
799    }
800
801    #[test]
802    fn blank_line_06_inline_objects_at_root() {
803        // Inline objects at root - no blank line needed
804        let inline1 = obj_inline(vec![entry("x", scalar("1"))]);
805        let inline2 = obj_inline(vec![entry("y", scalar("2"))]);
806        let obj = obj_multiline(vec![entry("point1", inline1), entry("point2", inline2)]);
807        assert_matches_cst_formatter(&obj, "inline objects at root");
808    }
809
810    #[test]
811    fn blank_line_07_scalar_inline_scalar() {
812        let inline = obj_inline(vec![entry("x", scalar("1"))]);
813        let obj = obj_multiline(vec![
814            entry("name", scalar("test")),
815            entry("point", inline),
816            entry("count", scalar("5")),
817        ]);
818        assert_matches_cst_formatter(&obj, "scalar inline scalar");
819    }
820
821    #[test]
822    fn blank_line_08_doc_comment_entries() {
823        // Entries with doc comments
824        let obj = obj_multiline(vec![
825            entry_with_doc("name", scalar("Alice"), "The user's name"),
826            entry_with_doc("age", scalar("30"), "Age in years"),
827        ]);
828        assert_matches_cst_formatter(&obj, "doc comment entries");
829    }
830
831    #[test]
832    fn blank_line_09_mixed_doc_and_plain() {
833        let obj = obj_multiline(vec![
834            entry("plain", scalar("1")),
835            entry_with_doc("documented", scalar("2"), "Has docs"),
836            entry("another_plain", scalar("3")),
837        ]);
838        assert_matches_cst_formatter(&obj, "mixed doc and plain");
839    }
840
841    #[test]
842    fn blank_line_10_schema_declaration_first() {
843        // Schema declaration at start should have blank line after
844        let obj = obj_multiline(vec![
845            schema_entry(scalar("schema.styx")),
846            entry("name", scalar("test")),
847        ]);
848        assert_matches_cst_formatter(&obj, "schema declaration first");
849    }
850
851    #[test]
852    fn blank_line_11_nested_blocks_dont_get_extra_blanks() {
853        // Inside a non-root object, no extra blank lines
854        let inner = obj_multiline(vec![entry("a", scalar("1")), entry("b", scalar("2"))]);
855        let obj = obj_multiline(vec![entry("wrapper", inner)]);
856        assert_matches_cst_formatter(&obj, "nested block internal");
857    }
858
859    #[test]
860    fn blank_line_12_deeply_nested() {
861        let level3 = obj_multiline(vec![entry("deep", scalar("value"))]);
862        let level2 = obj_multiline(vec![entry("inner", level3)]);
863        let obj = obj_multiline(vec![
864            entry("outer", level2),
865            entry("sibling", scalar("test")),
866        ]);
867        assert_matches_cst_formatter(&obj, "deeply nested");
868    }
869
870    #[test]
871    fn blank_line_13_tagged_block() {
872        let tagged_block = tagged_obj(
873            "object",
874            vec![entry("field", tagged("string"))],
875            Separator::Newline,
876        );
877        let obj = obj_multiline(vec![
878            entry("name", scalar("test")),
879            entry("schema", tagged_block),
880        ]);
881        assert_matches_cst_formatter(&obj, "tagged block");
882    }
883
884    #[test]
885    fn blank_line_14_sequence_of_scalars() {
886        let seq = seq_value(vec![scalar("a"), scalar("b"), scalar("c")]);
887        let obj = obj_multiline(vec![entry("items", seq), entry("count", scalar("3"))]);
888        assert_matches_cst_formatter(&obj, "sequence of scalars");
889    }
890
891    #[test]
892    fn blank_line_15_meta_then_schema_block() {
893        // Real-world pattern: meta block followed by schema block
894        let meta = obj_multiline(vec![
895            entry("id", scalar("test")),
896            entry("version", scalar("1")),
897        ]);
898        let schema_content = tagged_obj(
899            "object",
900            vec![entry("name", tagged("string"))],
901            Separator::Newline,
902        );
903        let schema = obj_multiline(vec![unit_entry(schema_content)]);
904        let obj = obj_multiline(vec![entry("meta", meta), entry("schema", schema)]);
905        assert_matches_cst_formatter(&obj, "meta then schema block");
906    }
907
908    #[test]
909    fn blank_line_16_three_blocks() {
910        let b1 = obj_multiline(vec![entry("a", scalar("1"))]);
911        let b2 = obj_multiline(vec![entry("b", scalar("2"))]);
912        let b3 = obj_multiline(vec![entry("c", scalar("3"))]);
913        let obj = obj_multiline(vec![
914            entry("first", b1),
915            entry("second", b2),
916            entry("third", b3),
917        ]);
918        assert_matches_cst_formatter(&obj, "three blocks");
919    }
920
921    #[test]
922    fn blank_line_17_block_with_doc_comment() {
923        let block = obj_multiline(vec![entry("inner", scalar("value"))]);
924        let obj = obj_multiline(vec![
925            entry_with_doc("config", block, "Configuration section"),
926            entry("name", scalar("test")),
927        ]);
928        assert_matches_cst_formatter(&obj, "block with doc comment");
929    }
930
931    #[test]
932    fn blank_line_18_empty_inline_objects() {
933        let empty1 = obj_inline(vec![]);
934        let empty2 = obj_inline(vec![]);
935        let obj = obj_multiline(vec![entry("a", empty1), entry("b", empty2)]);
936        assert_matches_cst_formatter(&obj, "empty inline objects");
937    }
938
939    #[test]
940    fn blank_line_19_single_entry_block() {
941        let block = obj_multiline(vec![entry("only", scalar("one"))]);
942        let obj = obj_multiline(vec![entry("wrapper", block)]);
943        assert_matches_cst_formatter(&obj, "single entry block");
944    }
945
946    #[test]
947    fn blank_line_20_alternating_scalar_block() {
948        let b1 = obj_multiline(vec![entry("x", scalar("1"))]);
949        let b2 = obj_multiline(vec![entry("y", scalar("2"))]);
950        let obj = obj_multiline(vec![
951            entry("s1", scalar("a")),
952            entry("block1", b1),
953            entry("s2", scalar("b")),
954            entry("block2", b2),
955            entry("s3", scalar("c")),
956        ]);
957        assert_matches_cst_formatter(&obj, "alternating scalar block");
958    }
959}