Skip to main content

facet_pretty/
shape.rs

1//! Pretty printer for Shape types as Rust-like code
2//!
3//! This module provides functionality to format a `Shape` as Rust source code,
4//! showing the type definition with its attributes.
5
6use alloc::borrow::Cow;
7use alloc::collections::BTreeMap;
8use alloc::collections::BTreeSet;
9use alloc::string::String;
10use alloc::vec;
11use alloc::vec::Vec;
12use core::fmt::Write;
13
14use facet_core::{
15    Attr, Def, EnumRepr, EnumType, Field, PointerType, Shape, StructKind, StructType, Type,
16    UserType, Variant,
17};
18use owo_colors::OwoColorize;
19
20/// Tokyo Night color scheme for syntax highlighting
21pub mod colors {
22    use owo_colors::Style;
23
24    /// Keywords: struct, enum, pub, etc. (purple)
25    pub const fn keyword() -> Style {
26        Style::new().fg_rgb::<187, 154, 247>()
27    }
28
29    /// Type names and identifiers (light blue)
30    pub const fn type_name() -> Style {
31        Style::new().fg_rgb::<192, 202, 245>()
32    }
33
34    /// Field names (cyan)
35    pub const fn field_name() -> Style {
36        Style::new().fg_rgb::<125, 207, 255>()
37    }
38
39    /// Primitive types: u8, i32, bool, String, etc. (teal)
40    pub const fn primitive() -> Style {
41        Style::new().fg_rgb::<115, 218, 202>()
42    }
43
44    /// Punctuation: {, }, (, ), :, etc. (gray-blue)
45    pub const fn punctuation() -> Style {
46        Style::new().fg_rgb::<154, 165, 206>()
47    }
48
49    /// Attribute markers: #[...] (light cyan)
50    pub const fn attribute() -> Style {
51        Style::new().fg_rgb::<137, 221, 255>()
52    }
53
54    /// Attribute content: derive, facet, repr (blue)
55    pub const fn attribute_content() -> Style {
56        Style::new().fg_rgb::<122, 162, 247>()
57    }
58
59    /// String literals (green)
60    pub const fn string() -> Style {
61        Style::new().fg_rgb::<158, 206, 106>()
62    }
63
64    /// Container types: Vec, Option, HashMap (orange)
65    pub const fn container() -> Style {
66        Style::new().fg_rgb::<255, 158, 100>()
67    }
68
69    /// Doc comments (muted gray)
70    pub const fn comment() -> Style {
71        Style::new().fg_rgb::<86, 95, 137>()
72    }
73}
74
75/// Configuration options for shape formatting
76#[derive(Clone, Debug, Default)]
77pub struct ShapeFormatConfig {
78    /// Whether to include doc comments in the output
79    pub show_doc_comments: bool,
80    /// Whether to include third-party (namespaced) attributes
81    pub show_third_party_attrs: bool,
82    /// Whether to expand and print nested types (default: true)
83    pub expand_nested_types: bool,
84}
85
86impl ShapeFormatConfig {
87    /// Create a new config with default settings (no doc comments, no third-party attrs, expand nested)
88    pub fn new() -> Self {
89        Self {
90            expand_nested_types: true,
91            ..Self::default()
92        }
93    }
94
95    /// Enable doc comment display
96    pub const fn with_doc_comments(mut self) -> Self {
97        self.show_doc_comments = true;
98        self
99    }
100
101    /// Enable third-party attribute display
102    pub const fn with_third_party_attrs(mut self) -> Self {
103        self.show_third_party_attrs = true;
104        self
105    }
106
107    /// Enable all metadata (doc comments and third-party attrs)
108    pub const fn with_all_metadata(mut self) -> Self {
109        self.show_doc_comments = true;
110        self.show_third_party_attrs = true;
111        self
112    }
113
114    /// Disable nested type expansion (only format the root type)
115    pub const fn without_nested_types(mut self) -> Self {
116        self.expand_nested_types = false;
117        self
118    }
119}
120
121/// A segment in a path through a type structure
122#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
123pub enum PathSegment {
124    /// A field name in a struct
125    Field(Cow<'static, str>),
126    /// A variant name in an enum
127    Variant(Cow<'static, str>),
128    /// An index in a list/array/tuple
129    Index(usize),
130    /// A key in a map (stored as formatted string representation)
131    Key(Cow<'static, str>),
132}
133
134/// A path to a location within a type structure
135pub type Path = Vec<PathSegment>;
136
137/// A byte span in formatted output (start, end)
138pub type Span = (usize, usize);
139
140/// Spans for a field or variant, tracking both key (name) and value (type) positions
141#[derive(Clone, Debug, Default, PartialEq, Eq)]
142pub struct FieldSpan {
143    /// Span of the field/variant name (e.g., "max_retries" in "max_retries: u8")
144    pub key: Span,
145    /// Span of the type annotation (e.g., "u8" in "max_retries: u8")
146    pub value: Span,
147}
148
149/// Result of formatting a shape with span tracking
150#[derive(Debug)]
151pub struct FormattedShape {
152    /// The formatted text (plain text, no ANSI colors)
153    pub text: String,
154    /// Map from paths to their field spans (key + value) in `text`
155    pub spans: BTreeMap<Path, FieldSpan>,
156    /// Span of the type name (e.g., "Server" in "struct Server {")
157    pub type_name_span: Option<Span>,
158}
159
160/// Strip ANSI escape codes from a string
161pub fn strip_ansi(s: &str) -> String {
162    let mut result = String::with_capacity(s.len());
163    let mut chars = s.chars().peekable();
164
165    while let Some(c) = chars.next() {
166        if c == '\x1b' {
167            // Skip escape sequence
168            if chars.peek() == Some(&'[') {
169                chars.next(); // consume '['
170                // Skip until we hit a letter (the terminator)
171                while let Some(&next) = chars.peek() {
172                    chars.next();
173                    if next.is_ascii_alphabetic() {
174                        break;
175                    }
176                }
177            }
178        } else {
179            result.push(c);
180        }
181    }
182    result
183}
184
185/// Format a Shape as Rust-like source code (plain text, no colors)
186///
187/// By default, this includes doc comments and third-party attributes.
188pub fn format_shape(shape: &Shape) -> String {
189    strip_ansi(&format_shape_colored(shape))
190}
191
192/// Format a Shape as Rust-like source code with config options (plain text, no colors)
193pub fn format_shape_with_config(shape: &Shape, config: &ShapeFormatConfig) -> String {
194    strip_ansi(&format_shape_colored_with_config(shape, config))
195}
196
197/// Format a Shape with span tracking for each field/variant
198/// Note: spans are computed on the plain text (no ANSI codes)
199///
200/// By default, this includes doc comments and third-party attributes.
201pub fn format_shape_with_spans(shape: &Shape) -> FormattedShape {
202    format_shape_with_spans_and_config(shape, &ShapeFormatConfig::default().with_all_metadata())
203}
204
205/// Format a Shape with span tracking and config options
206/// Note: spans are computed on the plain text (no ANSI codes)
207pub fn format_shape_with_spans_and_config(
208    shape: &Shape,
209    config: &ShapeFormatConfig,
210) -> FormattedShape {
211    let mut ctx = SpanTrackingContext::new(config);
212    format_shape_into_with_spans(shape, &mut ctx).expect("Formatting failed");
213    FormattedShape {
214        text: ctx.output,
215        spans: ctx.spans,
216        type_name_span: ctx.type_name_span,
217    }
218}
219
220/// Format a Shape as Rust-like source code with ANSI colors (Tokyo Night theme)
221///
222/// By default, this includes doc comments and third-party attributes.
223pub fn format_shape_colored(shape: &Shape) -> String {
224    format_shape_colored_with_config(shape, &ShapeFormatConfig::default().with_all_metadata())
225}
226
227/// Format a Shape as Rust-like source code with ANSI colors and config options
228pub fn format_shape_colored_with_config(shape: &Shape, config: &ShapeFormatConfig) -> String {
229    let mut output = String::new();
230    format_shape_colored_into_with_config(shape, &mut output, config).expect("Formatting failed");
231    output
232}
233
234/// Format a Shape with ANSI colors into an existing String
235pub fn format_shape_colored_into(shape: &Shape, output: &mut String) -> core::fmt::Result {
236    format_shape_colored_into_with_config(shape, output, &ShapeFormatConfig::default())
237}
238
239/// Format a Shape with ANSI colors into an existing String with config options
240pub fn format_shape_colored_into_with_config(
241    shape: &Shape,
242    output: &mut String,
243    config: &ShapeFormatConfig,
244) -> core::fmt::Result {
245    let mut printed: BTreeSet<&'static str> = BTreeSet::new();
246    let mut queue: Vec<&Shape> = Vec::new();
247    queue.push(shape);
248
249    while let Some(current) = queue.pop() {
250        if !printed.insert(current.type_identifier) {
251            continue;
252        }
253
254        if printed.len() > 1 {
255            writeln!(output)?;
256            writeln!(output)?;
257        }
258
259        match current.def {
260            Def::Map(_) | Def::List(_) | Def::Option(_) | Def::Array(_) => {
261                printed.remove(current.type_identifier);
262                continue;
263            }
264            _ => {}
265        }
266
267        match &current.ty {
268            Type::User(user_type) => match user_type {
269                UserType::Struct(struct_type) => {
270                    format_struct_colored(current, struct_type, output, config)?;
271                    collect_nested_types(struct_type, &mut queue);
272                }
273                UserType::Enum(enum_type) => {
274                    format_enum_colored(current, enum_type, output, config)?;
275                    for variant in enum_type.variants {
276                        collect_nested_types(&variant.data, &mut queue);
277                    }
278                }
279                UserType::Union(_) | UserType::Opaque => {
280                    printed.remove(current.type_identifier);
281                }
282            },
283            _ => {
284                printed.remove(current.type_identifier);
285            }
286        }
287    }
288    Ok(())
289}
290
291fn format_struct_colored(
292    shape: &Shape,
293    struct_type: &StructType,
294    output: &mut String,
295    config: &ShapeFormatConfig,
296) -> core::fmt::Result {
297    // Write doc comments for the struct if enabled
298    if config.show_doc_comments {
299        write_doc_comments_colored(shape.doc, output, "")?;
300    }
301
302    // #[derive(Facet)]
303    write!(output, "{}", "#[".style(colors::attribute()))?;
304    write!(output, "{}", "derive".style(colors::attribute_content()))?;
305    write!(output, "{}", "(".style(colors::attribute()))?;
306    write!(output, "{}", "Facet".style(colors::attribute_content()))?;
307    writeln!(output, "{}", ")]".style(colors::attribute()))?;
308
309    // Write facet attributes if any
310    write_facet_attrs_colored(shape, output)?;
311
312    // Write third-party attributes if enabled
313    if config.show_third_party_attrs {
314        write_third_party_attrs_colored(shape.attributes, output, "")?;
315    }
316
317    match struct_type.kind {
318        StructKind::Struct => {
319            write!(output, "{} ", "struct".style(colors::keyword()))?;
320            write!(
321                output,
322                "{}",
323                shape.type_identifier.style(colors::type_name())
324            )?;
325            writeln!(output, " {}", "{".style(colors::punctuation()))?;
326
327            for (i, field) in struct_type.fields.iter().enumerate() {
328                // Blank line between fields (not before the first one)
329                if i > 0 {
330                    writeln!(output)?;
331                }
332                // Write doc comments for the field if enabled
333                if config.show_doc_comments {
334                    write_doc_comments_colored(field.doc, output, "    ")?;
335                }
336                // Write third-party attributes for the field if enabled
337                if config.show_third_party_attrs {
338                    write_field_third_party_attrs_colored(field, output, "    ")?;
339                }
340                write!(output, "    {}", field.name.style(colors::field_name()))?;
341                write!(output, "{} ", ":".style(colors::punctuation()))?;
342                write_type_name_colored(field.shape(), output)?;
343                writeln!(output, "{}", ",".style(colors::punctuation()))?;
344            }
345            write!(output, "{}", "}".style(colors::punctuation()))?;
346        }
347        StructKind::Tuple | StructKind::TupleStruct => {
348            write!(output, "{} ", "struct".style(colors::keyword()))?;
349            write!(
350                output,
351                "{}",
352                shape.type_identifier.style(colors::type_name())
353            )?;
354            write!(output, "{}", "(".style(colors::punctuation()))?;
355            for (i, field) in struct_type.fields.iter().enumerate() {
356                if i > 0 {
357                    write!(output, "{} ", ",".style(colors::punctuation()))?;
358                }
359                write_type_name_colored(field.shape(), output)?;
360            }
361            write!(
362                output,
363                "{}{}",
364                ")".style(colors::punctuation()),
365                ";".style(colors::punctuation())
366            )?;
367        }
368        StructKind::Unit => {
369            write!(output, "{} ", "struct".style(colors::keyword()))?;
370            write!(
371                output,
372                "{}",
373                shape.type_identifier.style(colors::type_name())
374            )?;
375            write!(output, "{}", ";".style(colors::punctuation()))?;
376        }
377    }
378    Ok(())
379}
380
381fn format_enum_colored(
382    shape: &Shape,
383    enum_type: &EnumType,
384    output: &mut String,
385    config: &ShapeFormatConfig,
386) -> core::fmt::Result {
387    // Write doc comments for the enum if enabled
388    if config.show_doc_comments {
389        write_doc_comments_colored(shape.doc, output, "")?;
390    }
391
392    // #[derive(Facet)]
393    write!(output, "{}", "#[".style(colors::attribute()))?;
394    write!(output, "{}", "derive".style(colors::attribute_content()))?;
395    write!(output, "{}", "(".style(colors::attribute()))?;
396    write!(output, "{}", "Facet".style(colors::attribute_content()))?;
397    writeln!(output, "{}", ")]".style(colors::attribute()))?;
398
399    // Write repr for the discriminant type
400    let repr_str = match enum_type.enum_repr {
401        EnumRepr::RustNPO => None,
402        EnumRepr::U8 => Some("u8"),
403        EnumRepr::U16 => Some("u16"),
404        EnumRepr::U32 => Some("u32"),
405        EnumRepr::U64 => Some("u64"),
406        EnumRepr::USize => Some("usize"),
407        EnumRepr::I8 => Some("i8"),
408        EnumRepr::I16 => Some("i16"),
409        EnumRepr::I32 => Some("i32"),
410        EnumRepr::I64 => Some("i64"),
411        EnumRepr::ISize => Some("isize"),
412    };
413
414    if let Some(repr) = repr_str {
415        write!(output, "{}", "#[".style(colors::attribute()))?;
416        write!(output, "{}", "repr".style(colors::attribute_content()))?;
417        write!(output, "{}", "(".style(colors::attribute()))?;
418        write!(output, "{}", repr.style(colors::primitive()))?;
419        writeln!(output, "{}", ")]".style(colors::attribute()))?;
420    }
421
422    // Write facet attributes if any
423    write_facet_attrs_colored(shape, output)?;
424
425    // Write third-party attributes if enabled
426    if config.show_third_party_attrs {
427        write_third_party_attrs_colored(shape.attributes, output, "")?;
428    }
429
430    // enum Name {
431    write!(output, "{} ", "enum".style(colors::keyword()))?;
432    write!(
433        output,
434        "{}",
435        shape.type_identifier.style(colors::type_name())
436    )?;
437    writeln!(output, " {}", "{".style(colors::punctuation()))?;
438
439    for (vi, variant) in enum_type.variants.iter().enumerate() {
440        // Blank line between variants (not before the first one)
441        if vi > 0 {
442            writeln!(output)?;
443        }
444        // Write doc comments for the variant if enabled
445        if config.show_doc_comments {
446            write_doc_comments_colored(variant.doc, output, "    ")?;
447        }
448        // Write third-party attributes for the variant if enabled
449        if config.show_third_party_attrs {
450            write_variant_third_party_attrs_colored(variant, output, "    ")?;
451        }
452
453        match variant.data.kind {
454            StructKind::Unit => {
455                write!(output, "    {}", variant.name.style(colors::type_name()))?;
456                writeln!(output, "{}", ",".style(colors::punctuation()))?;
457            }
458            StructKind::Tuple | StructKind::TupleStruct => {
459                write!(output, "    {}", variant.name.style(colors::type_name()))?;
460                write!(output, "{}", "(".style(colors::punctuation()))?;
461                for (i, field) in variant.data.fields.iter().enumerate() {
462                    if i > 0 {
463                        write!(output, "{} ", ",".style(colors::punctuation()))?;
464                    }
465                    write_type_name_colored(field.shape(), output)?;
466                }
467                write!(output, "{}", ")".style(colors::punctuation()))?;
468                writeln!(output, "{}", ",".style(colors::punctuation()))?;
469            }
470            StructKind::Struct => {
471                write!(output, "    {}", variant.name.style(colors::type_name()))?;
472                writeln!(output, " {}", "{".style(colors::punctuation()))?;
473                for (fi, field) in variant.data.fields.iter().enumerate() {
474                    // Blank line between variant fields (not before the first one)
475                    if fi > 0 {
476                        writeln!(output)?;
477                    }
478                    // Write doc comments for variant fields if enabled
479                    if config.show_doc_comments {
480                        write_doc_comments_colored(field.doc, output, "        ")?;
481                    }
482                    // Write third-party attributes for variant fields if enabled
483                    if config.show_third_party_attrs {
484                        write_field_third_party_attrs_colored(field, output, "        ")?;
485                    }
486                    write!(output, "        {}", field.name.style(colors::field_name()))?;
487                    write!(output, "{} ", ":".style(colors::punctuation()))?;
488                    write_type_name_colored(field.shape(), output)?;
489                    writeln!(output, "{}", ",".style(colors::punctuation()))?;
490                }
491                write!(output, "    {}", "}".style(colors::punctuation()))?;
492                writeln!(output, "{}", ",".style(colors::punctuation()))?;
493            }
494        }
495    }
496
497    write!(output, "{}", "}".style(colors::punctuation()))?;
498    Ok(())
499}
500
501fn write_facet_attrs_colored(shape: &Shape, output: &mut String) -> core::fmt::Result {
502    let mut attrs: Vec<String> = Vec::new();
503
504    if let Some(tag) = shape.get_tag_attr() {
505        if let Some(content) = shape.get_content_attr() {
506            attrs.push(alloc::format!(
507                "{}{}{}{}{}{}{}{}{}",
508                "tag".style(colors::attribute_content()),
509                " = ".style(colors::punctuation()),
510                "\"".style(colors::string()),
511                tag.style(colors::string()),
512                "\"".style(colors::string()),
513                ", ".style(colors::punctuation()),
514                "content".style(colors::attribute_content()),
515                " = ".style(colors::punctuation()),
516                format!("\"{content}\"").style(colors::string()),
517            ));
518        } else {
519            attrs.push(alloc::format!(
520                "{}{}{}",
521                "tag".style(colors::attribute_content()),
522                " = ".style(colors::punctuation()),
523                format!("\"{tag}\"").style(colors::string()),
524            ));
525        }
526    }
527
528    if shape.is_untagged() {
529        attrs.push(alloc::format!(
530            "{}",
531            "untagged".style(colors::attribute_content())
532        ));
533    }
534
535    if shape.has_deny_unknown_fields_attr() {
536        attrs.push(alloc::format!(
537            "{}",
538            "deny_unknown_fields".style(colors::attribute_content())
539        ));
540    }
541
542    if !attrs.is_empty() {
543        write!(output, "{}", "#[".style(colors::attribute()))?;
544        write!(output, "{}", "facet".style(colors::attribute_content()))?;
545        write!(output, "{}", "(".style(colors::attribute()))?;
546        write!(
547            output,
548            "{}",
549            attrs.join(&format!("{}", ", ".style(colors::punctuation())))
550        )?;
551        writeln!(output, "{}", ")]".style(colors::attribute()))?;
552    }
553
554    Ok(())
555}
556
557/// Write doc comments with the given indentation prefix
558fn write_doc_comments_colored(
559    doc: &[&str],
560    output: &mut String,
561    indent: &str,
562) -> core::fmt::Result {
563    for line in doc {
564        write!(output, "{indent}")?;
565        writeln!(output, "{}", format!("///{line}").style(colors::comment()))?;
566    }
567    Ok(())
568}
569
570/// Write third-party (namespaced) attributes from a Shape's attributes
571/// Groups attributes by namespace, e.g. `#[facet(args::named, args::short)]`
572fn write_third_party_attrs_colored(
573    attributes: &[Attr],
574    output: &mut String,
575    indent: &str,
576) -> core::fmt::Result {
577    // Group attributes by namespace
578    let mut by_namespace: BTreeMap<&'static str, Vec<&'static str>> = BTreeMap::new();
579    for attr in attributes {
580        if let Some(ns) = attr.ns {
581            by_namespace.entry(ns).or_default().push(attr.key);
582        }
583    }
584
585    // Write one line per namespace with all keys
586    for (ns, keys) in by_namespace {
587        write!(output, "{indent}")?;
588        write!(output, "{}", "#[".style(colors::attribute()))?;
589        write!(output, "{}", "facet".style(colors::attribute_content()))?;
590        write!(output, "{}", "(".style(colors::attribute()))?;
591
592        for (i, key) in keys.iter().enumerate() {
593            if i > 0 {
594                write!(output, "{}", ", ".style(colors::punctuation()))?;
595            }
596            write!(output, "{}", ns.style(colors::attribute_content()))?;
597            write!(output, "{}", "::".style(colors::punctuation()))?;
598            write!(output, "{}", key.style(colors::attribute_content()))?;
599        }
600
601        write!(output, "{}", ")".style(colors::attribute()))?;
602        writeln!(output, "{}", "]".style(colors::attribute()))?;
603    }
604    Ok(())
605}
606
607/// Write third-party attributes for a field
608fn write_field_third_party_attrs_colored(
609    field: &Field,
610    output: &mut String,
611    indent: &str,
612) -> core::fmt::Result {
613    write_third_party_attrs_colored(field.attributes, output, indent)
614}
615
616/// Write third-party attributes for a variant
617fn write_variant_third_party_attrs_colored(
618    variant: &Variant,
619    output: &mut String,
620    indent: &str,
621) -> core::fmt::Result {
622    write_third_party_attrs_colored(variant.attributes, output, indent)
623}
624
625fn write_type_name_colored(shape: &Shape, output: &mut String) -> core::fmt::Result {
626    match shape.def {
627        Def::Scalar => {
628            // Check if it's a primitive type
629            let id = shape.type_identifier;
630            if is_primitive_type(id) {
631                write!(output, "{}", id.style(colors::primitive()))?;
632            } else {
633                write!(output, "{}", id.style(colors::type_name()))?;
634            }
635        }
636        Def::Pointer(_) => {
637            if let Type::Pointer(PointerType::Reference(r)) = shape.ty
638                && let Def::Array(array_def) = r.target.def
639            {
640                write!(output, "{}", "&[".style(colors::punctuation()))?;
641                write_type_name_colored(array_def.t, output)?;
642                write!(
643                    output,
644                    "{}{}{}",
645                    "; ".style(colors::punctuation()),
646                    array_def.n.style(colors::primitive()),
647                    "]".style(colors::punctuation())
648                )?;
649                return Ok(());
650            }
651            write!(
652                output,
653                "{}",
654                shape.type_identifier.style(colors::type_name())
655            )?;
656        }
657        Def::List(list_def) => {
658            write!(output, "{}", "Vec".style(colors::container()))?;
659            write!(output, "{}", "<".style(colors::punctuation()))?;
660            write_type_name_colored(list_def.t, output)?;
661            write!(output, "{}", ">".style(colors::punctuation()))?;
662        }
663        Def::Array(array_def) => {
664            write!(output, "{}", "[".style(colors::punctuation()))?;
665            write_type_name_colored(array_def.t, output)?;
666            write!(
667                output,
668                "{}{}{}",
669                "; ".style(colors::punctuation()),
670                array_def.n.style(colors::primitive()),
671                "]".style(colors::punctuation())
672            )?;
673        }
674        Def::Map(map_def) => {
675            let map_name = if shape.type_identifier.contains("BTreeMap") {
676                "BTreeMap"
677            } else {
678                "HashMap"
679            };
680            write!(output, "{}", map_name.style(colors::container()))?;
681            write!(output, "{}", "<".style(colors::punctuation()))?;
682            write_type_name_colored(map_def.k, output)?;
683            write!(output, "{} ", ",".style(colors::punctuation()))?;
684            write_type_name_colored(map_def.v, output)?;
685            write!(output, "{}", ">".style(colors::punctuation()))?;
686        }
687        Def::Option(option_def) => {
688            write!(output, "{}", "Option".style(colors::container()))?;
689            write!(output, "{}", "<".style(colors::punctuation()))?;
690            write_type_name_colored(option_def.t, output)?;
691            write!(output, "{}", ">".style(colors::punctuation()))?;
692        }
693        _ => {
694            let id = shape.type_identifier;
695            if is_primitive_type(id) {
696                write!(output, "{}", id.style(colors::primitive()))?;
697            } else {
698                write!(output, "{}", id.style(colors::type_name()))?;
699            }
700        }
701    }
702    Ok(())
703}
704
705/// Check if a type identifier is a primitive type
706fn is_primitive_type(id: &str) -> bool {
707    matches!(
708        id,
709        "u8" | "u16"
710            | "u32"
711            | "u64"
712            | "u128"
713            | "usize"
714            | "i8"
715            | "i16"
716            | "i32"
717            | "i64"
718            | "i128"
719            | "isize"
720            | "f32"
721            | "f64"
722            | "bool"
723            | "char"
724            | "str"
725            | "&str"
726            | "String"
727    )
728}
729
730/// Context for tracking spans during formatting
731struct SpanTrackingContext<'a> {
732    output: String,
733    spans: BTreeMap<Path, FieldSpan>,
734    /// Span of the type name (struct/enum identifier)
735    type_name_span: Option<Span>,
736    /// Current path prefix (for nested types)
737    current_type: Option<&'static str>,
738    /// Configuration for what to include
739    config: &'a ShapeFormatConfig,
740}
741
742impl<'a> SpanTrackingContext<'a> {
743    const fn new(config: &'a ShapeFormatConfig) -> Self {
744        Self {
745            output: String::new(),
746            spans: BTreeMap::new(),
747            type_name_span: None,
748            current_type: None,
749            config,
750        }
751    }
752
753    const fn len(&self) -> usize {
754        self.output.len()
755    }
756
757    fn record_field_span(&mut self, path: Path, key_span: Span, value_span: Span) {
758        self.spans.insert(
759            path,
760            FieldSpan {
761                key: key_span,
762                value: value_span,
763            },
764        );
765    }
766}
767
768/// Format a Shape with span tracking
769fn format_shape_into_with_spans(shape: &Shape, ctx: &mut SpanTrackingContext) -> core::fmt::Result {
770    // Track which types we've already printed to avoid duplicates
771    let mut printed: BTreeSet<&'static str> = BTreeSet::new();
772    // Queue of types to print
773    let mut queue: Vec<&Shape> = Vec::new();
774
775    // Start with the root shape
776    queue.push(shape);
777
778    while let Some(current) = queue.pop() {
779        // Skip if we've already printed this type
780        if !printed.insert(current.type_identifier) {
781            continue;
782        }
783
784        // Add separator between type definitions
785        if printed.len() > 1 {
786            writeln!(ctx.output)?;
787            writeln!(ctx.output)?;
788        }
789
790        // First check def for container types (Map, List, Option, Array)
791        // These have rich generic info even when ty is Opaque
792        match current.def {
793            Def::Map(_) | Def::List(_) | Def::Option(_) | Def::Array(_) => {
794                // Don't print container types as definitions, they're inline
795                printed.remove(current.type_identifier);
796                continue;
797            }
798            _ => {}
799        }
800
801        // Then check ty for user-defined types
802        match &current.ty {
803            Type::User(user_type) => match user_type {
804                UserType::Struct(struct_type) => {
805                    ctx.current_type = Some(current.type_identifier);
806                    format_struct_with_spans(current, struct_type, ctx)?;
807                    ctx.current_type = None;
808                    // Queue nested types from fields (if expansion enabled)
809                    if ctx.config.expand_nested_types {
810                        collect_nested_types(struct_type, &mut queue);
811                    }
812                }
813                UserType::Enum(enum_type) => {
814                    ctx.current_type = Some(current.type_identifier);
815                    format_enum_with_spans(current, enum_type, ctx)?;
816                    ctx.current_type = None;
817                    // Queue nested types from variants (if expansion enabled)
818                    if ctx.config.expand_nested_types {
819                        for variant in enum_type.variants {
820                            collect_nested_types(&variant.data, &mut queue);
821                        }
822                    }
823                }
824                UserType::Union(_) | UserType::Opaque => {
825                    // For union/opaque types, just show the type identifier
826                    // Don't actually print anything since we can't expand them
827                    printed.remove(current.type_identifier);
828                }
829            },
830            _ => {
831                // For non-user types (primitives, pointers, etc.), don't print
832                printed.remove(current.type_identifier);
833            }
834        }
835    }
836    Ok(())
837}
838
839fn format_struct_with_spans(
840    shape: &Shape,
841    struct_type: &StructType,
842    ctx: &mut SpanTrackingContext,
843) -> core::fmt::Result {
844    // Track start of the whole type definition
845    let type_start = ctx.len();
846
847    // Write doc comments if enabled
848    if ctx.config.show_doc_comments {
849        write_doc_comments(shape.doc, &mut ctx.output, "")?;
850    }
851
852    // Write #[derive(Facet)]
853    writeln!(ctx.output, "#[derive(Facet)]")?;
854
855    // Write facet attributes if any
856    write_facet_attrs(shape, &mut ctx.output)?;
857
858    // Write third-party attributes if enabled
859    if ctx.config.show_third_party_attrs {
860        write_third_party_attrs(shape.attributes, &mut ctx.output, "")?;
861    }
862
863    // Write struct definition
864    match struct_type.kind {
865        StructKind::Struct => {
866            write!(ctx.output, "struct ")?;
867            let type_name_start = ctx.len();
868            write!(ctx.output, "{}", shape.type_identifier)?;
869            let type_name_end = ctx.len();
870            ctx.type_name_span = Some((type_name_start, type_name_end));
871            writeln!(ctx.output, " {{")?;
872            for field in struct_type.fields {
873                // Write doc comments for the field if enabled
874                if ctx.config.show_doc_comments {
875                    write_doc_comments(field.doc, &mut ctx.output, "    ")?;
876                }
877                // Write third-party attributes for the field if enabled
878                if ctx.config.show_third_party_attrs {
879                    write_field_third_party_attrs(field, &mut ctx.output, "    ")?;
880                }
881                write!(ctx.output, "    ")?;
882                // Track the span of the field name (key)
883                let key_start = ctx.len();
884                write!(ctx.output, "{}", field.name)?;
885                let key_end = ctx.len();
886                write!(ctx.output, ": ")?;
887                // Track the span of the type annotation (value)
888                let value_start = ctx.len();
889                write_type_name(field.shape(), &mut ctx.output)?;
890                let value_end = ctx.len();
891                ctx.record_field_span(
892                    vec![PathSegment::Field(Cow::Borrowed(field.name))],
893                    (key_start, key_end),
894                    (value_start, value_end),
895                );
896                writeln!(ctx.output, ",")?;
897            }
898            write!(ctx.output, "}}")?;
899        }
900        StructKind::Tuple | StructKind::TupleStruct => {
901            write!(ctx.output, "struct ")?;
902            let type_name_start = ctx.len();
903            write!(ctx.output, "{}", shape.type_identifier)?;
904            let type_name_end = ctx.len();
905            ctx.type_name_span = Some((type_name_start, type_name_end));
906            write!(ctx.output, "(")?;
907            for (i, field) in struct_type.fields.iter().enumerate() {
908                if i > 0 {
909                    write!(ctx.output, ", ")?;
910                }
911                // For tuple structs, key and value span are the same (just the type)
912                let type_start = ctx.len();
913                write_type_name(field.shape(), &mut ctx.output)?;
914                let type_end = ctx.len();
915                // Use field name if available, otherwise use index as string
916                let field_name = if !field.name.is_empty() {
917                    field.name
918                } else {
919                    // Tuple fields don't have names, skip span tracking
920                    continue;
921                };
922                ctx.record_field_span(
923                    vec![PathSegment::Field(Cow::Borrowed(field_name))],
924                    (type_start, type_end), // key is the type itself for tuples
925                    (type_start, type_end),
926                );
927            }
928            write!(ctx.output, ");")?;
929        }
930        StructKind::Unit => {
931            write!(ctx.output, "struct ")?;
932            let type_name_start = ctx.len();
933            write!(ctx.output, "{}", shape.type_identifier)?;
934            let type_name_end = ctx.len();
935            ctx.type_name_span = Some((type_name_start, type_name_end));
936            write!(ctx.output, ";")?;
937        }
938    }
939
940    // Record span for the root (empty path) covering the whole type
941    let type_end = ctx.len();
942    ctx.record_field_span(vec![], (type_start, type_end), (type_start, type_end));
943
944    Ok(())
945}
946
947fn format_enum_with_spans(
948    shape: &Shape,
949    enum_type: &EnumType,
950    ctx: &mut SpanTrackingContext,
951) -> core::fmt::Result {
952    // Track start of the whole type definition
953    let type_start = ctx.len();
954
955    // Write doc comments if enabled
956    if ctx.config.show_doc_comments {
957        write_doc_comments(shape.doc, &mut ctx.output, "")?;
958    }
959
960    // Write #[derive(Facet)]
961    writeln!(ctx.output, "#[derive(Facet)]")?;
962
963    // Write repr for the discriminant type
964    let repr_str = match enum_type.enum_repr {
965        EnumRepr::RustNPO => None,
966        EnumRepr::U8 => Some("u8"),
967        EnumRepr::U16 => Some("u16"),
968        EnumRepr::U32 => Some("u32"),
969        EnumRepr::U64 => Some("u64"),
970        EnumRepr::USize => Some("usize"),
971        EnumRepr::I8 => Some("i8"),
972        EnumRepr::I16 => Some("i16"),
973        EnumRepr::I32 => Some("i32"),
974        EnumRepr::I64 => Some("i64"),
975        EnumRepr::ISize => Some("isize"),
976    };
977
978    if let Some(repr) = repr_str {
979        writeln!(ctx.output, "#[repr({repr})]")?;
980    }
981
982    // Write facet attributes if any
983    write_facet_attrs(shape, &mut ctx.output)?;
984
985    // Write third-party attributes if enabled
986    if ctx.config.show_third_party_attrs {
987        write_third_party_attrs(shape.attributes, &mut ctx.output, "")?;
988    }
989
990    // Write enum definition
991    write!(ctx.output, "enum ")?;
992    let type_name_start = ctx.len();
993    write!(ctx.output, "{}", shape.type_identifier)?;
994    let type_name_end = ctx.len();
995    ctx.type_name_span = Some((type_name_start, type_name_end));
996    writeln!(ctx.output, " {{")?;
997
998    for variant in enum_type.variants {
999        // Write doc comments for the variant if enabled
1000        if ctx.config.show_doc_comments {
1001            write_doc_comments(variant.doc, &mut ctx.output, "    ")?;
1002        }
1003        // Write third-party attributes for the variant if enabled
1004        if ctx.config.show_third_party_attrs {
1005            write_variant_third_party_attrs(variant, &mut ctx.output, "    ")?;
1006        }
1007
1008        match variant.data.kind {
1009            StructKind::Unit => {
1010                write!(ctx.output, "    ")?;
1011                // For unit variants, key and value are the same (just the variant name)
1012                let name_start = ctx.len();
1013                write!(ctx.output, "{}", variant.name)?;
1014                let name_end = ctx.len();
1015                ctx.record_field_span(
1016                    vec![PathSegment::Variant(Cow::Borrowed(variant.name))],
1017                    (name_start, name_end),
1018                    (name_start, name_end),
1019                );
1020                writeln!(ctx.output, ",")?;
1021            }
1022            StructKind::Tuple | StructKind::TupleStruct => {
1023                write!(ctx.output, "    ")?;
1024                let variant_name_start = ctx.len();
1025                write!(ctx.output, "{}", variant.name)?;
1026                let variant_name_end = ctx.len();
1027                write!(ctx.output, "(")?;
1028                let tuple_start = ctx.len();
1029                for (i, field) in variant.data.fields.iter().enumerate() {
1030                    if i > 0 {
1031                        write!(ctx.output, ", ")?;
1032                    }
1033                    let type_start = ctx.len();
1034                    write_type_name(field.shape(), &mut ctx.output)?;
1035                    let type_end = ctx.len();
1036                    // Track span for variant field
1037                    if !field.name.is_empty() {
1038                        ctx.record_field_span(
1039                            vec![
1040                                PathSegment::Variant(Cow::Borrowed(variant.name)),
1041                                PathSegment::Field(Cow::Borrowed(field.name)),
1042                            ],
1043                            (type_start, type_end),
1044                            (type_start, type_end),
1045                        );
1046                    }
1047                }
1048                write!(ctx.output, ")")?;
1049                let tuple_end = ctx.len();
1050                // Record variant span: key is the name, value is the tuple contents
1051                ctx.record_field_span(
1052                    vec![PathSegment::Variant(Cow::Borrowed(variant.name))],
1053                    (variant_name_start, variant_name_end),
1054                    (tuple_start, tuple_end),
1055                );
1056                writeln!(ctx.output, ",")?;
1057            }
1058            StructKind::Struct => {
1059                write!(ctx.output, "    ")?;
1060                let variant_name_start = ctx.len();
1061                write!(ctx.output, "{}", variant.name)?;
1062                let variant_name_end = ctx.len();
1063                writeln!(ctx.output, " {{")?;
1064                let struct_start = ctx.len();
1065                for field in variant.data.fields {
1066                    // Write doc comments for variant fields if enabled
1067                    if ctx.config.show_doc_comments {
1068                        write_doc_comments(field.doc, &mut ctx.output, "        ")?;
1069                    }
1070                    // Write third-party attributes for variant fields if enabled
1071                    if ctx.config.show_third_party_attrs {
1072                        write_field_third_party_attrs(field, &mut ctx.output, "        ")?;
1073                    }
1074                    write!(ctx.output, "        ")?;
1075                    let key_start = ctx.len();
1076                    write!(ctx.output, "{}", field.name)?;
1077                    let key_end = ctx.len();
1078                    write!(ctx.output, ": ")?;
1079                    let value_start = ctx.len();
1080                    write_type_name(field.shape(), &mut ctx.output)?;
1081                    let value_end = ctx.len();
1082                    ctx.record_field_span(
1083                        vec![
1084                            PathSegment::Variant(Cow::Borrowed(variant.name)),
1085                            PathSegment::Field(Cow::Borrowed(field.name)),
1086                        ],
1087                        (key_start, key_end),
1088                        (value_start, value_end),
1089                    );
1090                    writeln!(ctx.output, ",")?;
1091                }
1092                write!(ctx.output, "    }}")?;
1093                let struct_end = ctx.len();
1094                // Record variant span: key is the name, value is the struct body
1095                ctx.record_field_span(
1096                    vec![PathSegment::Variant(Cow::Borrowed(variant.name))],
1097                    (variant_name_start, variant_name_end),
1098                    (struct_start, struct_end),
1099                );
1100                writeln!(ctx.output, ",")?;
1101            }
1102        }
1103    }
1104
1105    write!(ctx.output, "}}")?;
1106
1107    // Record span for the root (empty path) covering the whole type
1108    let type_end = ctx.len();
1109    ctx.record_field_span(vec![], (type_start, type_end), (type_start, type_end));
1110
1111    Ok(())
1112}
1113
1114/// Collect nested user-defined types from struct fields
1115fn collect_nested_types<'a>(struct_type: &'a StructType, queue: &mut Vec<&'a Shape>) {
1116    for field in struct_type.fields {
1117        collect_from_shape(field.shape(), queue);
1118    }
1119}
1120
1121/// Recursively collect user-defined types from a shape (handles containers)
1122fn collect_from_shape<'a>(shape: &'a Shape, queue: &mut Vec<&'a Shape>) {
1123    match shape.def {
1124        Def::List(list_def) => collect_from_shape(list_def.t, queue),
1125        Def::Array(array_def) => collect_from_shape(array_def.t, queue),
1126        Def::Map(map_def) => {
1127            collect_from_shape(map_def.k, queue);
1128            collect_from_shape(map_def.v, queue);
1129        }
1130        Def::Option(option_def) => collect_from_shape(option_def.t, queue),
1131        _ => {
1132            // Check if it's a user-defined type worth expanding
1133            if let Type::User(UserType::Struct(_) | UserType::Enum(_)) = &shape.ty {
1134                queue.push(shape);
1135            }
1136        }
1137    }
1138}
1139
1140// Plain text helpers for span tracking (ANSI codes would break byte offsets)
1141fn write_facet_attrs(shape: &Shape, output: &mut String) -> core::fmt::Result {
1142    let mut attrs: Vec<String> = Vec::new();
1143
1144    if let Some(tag) = shape.get_tag_attr() {
1145        if let Some(content) = shape.get_content_attr() {
1146            attrs.push(alloc::format!("tag = \"{tag}\", content = \"{content}\""));
1147        } else {
1148            attrs.push(alloc::format!("tag = \"{tag}\""));
1149        }
1150    }
1151
1152    if shape.is_untagged() {
1153        attrs.push("untagged".into());
1154    }
1155
1156    if shape.has_deny_unknown_fields_attr() {
1157        attrs.push("deny_unknown_fields".into());
1158    }
1159
1160    if !attrs.is_empty() {
1161        writeln!(output, "#[facet({})]", attrs.join(", "))?;
1162    }
1163
1164    Ok(())
1165}
1166
1167/// Write doc comments (plain text) with the given indentation prefix
1168fn write_doc_comments(doc: &[&str], output: &mut String, indent: &str) -> core::fmt::Result {
1169    for line in doc {
1170        write!(output, "{indent}")?;
1171        writeln!(output, "///{line}")?;
1172    }
1173    Ok(())
1174}
1175
1176/// Write third-party (namespaced) attributes (plain text)
1177fn write_third_party_attrs(
1178    attributes: &[Attr],
1179    output: &mut String,
1180    indent: &str,
1181) -> core::fmt::Result {
1182    // Group attributes by namespace
1183    let mut by_namespace: BTreeMap<&'static str, Vec<&'static str>> = BTreeMap::new();
1184    for attr in attributes {
1185        if let Some(ns) = attr.ns {
1186            by_namespace.entry(ns).or_default().push(attr.key);
1187        }
1188    }
1189
1190    // Write one line per namespace with all keys
1191    for (ns, keys) in by_namespace {
1192        write!(output, "{indent}")?;
1193        write!(output, "#[facet(")?;
1194        for (i, key) in keys.iter().enumerate() {
1195            if i > 0 {
1196                write!(output, ", ")?;
1197            }
1198            write!(output, "{ns}::{key}")?;
1199        }
1200        writeln!(output, ")]")?;
1201    }
1202    Ok(())
1203}
1204
1205/// Write third-party attributes for a field (plain text)
1206fn write_field_third_party_attrs(
1207    field: &Field,
1208    output: &mut String,
1209    indent: &str,
1210) -> core::fmt::Result {
1211    write_third_party_attrs(field.attributes, output, indent)
1212}
1213
1214/// Write third-party attributes for a variant (plain text)
1215fn write_variant_third_party_attrs(
1216    variant: &Variant,
1217    output: &mut String,
1218    indent: &str,
1219) -> core::fmt::Result {
1220    write_third_party_attrs(variant.attributes, output, indent)
1221}
1222
1223fn write_type_name(shape: &Shape, output: &mut String) -> core::fmt::Result {
1224    match shape.def {
1225        Def::Scalar => {
1226            write!(output, "{}", shape.type_identifier)?;
1227        }
1228        Def::Pointer(_) => {
1229            if let Type::Pointer(PointerType::Reference(r)) = shape.ty
1230                && let Def::Array(array_def) = r.target.def
1231            {
1232                write!(output, "&[")?;
1233                write_type_name(array_def.t, output)?;
1234                write!(output, "; {}]", array_def.n)?;
1235                return Ok(());
1236            }
1237            write!(output, "{}", shape.type_identifier)?;
1238        }
1239        Def::List(list_def) => {
1240            write!(output, "Vec<")?;
1241            write_type_name(list_def.t, output)?;
1242            write!(output, ">")?;
1243        }
1244        Def::Array(array_def) => {
1245            write!(output, "[")?;
1246            write_type_name(array_def.t, output)?;
1247            write!(output, "; {}]", array_def.n)?;
1248        }
1249        Def::Map(map_def) => {
1250            let map_name = if shape.type_identifier.contains("BTreeMap") {
1251                "BTreeMap"
1252            } else {
1253                "HashMap"
1254            };
1255            write!(output, "{map_name}<")?;
1256            write_type_name(map_def.k, output)?;
1257            write!(output, ", ")?;
1258            write_type_name(map_def.v, output)?;
1259            write!(output, ">")?;
1260        }
1261        Def::Option(option_def) => {
1262            write!(output, "Option<")?;
1263            write_type_name(option_def.t, output)?;
1264            write!(output, ">")?;
1265        }
1266        _ => {
1267            write!(output, "{}", shape.type_identifier)?;
1268        }
1269    }
1270    Ok(())
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275    use super::*;
1276    use facet::Facet;
1277
1278    #[test]
1279    fn test_simple_struct() {
1280        #[derive(Facet)]
1281        struct Simple {
1282            name: String,
1283            count: u32,
1284        }
1285
1286        let output = format_shape(Simple::SHAPE);
1287        assert!(output.contains("struct Simple"));
1288        assert!(output.contains("name: String"));
1289        assert!(output.contains("count: u32"));
1290    }
1291
1292    #[test]
1293    fn test_enum_with_tag() {
1294        #[derive(Facet)]
1295        #[repr(C)]
1296        #[facet(tag = "type")]
1297        #[allow(dead_code)]
1298        enum Tagged {
1299            A { x: i32 },
1300            B { y: String },
1301        }
1302
1303        let output = format_shape(Tagged::SHAPE);
1304        assert!(output.contains("enum Tagged"));
1305        assert!(output.contains("#[facet(tag = \"type\")]"));
1306    }
1307
1308    #[test]
1309    fn test_nested_types() {
1310        #[derive(Facet)]
1311        #[allow(dead_code)]
1312        struct Inner {
1313            value: i32,
1314        }
1315
1316        #[derive(Facet)]
1317        #[allow(dead_code)]
1318        struct Outer {
1319            inner: Inner,
1320            name: String,
1321        }
1322
1323        let output = format_shape(Outer::SHAPE);
1324        // Should contain both Outer and Inner definitions
1325        assert!(output.contains("struct Outer"), "Missing Outer: {output}");
1326        assert!(
1327            output.contains("inner: Inner"),
1328            "Missing inner field: {output}"
1329        );
1330        assert!(
1331            output.contains("struct Inner"),
1332            "Missing Inner definition: {output}"
1333        );
1334        assert!(
1335            output.contains("value: i32"),
1336            "Missing value field: {output}"
1337        );
1338    }
1339
1340    #[test]
1341    fn test_nested_in_vec() {
1342        #[derive(Facet)]
1343        #[allow(dead_code)]
1344        struct Item {
1345            id: u32,
1346        }
1347
1348        #[derive(Facet)]
1349        #[allow(dead_code)]
1350        struct Container {
1351            items: Vec<Item>,
1352        }
1353
1354        let output = format_shape(Container::SHAPE);
1355        // Should contain both Container and Item definitions
1356        assert!(
1357            output.contains("struct Container"),
1358            "Missing Container: {output}"
1359        );
1360        assert!(
1361            output.contains("items: Vec<Item>"),
1362            "Missing items field: {output}"
1363        );
1364        assert!(
1365            output.contains("struct Item"),
1366            "Missing Item definition: {output}"
1367        );
1368    }
1369
1370    #[test]
1371    fn test_format_shape_with_spans() {
1372        #[derive(Facet)]
1373        #[allow(dead_code)]
1374        struct Config {
1375            name: String,
1376            max_retries: u8,
1377            enabled: bool,
1378        }
1379
1380        let result = format_shape_with_spans(Config::SHAPE);
1381
1382        // Check that spans were recorded for each field
1383        let name_path = vec![PathSegment::Field(Cow::Borrowed("name"))];
1384        let retries_path = vec![PathSegment::Field(Cow::Borrowed("max_retries"))];
1385        let enabled_path = vec![PathSegment::Field(Cow::Borrowed("enabled"))];
1386
1387        assert!(
1388            result.spans.contains_key(&name_path),
1389            "Missing span for 'name' field. Spans: {:?}",
1390            result.spans
1391        );
1392        assert!(
1393            result.spans.contains_key(&retries_path),
1394            "Missing span for 'max_retries' field. Spans: {:?}",
1395            result.spans
1396        );
1397        assert!(
1398            result.spans.contains_key(&enabled_path),
1399            "Missing span for 'enabled' field. Spans: {:?}",
1400            result.spans
1401        );
1402
1403        // Verify the span for max_retries points to "u8"
1404        let field_span = &result.spans[&retries_path];
1405        let spanned_text = &result.text[field_span.value.0..field_span.value.1];
1406        assert_eq!(spanned_text, "u8", "Expected 'u8', got '{spanned_text}'");
1407    }
1408
1409    #[test]
1410    fn test_format_enum_with_spans() {
1411        #[derive(Facet)]
1412        #[repr(u8)]
1413        #[allow(dead_code)]
1414        enum Status {
1415            Active,
1416            Pending,
1417            Error { code: i32, message: String },
1418        }
1419
1420        let result = format_shape_with_spans(Status::SHAPE);
1421
1422        // Check variant spans
1423        let active_path = vec![PathSegment::Variant(Cow::Borrowed("Active"))];
1424        let error_path = vec![PathSegment::Variant(Cow::Borrowed("Error"))];
1425        let error_code_path = vec![
1426            PathSegment::Variant(Cow::Borrowed("Error")),
1427            PathSegment::Field(Cow::Borrowed("code")),
1428        ];
1429
1430        assert!(
1431            result.spans.contains_key(&active_path),
1432            "Missing span for 'Active' variant. Spans: {:?}",
1433            result.spans
1434        );
1435        assert!(
1436            result.spans.contains_key(&error_path),
1437            "Missing span for 'Error' variant. Spans: {:?}",
1438            result.spans
1439        );
1440        assert!(
1441            result.spans.contains_key(&error_code_path),
1442            "Missing span for 'Error.code' field. Spans: {:?}",
1443            result.spans
1444        );
1445
1446        // Verify the span for code points to "i32"
1447        let field_span = &result.spans[&error_code_path];
1448        let spanned_text = &result.text[field_span.value.0..field_span.value.1];
1449        assert_eq!(spanned_text, "i32", "Expected 'i32', got '{spanned_text}'");
1450    }
1451
1452    #[test]
1453    fn test_format_with_doc_comments() {
1454        /// A configuration struct for the application.
1455        #[derive(Facet)]
1456        #[allow(dead_code)]
1457        struct Config {
1458            /// The name of the configuration.
1459            name: String,
1460            /// Maximum number of retries.
1461            max_retries: u8,
1462        }
1463
1464        // With doc comments (default)
1465        let output = format_shape(Config::SHAPE);
1466        assert!(
1467            output.contains("/// A configuration struct"),
1468            "Should contain struct doc comment: {output}"
1469        );
1470        assert!(
1471            output.contains("/// The name of the configuration"),
1472            "Should contain field doc comment: {output}"
1473        );
1474        assert!(
1475            output.contains("/// Maximum number of retries"),
1476            "Should contain field doc comment: {output}"
1477        );
1478
1479        // Without doc comments (explicit config)
1480        let config = ShapeFormatConfig::new();
1481        let output_without = format_shape_with_config(Config::SHAPE, &config);
1482        assert!(
1483            !output_without.contains("///"),
1484            "Should not contain doc comments when disabled: {output_without}"
1485        );
1486    }
1487
1488    #[test]
1489    fn test_format_enum_with_doc_comments() {
1490        /// Status of an operation.
1491        #[derive(Facet)]
1492        #[repr(u8)]
1493        #[allow(dead_code)]
1494        enum Status {
1495            /// The operation is active.
1496            Active,
1497            /// The operation failed with an error.
1498            Error {
1499                /// Error code.
1500                code: i32,
1501            },
1502        }
1503
1504        let config = ShapeFormatConfig::new().with_doc_comments();
1505        let output = format_shape_with_config(Status::SHAPE, &config);
1506
1507        assert!(
1508            output.contains("/// Status of an operation"),
1509            "Should contain enum doc comment: {output}"
1510        );
1511        assert!(
1512            output.contains("/// The operation is active"),
1513            "Should contain variant doc comment: {output}"
1514        );
1515        assert!(
1516            output.contains("/// The operation failed"),
1517            "Should contain variant doc comment: {output}"
1518        );
1519        assert!(
1520            output.contains("/// Error code"),
1521            "Should contain variant field doc comment: {output}"
1522        );
1523    }
1524}