facet_diff_core/layout/
flavor.rs

1//! Diff output flavors (Rust, JSON, XML).
2//!
3//! Each flavor knows how to present struct fields and format values
4//! according to its format's conventions.
5
6use std::borrow::Cow;
7use std::fmt::Write;
8
9use facet_core::{Def, Field, PrimitiveType, Type};
10use facet_reflect::Peek;
11
12/// How a field should be presented in the diff output.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum FieldPresentation {
15    /// Show as an inline attribute/field on the struct line.
16    /// - Rust: `x: 10`
17    /// - JSON: `"x": 10`
18    /// - XML: `x="10"` (as attribute on opening tag)
19    Attribute {
20        /// The field name (possibly renamed)
21        name: Cow<'static, str>,
22    },
23
24    /// Show as a nested child element.
25    /// - XML: `<title>...</title>` as child element
26    /// - Rust/JSON: same as Attribute (nested structs are inline)
27    Child {
28        /// The element/field name
29        name: Cow<'static, str>,
30    },
31
32    /// Show as text content inside the parent.
33    /// - XML: `<p>this text</p>`
34    /// - Rust/JSON: same as Attribute
35    TextContent,
36
37    /// Show as multiple child elements (for sequences).
38    /// - XML: `<Item/><Item/>` as siblings
39    /// - Rust/JSON: same as Attribute (sequences are `[...]`)
40    Children {
41        /// The name for each item element
42        item_name: Cow<'static, str>,
43    },
44}
45
46/// A diff output flavor that knows how to format values and present fields.
47pub trait DiffFlavor {
48    /// Format a scalar/leaf value into a writer.
49    ///
50    /// The output should NOT include surrounding quotes for strings -
51    /// the renderer will add appropriate syntax based on context.
52    fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result;
53
54    /// Determine how a field should be presented.
55    fn field_presentation(&self, field: &Field) -> FieldPresentation;
56
57    /// Opening syntax for a struct/object.
58    /// - Rust: `Point {`
59    /// - JSON: `{`
60    /// - XML: `<Point`
61    fn struct_open(&self, name: &str) -> Cow<'static, str>;
62
63    /// Closing syntax for a struct/object.
64    /// - Rust: `}`
65    /// - JSON: `}`
66    /// - XML: `/>` (self-closing) or `</Point>`
67    fn struct_close(&self, name: &str, self_closing: bool) -> Cow<'static, str>;
68
69    /// Separator between fields.
70    /// - Rust: `, `
71    /// - JSON: `, `
72    /// - XML: ` ` (space between attributes)
73    fn field_separator(&self) -> &'static str;
74
75    /// Trailing comma/separator (no trailing space).
76    /// Used at end of lines when fields are broken across lines.
77    /// - Rust: `,`
78    /// - JSON: `,`
79    /// - XML: `` (empty - no trailing separator)
80    fn trailing_separator(&self) -> &'static str {
81        ","
82    }
83
84    /// Opening syntax for a sequence/array.
85    /// - Rust: `[`
86    /// - JSON: `[`
87    /// - XML: (wrapper element, handled differently)
88    fn seq_open(&self) -> Cow<'static, str>;
89
90    /// Closing syntax for a sequence/array.
91    /// - Rust: `]`
92    /// - JSON: `]`
93    /// - XML: (wrapper element, handled differently)
94    fn seq_close(&self) -> Cow<'static, str>;
95
96    /// Separator between sequence items.
97    /// - Rust: `, `
98    /// - JSON: `,`
99    /// - XML: (newlines/whitespace)
100    fn item_separator(&self) -> &'static str;
101
102    /// Format a sequence item value, optionally wrapping in element tags.
103    /// - Rust: `0` (no wrapping)
104    /// - JSON: `0` (no wrapping)
105    /// - XML: `<i32>0</i32>` (wrapped in element)
106    fn format_seq_item<'a>(&self, _item_type: &str, value: &'a str) -> Cow<'a, str> {
107        // Default: no wrapping, just return the value
108        Cow::Borrowed(value)
109    }
110
111    /// Opening for a sequence that is a struct field value.
112    /// - Rust: `fieldname: [`
113    /// - JSON: `"fieldname": [`
114    /// - XML: `<fieldname>` (wrapper element, not attribute!)
115    fn format_seq_field_open(&self, field_name: &str) -> String {
116        // Default: use field prefix + seq_open
117        format!(
118            "{}{}",
119            self.format_field_prefix(field_name),
120            self.seq_open()
121        )
122    }
123
124    /// Closing for a sequence that is a struct field value.
125    /// - Rust: `]`
126    /// - JSON: `]`
127    /// - XML: `</fieldname>`
128    fn format_seq_field_close(&self, _field_name: &str) -> Cow<'static, str> {
129        // Default: just seq_close
130        self.seq_close()
131    }
132
133    /// Format a comment (for collapsed items).
134    /// - Rust: `/* ...5 more */`
135    /// - JSON: `// ...5 more`
136    /// - XML: `<!-- ...5 more -->`
137    fn comment(&self, text: &str) -> String;
138
139    /// Format a field assignment (name and value).
140    /// - Rust: `name: value`
141    /// - JSON: `"name": value`
142    /// - XML: `name="value"`
143    fn format_field(&self, name: &str, value: &str) -> String;
144
145    /// Format just the field name with assignment operator.
146    /// - Rust: `name: `
147    /// - JSON: `"name": `
148    /// - XML: `name="`
149    fn format_field_prefix(&self, name: &str) -> String;
150
151    /// Suffix after the value (if any).
152    /// - Rust: `` (empty)
153    /// - JSON: `` (empty)
154    /// - XML: `"` (closing quote)
155    fn format_field_suffix(&self) -> &'static str;
156
157    /// Close the opening tag when there are children.
158    /// - Rust: `` (empty - no separate closing for opening tag)
159    /// - JSON: `` (empty)
160    /// - XML: `>` (close the opening tag before children)
161    fn struct_open_close(&self) -> &'static str {
162        ""
163    }
164
165    /// Optional type name comment to show after struct_open.
166    /// Rendered in muted color for readability.
167    /// - Rust: None (type name is in struct_open)
168    /// - JSON: Some("/* Point */")
169    /// - XML: None
170    fn type_comment(&self, _name: &str) -> Option<String> {
171        None
172    }
173
174    /// Opening wrapper for a child element (nested struct field).
175    /// - Rust: `field_name: ` (field prefix)
176    /// - JSON: `"field_name": ` (field prefix)
177    /// - XML: `` (empty - no wrapper, or could be `<field_name>\n`)
178    fn format_child_open(&self, name: &str) -> Cow<'static, str> {
179        // Default: use field prefix (works for Rust/JSON)
180        Cow::Owned(self.format_field_prefix(name))
181    }
182
183    /// Closing wrapper for a child element (nested struct field).
184    /// - Rust: `` (empty)
185    /// - JSON: `` (empty)
186    /// - XML: `` (empty, or `</field_name>` if wrapping)
187    fn format_child_close(&self, _name: &str) -> Cow<'static, str> {
188        Cow::Borrowed("")
189    }
190}
191
192/// Rust-style output flavor.
193///
194/// Produces output like: `Point { x: 10, y: 20 }`
195#[derive(Debug, Clone, Default)]
196pub struct RustFlavor;
197
198impl DiffFlavor for RustFlavor {
199    fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
200        format_value_quoted(peek, w)
201    }
202
203    fn field_presentation(&self, field: &Field) -> FieldPresentation {
204        // Rust flavor: all fields are attributes (key: value)
205        FieldPresentation::Attribute {
206            name: Cow::Borrowed(field.name),
207        }
208    }
209
210    fn struct_open(&self, name: &str) -> Cow<'static, str> {
211        Cow::Owned(format!("{} {{", name))
212    }
213
214    fn struct_close(&self, _name: &str, _self_closing: bool) -> Cow<'static, str> {
215        Cow::Borrowed("}")
216    }
217
218    fn field_separator(&self) -> &'static str {
219        ", "
220    }
221
222    fn seq_open(&self) -> Cow<'static, str> {
223        Cow::Borrowed("[")
224    }
225
226    fn seq_close(&self) -> Cow<'static, str> {
227        Cow::Borrowed("]")
228    }
229
230    fn item_separator(&self) -> &'static str {
231        ", "
232    }
233
234    fn comment(&self, text: &str) -> String {
235        format!("/* {} */", text)
236    }
237
238    fn format_field(&self, name: &str, value: &str) -> String {
239        format!("{}: {}", name, value)
240    }
241
242    fn format_field_prefix(&self, name: &str) -> String {
243        format!("{}: ", name)
244    }
245
246    fn format_field_suffix(&self) -> &'static str {
247        ""
248    }
249}
250
251/// JSON-style output flavor (JSONC with comments for type names).
252///
253/// Produces output like: `{ // Point\n  "x": 10, "y": 20\n}`
254#[derive(Debug, Clone, Default)]
255pub struct JsonFlavor;
256
257impl DiffFlavor for JsonFlavor {
258    fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
259        format_value_quoted(peek, w)
260    }
261
262    fn field_presentation(&self, field: &Field) -> FieldPresentation {
263        // JSON flavor: all fields are attributes ("key": value)
264        FieldPresentation::Attribute {
265            name: Cow::Borrowed(field.name),
266        }
267    }
268
269    fn struct_open(&self, _name: &str) -> Cow<'static, str> {
270        Cow::Borrowed("{")
271    }
272
273    fn type_comment(&self, name: &str) -> Option<String> {
274        Some(format!("/* {} */", name))
275    }
276
277    fn struct_close(&self, _name: &str, _self_closing: bool) -> Cow<'static, str> {
278        Cow::Borrowed("}")
279    }
280
281    fn field_separator(&self) -> &'static str {
282        ", "
283    }
284
285    fn seq_open(&self) -> Cow<'static, str> {
286        Cow::Borrowed("[")
287    }
288
289    fn seq_close(&self) -> Cow<'static, str> {
290        Cow::Borrowed("]")
291    }
292
293    fn item_separator(&self) -> &'static str {
294        ", "
295    }
296
297    fn comment(&self, text: &str) -> String {
298        format!("// {}", text)
299    }
300
301    fn format_field(&self, name: &str, value: &str) -> String {
302        format!("\"{}\": {}", name, value)
303    }
304
305    fn format_field_prefix(&self, name: &str) -> String {
306        format!("\"{}\": ", name)
307    }
308
309    fn format_field_suffix(&self) -> &'static str {
310        ""
311    }
312}
313
314/// XML-style output flavor.
315///
316/// Produces output like: `<Point x="10" y="20"/>`
317///
318/// Respects `#[facet(xml::attribute)]`, `#[facet(xml::element)]`, etc.
319#[derive(Debug, Clone, Default)]
320pub struct XmlFlavor;
321
322impl DiffFlavor for XmlFlavor {
323    fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
324        format_value_raw(peek, w)
325    }
326
327    fn field_presentation(&self, field: &Field) -> FieldPresentation {
328        // Check for XML-specific attributes
329        //
330        // NOTE: We detect XML attributes by namespace string "xml" (e.g., `field.has_attr(Some("xml"), "attribute")`).
331        // This works because the namespace is defined in the `define_attr_grammar!` macro in facet-xml
332        // with `ns "xml"`, NOT by the import alias. So even if someone writes `use facet_xml as html;`
333        // and uses `#[facet(html::attribute)]`, the namespace stored in the attribute is still "xml".
334        // This should be tested to confirm, but not now.
335        if field.has_attr(Some("xml"), "attribute") {
336            FieldPresentation::Attribute {
337                name: Cow::Borrowed(field.name),
338            }
339        } else if field.has_attr(Some("xml"), "elements") {
340            FieldPresentation::Children {
341                item_name: Cow::Borrowed(field.name),
342            }
343        } else if field.has_attr(Some("xml"), "text") {
344            FieldPresentation::TextContent
345        } else if field.has_attr(Some("xml"), "element") {
346            FieldPresentation::Child {
347                name: Cow::Borrowed(field.name),
348            }
349        } else {
350            // Default: treat as child element (XML's default for non-attributed fields)
351            // In XML, fields without explicit annotation typically become child elements
352            FieldPresentation::Child {
353                name: Cow::Borrowed(field.name),
354            }
355        }
356    }
357
358    fn struct_open(&self, name: &str) -> Cow<'static, str> {
359        Cow::Owned(format!("<{}", name))
360    }
361
362    fn struct_close(&self, name: &str, self_closing: bool) -> Cow<'static, str> {
363        if self_closing {
364            Cow::Borrowed("/>")
365        } else {
366            Cow::Owned(format!("</{}>", name))
367        }
368    }
369
370    fn field_separator(&self) -> &'static str {
371        " "
372    }
373
374    fn seq_open(&self) -> Cow<'static, str> {
375        // XML sequences don't need wrapper elements - items render as siblings
376        Cow::Borrowed("")
377    }
378
379    fn seq_close(&self) -> Cow<'static, str> {
380        // XML sequences don't need wrapper elements - items render as siblings
381        Cow::Borrowed("")
382    }
383
384    fn item_separator(&self) -> &'static str {
385        " "
386    }
387
388    fn format_seq_item<'a>(&self, item_type: &str, value: &'a str) -> Cow<'a, str> {
389        // Wrap each item in an element tag: <i32>0</i32>
390        Cow::Owned(format!("<{}>{}</{}>", item_type, value, item_type))
391    }
392
393    fn comment(&self, text: &str) -> String {
394        format!("<!-- {} -->", text)
395    }
396
397    fn format_field(&self, name: &str, value: &str) -> String {
398        format!("{}=\"{}\"", name, value)
399    }
400
401    fn format_field_prefix(&self, name: &str) -> String {
402        format!("{}=\"", name)
403    }
404
405    fn format_field_suffix(&self) -> &'static str {
406        "\""
407    }
408
409    fn struct_open_close(&self) -> &'static str {
410        ">"
411    }
412
413    fn format_child_open(&self, _name: &str) -> Cow<'static, str> {
414        // XML: nested elements don't use attribute-style prefix
415        // The nested element tag is self-describing
416        Cow::Borrowed("")
417    }
418
419    fn format_child_close(&self, _name: &str) -> Cow<'static, str> {
420        Cow::Borrowed("")
421    }
422
423    fn trailing_separator(&self) -> &'static str {
424        // XML doesn't use trailing commas/separators
425        ""
426    }
427
428    fn format_seq_field_open(&self, _field_name: &str) -> String {
429        // XML: sequences render items directly without wrapper elements
430        // The items are children of the parent element
431        String::new()
432    }
433
434    fn format_seq_field_close(&self, _field_name: &str) -> Cow<'static, str> {
435        // XML: sequences render items directly without wrapper elements
436        Cow::Borrowed("")
437    }
438}
439
440/// Value formatting with quotes for strings (Rust/JSON style).
441fn format_value_quoted(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
442    use facet_core::{PointerType, TextualType};
443
444    let shape = peek.shape();
445
446    match (shape.def, shape.ty) {
447        // Strings: write with quotes
448        (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
449            write!(w, "\"{}\"", peek.get::<str>().unwrap())
450        }
451        // String type (owned)
452        (Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
453            write!(w, "\"{}\"", peek.get::<String>().unwrap())
454        }
455        // Reference to str (&str) - check if target is str
456        (_, Type::Pointer(PointerType::Reference(ptr)))
457            if matches!(
458                ptr.target.ty,
459                Type::Primitive(PrimitiveType::Textual(TextualType::Str))
460            ) =>
461        {
462            // Use Display which will show the string content
463            write!(w, "\"{}\"", peek)
464        }
465        // Booleans
466        (Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
467            let b = peek.get::<bool>().unwrap();
468            write!(w, "{}", if *b { "true" } else { "false" })
469        }
470        // Chars: show with single quotes for Rust
471        (Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
472            write!(w, "'{}'", peek.get::<char>().unwrap())
473        }
474        // Everything else: use Display if available, else Debug
475        _ => {
476            if shape.is_display() {
477                write!(w, "{}", peek)
478            } else if shape.is_debug() {
479                write!(w, "{:?}", peek)
480            } else {
481                write!(w, "<{}>", shape.type_identifier)
482            }
483        }
484    }
485}
486
487/// Value formatting without quotes (XML style - quotes come from attribute syntax).
488fn format_value_raw(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
489    use facet_core::{DynValueKind, TextualType};
490
491    let shape = peek.shape();
492
493    match (shape.def, shape.ty) {
494        // Strings: write raw content (no quotes)
495        (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
496            write!(w, "{}", peek.get::<str>().unwrap())
497        }
498        // String type (owned)
499        (Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
500            write!(w, "{}", peek.get::<String>().unwrap())
501        }
502        // Booleans
503        (Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
504            let b = peek.get::<bool>().unwrap();
505            write!(w, "{}", if *b { "true" } else { "false" })
506        }
507        // Chars: show as-is
508        (Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
509            write!(w, "{}", peek.get::<char>().unwrap())
510        }
511        // Dynamic values: handle based on their kind
512        (Def::DynamicValue(_), _) => {
513            // Write string without quotes for XML
514            if let Ok(dv) = peek.into_dynamic_value()
515                && dv.kind() == DynValueKind::String
516                && let Some(s) = dv.as_str()
517            {
518                return write!(w, "{}", s);
519            }
520            // Fall back to Display for other dynamic values
521            if shape.is_display() {
522                write!(w, "{}", peek)
523            } else if shape.is_debug() {
524                write!(w, "{:?}", peek)
525            } else {
526                write!(w, "<{}>", shape.type_identifier)
527            }
528        }
529        // Everything else: use Display if available, else Debug
530        _ => {
531            if shape.is_display() {
532                write!(w, "{}", peek)
533            } else if shape.is_debug() {
534                write!(w, "{:?}", peek)
535            } else {
536                write!(w, "<{}>", shape.type_identifier)
537            }
538        }
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use facet::Facet;
546    use facet_core::{Shape, Type, UserType};
547
548    // Helper to get field from a struct shape
549    fn get_field<'a>(shape: &'a Shape, name: &str) -> &'a Field {
550        if let Type::User(UserType::Struct(st)) = shape.ty {
551            st.fields.iter().find(|f| f.name == name).unwrap()
552        } else {
553            panic!("expected struct type")
554        }
555    }
556
557    #[test]
558    fn test_rust_flavor_field_presentation() {
559        #[derive(Facet)]
560        struct Point {
561            x: i32,
562            y: i32,
563        }
564
565        let shape = <Point as Facet>::SHAPE;
566        let flavor = RustFlavor;
567
568        let x_field = get_field(shape, "x");
569        let y_field = get_field(shape, "y");
570
571        // Rust flavor: all fields are attributes
572        assert_eq!(
573            flavor.field_presentation(x_field),
574            FieldPresentation::Attribute {
575                name: Cow::Borrowed("x")
576            }
577        );
578        assert_eq!(
579            flavor.field_presentation(y_field),
580            FieldPresentation::Attribute {
581                name: Cow::Borrowed("y")
582            }
583        );
584    }
585
586    #[test]
587    fn test_json_flavor_field_presentation() {
588        #[derive(Facet)]
589        struct Point {
590            x: i32,
591            y: i32,
592        }
593
594        let shape = <Point as Facet>::SHAPE;
595        let flavor = JsonFlavor;
596
597        let x_field = get_field(shape, "x");
598
599        // JSON flavor: all fields are attributes
600        assert_eq!(
601            flavor.field_presentation(x_field),
602            FieldPresentation::Attribute {
603                name: Cow::Borrowed("x")
604            }
605        );
606    }
607
608    #[test]
609    fn test_xml_flavor_field_presentation_default() {
610        // Without XML attributes, fields default to Child
611        #[derive(Facet)]
612        struct Book {
613            title: String,
614            author: String,
615        }
616
617        let shape = <Book as Facet>::SHAPE;
618        let flavor = XmlFlavor;
619
620        let title_field = get_field(shape, "title");
621
622        // XML default: child element
623        assert_eq!(
624            flavor.field_presentation(title_field),
625            FieldPresentation::Child {
626                name: Cow::Borrowed("title")
627            }
628        );
629    }
630
631    #[test]
632    fn test_xml_flavor_field_presentation_with_attrs() {
633        use facet_xml as xml;
634
635        #[derive(Facet)]
636        struct Element {
637            #[facet(xml::attribute)]
638            id: String,
639            #[facet(xml::element)]
640            title: String,
641            #[facet(xml::text)]
642            content: String,
643            #[facet(xml::elements)]
644            items: Vec<String>,
645        }
646
647        let shape = <Element as Facet>::SHAPE;
648        let flavor = XmlFlavor;
649
650        let id_field = get_field(shape, "id");
651        let title_field = get_field(shape, "title");
652        let content_field = get_field(shape, "content");
653        let items_field = get_field(shape, "items");
654
655        assert_eq!(
656            flavor.field_presentation(id_field),
657            FieldPresentation::Attribute {
658                name: Cow::Borrowed("id")
659            }
660        );
661
662        assert_eq!(
663            flavor.field_presentation(title_field),
664            FieldPresentation::Child {
665                name: Cow::Borrowed("title")
666            }
667        );
668
669        assert_eq!(
670            flavor.field_presentation(content_field),
671            FieldPresentation::TextContent
672        );
673
674        assert_eq!(
675            flavor.field_presentation(items_field),
676            FieldPresentation::Children {
677                item_name: Cow::Borrowed("items")
678            }
679        );
680    }
681
682    fn format_to_string<F: DiffFlavor>(flavor: &F, peek: Peek<'_, '_>) -> String {
683        let mut buf = String::new();
684        flavor.format_value(peek, &mut buf).unwrap();
685        buf
686    }
687
688    #[test]
689    fn test_format_value_integers() {
690        let value = 42i32;
691        let peek = Peek::new(&value);
692
693        assert_eq!(format_to_string(&RustFlavor, peek), "42");
694        assert_eq!(format_to_string(&JsonFlavor, peek), "42");
695        assert_eq!(format_to_string(&XmlFlavor, peek), "42");
696    }
697
698    #[test]
699    fn test_format_value_strings() {
700        let value = "hello";
701        let peek = Peek::new(&value);
702
703        // Rust/JSON add quotes around strings, XML doesn't (quotes come from attr syntax)
704        assert_eq!(format_to_string(&RustFlavor, peek), "\"hello\"");
705        assert_eq!(format_to_string(&JsonFlavor, peek), "\"hello\"");
706        assert_eq!(format_to_string(&XmlFlavor, peek), "hello");
707    }
708
709    #[test]
710    fn test_format_value_booleans() {
711        let t = true;
712        let f = false;
713
714        assert_eq!(format_to_string(&RustFlavor, Peek::new(&t)), "true");
715        assert_eq!(format_to_string(&RustFlavor, Peek::new(&f)), "false");
716        assert_eq!(format_to_string(&JsonFlavor, Peek::new(&t)), "true");
717        assert_eq!(format_to_string(&JsonFlavor, Peek::new(&f)), "false");
718        assert_eq!(format_to_string(&XmlFlavor, Peek::new(&t)), "true");
719        assert_eq!(format_to_string(&XmlFlavor, Peek::new(&f)), "false");
720    }
721
722    #[test]
723    fn test_syntax_methods() {
724        let rust = RustFlavor;
725        let json = JsonFlavor;
726        let xml = XmlFlavor;
727
728        // struct_open
729        assert_eq!(rust.struct_open("Point"), "Point {");
730        assert_eq!(json.struct_open("Point"), "{");
731        assert_eq!(xml.struct_open("Point"), "<Point");
732
733        // type_comment (rendered separately in muted color)
734        assert_eq!(rust.type_comment("Point"), None);
735        assert_eq!(json.type_comment("Point"), Some("/* Point */".to_string()));
736        assert_eq!(xml.type_comment("Point"), None);
737
738        // struct_close (non-self-closing)
739        assert_eq!(rust.struct_close("Point", false), "}");
740        assert_eq!(json.struct_close("Point", false), "}");
741        assert_eq!(xml.struct_close("Point", false), "</Point>");
742
743        // struct_close (self-closing)
744        assert_eq!(rust.struct_close("Point", true), "}");
745        assert_eq!(json.struct_close("Point", true), "}");
746        assert_eq!(xml.struct_close("Point", true), "/>");
747
748        // field_separator
749        assert_eq!(rust.field_separator(), ", ");
750        assert_eq!(json.field_separator(), ", ");
751        assert_eq!(xml.field_separator(), " ");
752
753        // seq_open/close
754        assert_eq!(rust.seq_open(), "[");
755        assert_eq!(rust.seq_close(), "]");
756        assert_eq!(json.seq_open(), "[");
757        assert_eq!(json.seq_close(), "]");
758        // XML sequences render items as siblings without wrapper elements
759        assert_eq!(xml.seq_open(), "");
760        assert_eq!(xml.seq_close(), "");
761
762        // comment
763        assert_eq!(rust.comment("5 more"), "/* 5 more */");
764        assert_eq!(json.comment("5 more"), "// 5 more");
765        assert_eq!(xml.comment("5 more"), "<!-- 5 more -->");
766
767        // format_field
768        assert_eq!(rust.format_field("x", "10"), "x: 10");
769        assert_eq!(json.format_field("x", "10"), "\"x\": 10");
770        assert_eq!(xml.format_field("x", "10"), "x=\"10\"");
771    }
772}