Skip to main content

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