Skip to main content

facet_solver/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![doc = include_str!("../README.md")]
3
4extern crate alloc;
5
6use alloc::borrow::Cow;
7use alloc::collections::BTreeMap;
8use alloc::collections::BTreeSet;
9use alloc::string::{String, ToString};
10use alloc::vec;
11use alloc::vec::Vec;
12use core::fmt;
13
14use facet_core::{Def, Field, Shape, StructType, Type, UserType, Variant};
15
16// Re-export resolution types from facet-reflect
17pub use facet_reflect::{
18    DuplicateFieldError, FieldCategory, FieldInfo, FieldKey, FieldPath, KeyPath, MatchResult,
19    PathSegment, Resolution, VariantSelection,
20};
21
22/// Format determines how fields are categorized and indexed in the schema.
23///
24/// Different serialization formats have different concepts of "fields":
25/// - Flat formats (JSON, TOML, YAML) treat all fields as key-value pairs
26/// - DOM formats (XML, HTML) distinguish attributes, elements, and text content
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum Format {
29    /// Flat key-value formats (JSON, TOML, YAML, etc.)
30    ///
31    /// All fields are treated as keys with no distinction. The solver
32    /// uses `see_key()` to report field names.
33    #[default]
34    Flat,
35
36    /// DOM/tree formats (XML, HTML)
37    ///
38    /// Fields are categorized as attributes, elements, or text content.
39    /// The solver uses `see_attribute()`, `see_element()`, etc. to report
40    /// fields with their category.
41    Dom,
42}
43
44/// Cached schema for a type that may contain flattened fields.
45///
46/// This is computed once per Shape and can be cached forever since
47/// type information is static.
48#[derive(Debug)]
49pub struct Schema {
50    /// The shape this schema is for (kept for future caching key)
51    #[allow(dead_code)]
52    shape: &'static Shape,
53
54    /// The format this schema was built for.
55    format: Format,
56
57    /// All possible resolutions of this type.
58    /// For types with no enums in flatten paths, this has exactly 1 entry.
59    /// For types with enums, this has one entry per valid combination of variants.
60    resolutions: Vec<Resolution>,
61
62    /// Inverted index for Flat format: field_name → bitmask of configuration indices.
63    /// Bit i is set if `resolutions[i]` contains this field.
64    /// Uses a `Vec<u64>` to support arbitrary numbers of resolutions.
65    field_to_resolutions: BTreeMap<&'static str, ResolutionSet>,
66
67    /// Inverted index for Dom format: (category, name) → bitmask of configuration indices.
68    /// Only populated when format is Dom.
69    dom_field_to_resolutions: BTreeMap<(FieldCategory, &'static str), ResolutionSet>,
70}
71
72/// Handle that identifies a specific resolution inside a schema.
73#[derive(Debug, Clone, Copy)]
74pub struct ResolutionHandle<'a> {
75    index: usize,
76    resolution: &'a Resolution,
77}
78
79impl<'a> PartialEq for ResolutionHandle<'a> {
80    fn eq(&self, other: &Self) -> bool {
81        self.index == other.index
82    }
83}
84
85impl<'a> Eq for ResolutionHandle<'a> {}
86
87impl<'a> ResolutionHandle<'a> {
88    /// Internal helper to build a handle for an index within a schema.
89    fn from_schema(schema: &'a Schema, index: usize) -> Self {
90        Self {
91            index,
92            resolution: &schema.resolutions[index],
93        }
94    }
95
96    /// Resolution index within the originating schema.
97    pub const fn index(self) -> usize {
98        self.index
99    }
100
101    /// Access the underlying resolution metadata.
102    pub const fn resolution(self) -> &'a Resolution {
103        self.resolution
104    }
105}
106
107/// A set of configuration indices, stored as a bitmask for O(1) intersection.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct ResolutionSet {
110    /// Bitmask where bit i indicates `resolutions[i]` is in the set.
111    /// For most types, a single u64 suffices (up to 64 configs).
112    bits: Vec<u64>,
113    /// Number of resolutions in the set.
114    count: usize,
115}
116
117impl ResolutionSet {
118    /// Create an empty config set.
119    fn empty(num_resolutions: usize) -> Self {
120        let num_words = num_resolutions.div_ceil(64);
121        Self {
122            bits: vec![0; num_words],
123            count: 0,
124        }
125    }
126
127    /// Create a full config set (all configs present).
128    fn full(num_resolutions: usize) -> Self {
129        let num_words = num_resolutions.div_ceil(64);
130        let mut bits = vec![!0u64; num_words];
131        // Clear bits beyond num_resolutions
132        if !num_resolutions.is_multiple_of(64) {
133            let last_word_bits = num_resolutions % 64;
134            bits[num_words - 1] = (1u64 << last_word_bits) - 1;
135        }
136        Self {
137            bits,
138            count: num_resolutions,
139        }
140    }
141
142    /// Insert a configuration index.
143    fn insert(&mut self, idx: usize) {
144        let word = idx / 64;
145        let bit = idx % 64;
146        if self.bits[word] & (1u64 << bit) == 0 {
147            self.bits[word] |= 1u64 << bit;
148            self.count += 1;
149        }
150    }
151
152    /// Intersect with another config set in place.
153    fn intersect_with(&mut self, other: &ResolutionSet) {
154        self.count = 0;
155        for (a, b) in self.bits.iter_mut().zip(other.bits.iter()) {
156            *a &= *b;
157            self.count += a.count_ones() as usize;
158        }
159    }
160
161    /// Check if intersection with another set would be non-empty.
162    /// Does not modify either set.
163    fn intersects(&self, other: &ResolutionSet) -> bool {
164        self.bits
165            .iter()
166            .zip(other.bits.iter())
167            .any(|(a, b)| (*a & *b) != 0)
168    }
169
170    /// Get the number of resolutions in the set.
171    const fn len(&self) -> usize {
172        self.count
173    }
174
175    /// Check if empty.
176    const fn is_empty(&self) -> bool {
177        self.count == 0
178    }
179
180    /// Get the first (lowest) configuration index in the set.
181    fn first(&self) -> Option<usize> {
182        for (word_idx, &word) in self.bits.iter().enumerate() {
183            if word != 0 {
184                return Some(word_idx * 64 + word.trailing_zeros() as usize);
185            }
186        }
187        None
188    }
189
190    /// Iterate over configuration indices in the set.
191    fn iter(&self) -> impl Iterator<Item = usize> + '_ {
192        self.bits.iter().enumerate().flat_map(|(word_idx, &word)| {
193            (0..64).filter_map(move |bit| {
194                if word & (1u64 << bit) != 0 {
195                    Some(word_idx * 64 + bit)
196                } else {
197                    None
198                }
199            })
200        })
201    }
202}
203
204/// Find fields that could disambiguate between resolutions.
205/// Returns fields that exist in some but not all resolutions.
206fn find_disambiguating_fields(configs: &[&Resolution]) -> Vec<String> {
207    if configs.len() < 2 {
208        return Vec::new();
209    }
210
211    // Collect all field names across all configs
212    let mut all_fields: BTreeSet<&str> = BTreeSet::new();
213    for config in configs {
214        for info in config.fields().values() {
215            all_fields.insert(info.serialized_name);
216        }
217    }
218
219    // Find fields that are in some but not all configs
220    let mut disambiguating = Vec::new();
221    for field in all_fields {
222        let count = configs
223            .iter()
224            .filter(|c| c.field_by_name(field).is_some())
225            .count();
226        if count > 0 && count < configs.len() {
227            disambiguating.push(field.to_string());
228        }
229    }
230
231    disambiguating
232}
233
234/// Information about a missing required field for error reporting.
235#[derive(Debug, Clone)]
236pub struct MissingFieldInfo {
237    /// The serialized field name (as it appears in input)
238    pub name: &'static str,
239    /// Full path to the field (e.g., "backend.connection.port")
240    pub path: String,
241    /// The Rust type that defines this field
242    pub defined_in: String,
243}
244
245impl MissingFieldInfo {
246    /// Create from a FieldInfo
247    fn from_field_info(info: &FieldInfo) -> Self {
248        Self {
249            name: info.serialized_name,
250            path: info.path.to_string(),
251            defined_in: info.value_shape.type_identifier.to_string(),
252        }
253    }
254}
255
256/// Information about why a specific candidate (resolution) failed to match.
257#[derive(Debug, Clone)]
258pub struct CandidateFailure {
259    /// Human-readable description of the variant (e.g., "DatabaseBackend::Postgres")
260    pub variant_name: String,
261    /// Required fields that were not provided in the input
262    pub missing_fields: Vec<MissingFieldInfo>,
263    /// Fields in the input that don't exist in this candidate
264    pub unknown_fields: Vec<String>,
265    /// Number of unknown fields that have "did you mean?" suggestions for this candidate
266    /// Higher = more likely the user intended this variant
267    pub suggestion_matches: usize,
268}
269
270/// Suggestion for a field that might have been misspelled.
271#[derive(Debug, Clone)]
272pub struct FieldSuggestion {
273    /// The unknown field from input
274    pub unknown: String,
275    /// The suggested correct field name
276    pub suggestion: &'static str,
277    /// Similarity score (0.0 to 1.0, higher is more similar)
278    pub similarity: f64,
279}
280
281/// Errors that can occur when building a schema.
282#[derive(Debug, Clone)]
283pub enum SchemaError {
284    /// A field name appears from multiple sources (parent struct and flattened struct)
285    DuplicateField(DuplicateFieldError),
286}
287
288impl From<DuplicateFieldError> for SchemaError {
289    fn from(err: DuplicateFieldError) -> Self {
290        SchemaError::DuplicateField(err)
291    }
292}
293
294impl fmt::Display for SchemaError {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        match self {
297            SchemaError::DuplicateField(err) => {
298                write!(
299                    f,
300                    "Duplicate field name '{}' from different sources: {} vs {}. \
301                     This usually means a parent struct and a flattened struct both \
302                     define a field with the same name.",
303                    err.field_name, err.first_path, err.second_path
304                )
305            }
306        }
307    }
308}
309
310#[cfg(feature = "std")]
311impl std::error::Error for SchemaError {}
312
313/// Errors that can occur during flatten resolution.
314#[derive(Debug, Clone)]
315pub enum SolverError {
316    /// No configuration matches the input fields
317    NoMatch {
318        /// The input fields that were provided
319        input_fields: Vec<String>,
320        /// Missing required fields (from the closest matching config) - simple names for backwards compat
321        missing_required: Vec<&'static str>,
322        /// Missing required fields with full path information
323        missing_required_detailed: Vec<MissingFieldInfo>,
324        /// Unknown fields that don't belong to any config
325        unknown_fields: Vec<String>,
326        /// Description of the closest matching configuration
327        closest_resolution: Option<String>,
328        /// Why each candidate failed to match (detailed per-candidate info)
329        candidate_failures: Vec<CandidateFailure>,
330        /// "Did you mean?" suggestions for unknown fields
331        suggestions: Vec<FieldSuggestion>,
332    },
333    /// Multiple resolutions match the input fields
334    Ambiguous {
335        /// Descriptions of the matching resolutions
336        candidates: Vec<String>,
337        /// Fields that could disambiguate (unique to specific configs)
338        disambiguating_fields: Vec<String>,
339    },
340}
341
342impl fmt::Display for SolverError {
343    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344        match self {
345            SolverError::NoMatch {
346                input_fields,
347                missing_required: _,
348                missing_required_detailed,
349                unknown_fields,
350                closest_resolution,
351                candidate_failures,
352                suggestions,
353            } => {
354                write!(f, "No matching configuration for fields {input_fields:?}")?;
355
356                // Show per-candidate failure reasons if available
357                if !candidate_failures.is_empty() {
358                    write!(f, "\n\nNo variant matched:")?;
359                    for failure in candidate_failures {
360                        write!(f, "\n  - {}", failure.variant_name)?;
361                        if !failure.missing_fields.is_empty() {
362                            let names: Vec<_> =
363                                failure.missing_fields.iter().map(|m| m.name).collect();
364                            if names.len() == 1 {
365                                write!(f, ": missing field '{}'", names[0])?;
366                            } else {
367                                write!(f, ": missing fields {names:?}")?;
368                            }
369                        }
370                        if !failure.unknown_fields.is_empty() {
371                            if failure.missing_fields.is_empty() {
372                                write!(f, ":")?;
373                            } else {
374                                write!(f, ",")?;
375                            }
376                            write!(f, " unknown fields {:?}", failure.unknown_fields)?;
377                        }
378                    }
379                } else if let Some(config) = closest_resolution {
380                    // Fallback to closest match if no per-candidate info
381                    write!(f, " (closest match: {config})")?;
382                    if !missing_required_detailed.is_empty() {
383                        write!(f, "; missing required fields:")?;
384                        for info in missing_required_detailed {
385                            write!(f, " {} (at path: {})", info.name, info.path)?;
386                        }
387                    }
388                }
389
390                // Show unknown fields with suggestions
391                if !unknown_fields.is_empty() {
392                    write!(f, "\n\nUnknown fields: {unknown_fields:?}")?;
393                }
394                for suggestion in suggestions {
395                    write!(
396                        f,
397                        "\n  Did you mean '{}' instead of '{}'?",
398                        suggestion.suggestion, suggestion.unknown
399                    )?;
400                }
401
402                Ok(())
403            }
404            SolverError::Ambiguous {
405                candidates,
406                disambiguating_fields,
407            } => {
408                write!(f, "Ambiguous: multiple resolutions match: {candidates:?}")?;
409                if !disambiguating_fields.is_empty() {
410                    write!(
411                        f,
412                        "; try adding one of these fields to disambiguate: {disambiguating_fields:?}"
413                    )?;
414                }
415                Ok(())
416            }
417        }
418    }
419}
420
421#[cfg(feature = "std")]
422impl std::error::Error for SolverError {}
423
424/// Compute a specificity score for a shape. Lower score = more specific.
425///
426/// This is used to disambiguate when a value could satisfy multiple types.
427/// For example, the value `42` fits both `u8` and `u16`, but `u8` is more
428/// specific (lower score), so it should be preferred.
429/// Compute a specificity score for a shape.
430///
431/// Lower score = more specific type. Used for type-based disambiguation
432/// where we want to try more specific types first (e.g., u8 before u16).
433pub fn specificity_score(shape: &'static Shape) -> u64 {
434    // Use type_identifier to determine specificity
435    // Smaller integer types are more specific
436    match shape.type_identifier {
437        "u8" | "i8" => 8,
438        "u16" | "i16" => 16,
439        "u32" | "i32" | "f32" => 32,
440        "u64" | "i64" | "f64" => 64,
441        "u128" | "i128" => 128,
442        "usize" | "isize" => 64, // Treat as 64-bit
443        // Other types get a high score (less specific)
444        _ => 1000,
445    }
446}
447
448// ============================================================================
449// Solver (State Machine)
450// ============================================================================
451
452/// Result of reporting a key to the solver.
453#[derive(Debug)]
454pub enum KeyResult<'a> {
455    /// All candidates have the same type for this key.
456    /// The deserializer can parse the value directly.
457    Unambiguous {
458        /// The shape all candidates expect for this field
459        shape: &'static Shape,
460    },
461
462    /// Candidates have different types for this key - need disambiguation.
463    /// Deserializer should parse the value, determine which fields it can
464    /// satisfy, and call `satisfy()` with the viable fields.
465    ///
466    /// **Important**: When multiple fields can be satisfied by the value,
467    /// pick the one with the lowest score (most specific). Scores are assigned
468    /// by specificity, e.g., `u8` has a lower score than `u16`.
469    Ambiguous {
470        /// The unique fields across remaining candidates (deduplicated by shape),
471        /// paired with a specificity score. Lower score = more specific type.
472        /// Deserializer should check which of these the value can satisfy,
473        /// then pick the one with the lowest score.
474        fields: Vec<(&'a FieldInfo, u64)>,
475    },
476
477    /// This key disambiguated to exactly one configuration.
478    Solved(ResolutionHandle<'a>),
479
480    /// This key doesn't exist in any remaining candidate.
481    Unknown,
482}
483
484/// Result of reporting which fields the value can satisfy.
485#[derive(Debug)]
486pub enum SatisfyResult<'a> {
487    /// Continue - still multiple candidates, keep feeding keys.
488    Continue,
489
490    /// Solved to exactly one configuration.
491    Solved(ResolutionHandle<'a>),
492
493    /// No configuration can accept the value (no fields were satisfied).
494    NoMatch,
495}
496
497/// State machine solver for lazy value-based disambiguation.
498///
499/// This solver only requests value inspection when candidates disagree on type.
500/// For keys where all candidates expect the same type, the deserializer can
501/// skip detailed value analysis.
502///
503/// # Example
504///
505/// ```rust
506/// use facet::Facet;
507/// use facet_solver::{Schema, Solver, KeyResult, SatisfyResult};
508///
509/// #[derive(Facet)]
510/// #[repr(u8)]
511/// enum NumericValue {
512///     Small(u8),
513///     Large(u16),
514/// }
515///
516/// #[derive(Facet)]
517/// struct Container {
518///     #[facet(flatten)]
519///     value: NumericValue,
520/// }
521///
522/// let schema = Schema::build(Container::SHAPE).unwrap();
523/// let mut solver = Solver::new(&schema);
524///
525/// // The field "0" has different types (u8 vs u16) - solver needs disambiguation
526/// match solver.see_key("0") {
527///     KeyResult::Ambiguous { fields } => {
528///         // Deserializer sees value "1000", checks which fields can accept it
529///         // u8 can't hold 1000, u16 can - so only report the u16 field
530///         // Fields come with specificity scores - lower = more specific
531///         let satisfied: Vec<_> = fields.iter()
532///             .filter(|(f, _score)| {
533///                 // deserializer's logic: can this value parse as this field's type?
534///                 f.value_shape.type_identifier == "u16"
535///             })
536///             .map(|(f, _)| *f)
537///             .collect();
538///
539///         match solver.satisfy(&satisfied) {
540///             SatisfyResult::Solved(config) => {
541///                 assert!(config.resolution().describe().contains("Large"));
542///             }
543///             _ => panic!("expected solved"),
544///         }
545///     }
546///     _ => panic!("expected Ambiguous"),
547/// }
548/// ```
549#[derive(Debug)]
550pub struct Solver<'a> {
551    /// Reference to the schema for configuration lookup
552    schema: &'a Schema,
553    /// Bitmask of remaining candidate configuration indices
554    candidates: ResolutionSet,
555    /// Set of seen keys for required field checking.
556    /// For Flat format, stores FieldKey::Flat. For Dom format, stores FieldKey::Dom.
557    seen_keys: BTreeSet<FieldKey<'a>>,
558}
559
560impl<'a> Solver<'a> {
561    /// Create a new solver from a schema.
562    pub fn new(schema: &'a Schema) -> Self {
563        Self {
564            schema,
565            candidates: ResolutionSet::full(schema.resolutions.len()),
566            seen_keys: BTreeSet::new(),
567        }
568    }
569
570    /// Report a key. Returns what to do next.
571    ///
572    /// - `Unambiguous`: All candidates agree on the type - parse directly
573    /// - `Ambiguous`: Types differ - check which fields the value can satisfy
574    /// - `Solved`: Disambiguated to one config
575    /// - `Unknown`: Key not found in any candidate
576    ///
577    /// Accepts both borrowed (`&str`) and owned (`String`) keys via `Cow`.
578    /// For DOM format, use `see_attribute()`, `see_element()`, etc. instead.
579    pub fn see_key(&mut self, key: impl Into<FieldKey<'a>>) -> KeyResult<'a> {
580        let key = key.into();
581        self.see_key_internal(key)
582    }
583
584    /// Report an attribute key (DOM format only).
585    pub fn see_attribute(&mut self, name: impl Into<Cow<'a, str>>) -> KeyResult<'a> {
586        self.see_key_internal(FieldKey::attribute(name))
587    }
588
589    /// Report an element key (DOM format only).
590    pub fn see_element(&mut self, name: impl Into<Cow<'a, str>>) -> KeyResult<'a> {
591        self.see_key_internal(FieldKey::element(name))
592    }
593
594    /// Report a text content key (DOM format only).
595    pub fn see_text(&mut self) -> KeyResult<'a> {
596        self.see_key_internal(FieldKey::text())
597    }
598
599    /// Internal implementation of key lookup.
600    fn see_key_internal(&mut self, key: FieldKey<'a>) -> KeyResult<'a> {
601        self.seen_keys.insert(key.clone());
602
603        // Key-based filtering - use appropriate index based on format
604        let resolutions_with_key = match (&key, self.schema.format) {
605            (FieldKey::Flat(name), Format::Flat) => {
606                self.schema.field_to_resolutions.get(name.as_ref())
607            }
608            (FieldKey::Flat(name), Format::Dom) => {
609                // Flat key on DOM schema - try as element (most common)
610                self.schema
611                    .dom_field_to_resolutions
612                    .get(&(FieldCategory::Element, name.as_ref()))
613            }
614            (FieldKey::Dom(cat, name), Format::Dom) => {
615                // For Text/Tag/Elements categories, the name is often empty
616                // because there's only one such field per struct. Search by category.
617                if matches!(
618                    cat,
619                    FieldCategory::Text | FieldCategory::Tag | FieldCategory::Elements
620                ) && name.is_empty()
621                {
622                    // Find any field with this category
623                    self.schema
624                        .dom_field_to_resolutions
625                        .iter()
626                        .find(|((c, _), _)| c == cat)
627                        .map(|(_, rs)| rs)
628                } else {
629                    self.schema
630                        .dom_field_to_resolutions
631                        .get(&(*cat, name.as_ref()))
632                }
633            }
634            (FieldKey::Dom(_, name), Format::Flat) => {
635                // DOM key on flat schema - ignore category
636                self.schema.field_to_resolutions.get(name.as_ref())
637            }
638        };
639
640        let resolutions_with_key = match resolutions_with_key {
641            Some(set) => set,
642            None => return KeyResult::Unknown,
643        };
644
645        // Check if this key exists in any current candidate.
646        // If not, treat it as unknown without modifying candidates.
647        // This ensures that extra/unknown fields don't eliminate valid candidates,
648        // which is important for "ignore unknown fields" semantics.
649        if !self.candidates.intersects(resolutions_with_key) {
650            return KeyResult::Unknown;
651        }
652
653        self.candidates.intersect_with(resolutions_with_key);
654
655        // Check if we've disambiguated to exactly one
656        if self.candidates.len() == 1 {
657            let idx = self.candidates.first().unwrap();
658            return KeyResult::Solved(self.handle(idx));
659        }
660
661        // Collect unique fields (by shape pointer) across remaining candidates
662        let mut unique_fields: Vec<&'a FieldInfo> = Vec::new();
663        for idx in self.candidates.iter() {
664            let config = &self.schema.resolutions[idx];
665            if let Some(info) = config.field_by_key(&key) {
666                // Deduplicate by shape pointer
667                if !unique_fields
668                    .iter()
669                    .any(|f| core::ptr::eq(f.value_shape, info.value_shape))
670                {
671                    unique_fields.push(info);
672                }
673            }
674        }
675
676        if unique_fields.len() == 1 {
677            // All candidates have the same type - unambiguous
678            KeyResult::Unambiguous {
679                shape: unique_fields[0].value_shape,
680            }
681        } else if unique_fields.is_empty() {
682            KeyResult::Unknown
683        } else {
684            // Different types - need disambiguation
685            // Attach specificity scores so caller can pick most specific when multiple match
686            let fields_with_scores: Vec<_> = unique_fields
687                .into_iter()
688                .map(|f| (f, specificity_score(f.value_shape)))
689                .collect();
690            KeyResult::Ambiguous {
691                fields: fields_with_scores,
692            }
693        }
694    }
695
696    /// Report which fields the value can satisfy after `Ambiguous` result.
697    ///
698    /// The deserializer should pass the subset of fields (from the `Ambiguous` result)
699    /// that the actual value can be parsed into.
700    pub fn satisfy(&mut self, satisfied_fields: &[&FieldInfo]) -> SatisfyResult<'a> {
701        let satisfied_shapes: Vec<_> = satisfied_fields.iter().map(|f| f.value_shape).collect();
702        self.satisfy_shapes(&satisfied_shapes)
703    }
704
705    /// Report which shapes the value can satisfy after `Ambiguous` result from `probe_key`.
706    ///
707    /// This is the shape-based version of `satisfy`, used when disambiguating
708    /// by nested field types. The deserializer should pass the shapes that
709    /// the actual value can be parsed into.
710    ///
711    /// # Example
712    ///
713    /// ```rust
714    /// use facet::Facet;
715    /// use facet_solver::{Schema, Solver, KeyResult, SatisfyResult};
716    ///
717    /// #[derive(Facet)]
718    /// struct SmallPayload { value: u8 }
719    ///
720    /// #[derive(Facet)]
721    /// struct LargePayload { value: u16 }
722    ///
723    /// #[derive(Facet)]
724    /// #[repr(u8)]
725    /// enum PayloadKind {
726    ///     Small { payload: SmallPayload },
727    ///     Large { payload: LargePayload },
728    /// }
729    ///
730    /// #[derive(Facet)]
731    /// struct Container {
732    ///     #[facet(flatten)]
733    ///     inner: PayloadKind,
734    /// }
735    ///
736    /// let schema = Schema::build(Container::SHAPE).unwrap();
737    /// let mut solver = Solver::new(&schema);
738    ///
739    /// // Report nested key
740    /// solver.probe_key(&[], "payload");
741    ///
742    /// // At payload.value, value is 1000 - doesn't fit u8
743    /// // Get shapes at this path
744    /// let shapes = solver.get_shapes_at_path(&["payload", "value"]);
745    /// // Filter to shapes that can hold 1000
746    /// let works: Vec<_> = shapes.iter()
747    ///     .filter(|s| s.type_identifier == "u16")
748    ///     .copied()
749    ///     .collect();
750    /// solver.satisfy_shapes(&works);
751    /// ```
752    pub fn satisfy_shapes(&mut self, satisfied_shapes: &[&'static Shape]) -> SatisfyResult<'a> {
753        if satisfied_shapes.is_empty() {
754            self.candidates = ResolutionSet::empty(self.schema.resolutions.len());
755            return SatisfyResult::NoMatch;
756        }
757
758        let mut new_candidates = ResolutionSet::empty(self.schema.resolutions.len());
759        for idx in self.candidates.iter() {
760            let config = &self.schema.resolutions[idx];
761            // Check if any of this config's fields match the satisfied shapes
762            for field in config.fields().values() {
763                if satisfied_shapes
764                    .iter()
765                    .any(|s| core::ptr::eq(*s, field.value_shape))
766                {
767                    new_candidates.insert(idx);
768                    break;
769                }
770            }
771        }
772        self.candidates = new_candidates;
773
774        match self.candidates.len() {
775            0 => SatisfyResult::NoMatch,
776            1 => {
777                let idx = self.candidates.first().unwrap();
778                SatisfyResult::Solved(self.handle(idx))
779            }
780            _ => SatisfyResult::Continue,
781        }
782    }
783
784    /// Get the shapes at a nested path across all remaining candidates.
785    ///
786    /// This is useful when you have an `Ambiguous` result from `probe_key`
787    /// and need to know what types are possible at that path.
788    pub fn get_shapes_at_path(&self, path: &[&str]) -> Vec<&'static Shape> {
789        let mut shapes: Vec<&'static Shape> = Vec::new();
790        for idx in self.candidates.iter() {
791            let config = &self.schema.resolutions[idx];
792            if let Some(shape) = self.get_shape_at_path(config, path)
793                && !shapes.iter().any(|s| core::ptr::eq(*s, shape))
794            {
795                shapes.push(shape);
796            }
797        }
798        shapes
799    }
800
801    /// Report which shapes at a nested path the value can satisfy.
802    ///
803    /// This is the path-aware version of `satisfy_shapes`, used when disambiguating
804    /// by nested field types after `probe_key`.
805    ///
806    /// - `path`: The full path to the field (e.g., `["payload", "value"]`)
807    /// - `satisfied_shapes`: The shapes that the value can be parsed into
808    pub fn satisfy_at_path(
809        &mut self,
810        path: &[&str],
811        satisfied_shapes: &[&'static Shape],
812    ) -> SatisfyResult<'a> {
813        if satisfied_shapes.is_empty() {
814            self.candidates = ResolutionSet::empty(self.schema.resolutions.len());
815            return SatisfyResult::NoMatch;
816        }
817
818        // Keep only candidates where the shape at this path is in the satisfied set
819        let mut new_candidates = ResolutionSet::empty(self.schema.resolutions.len());
820        for idx in self.candidates.iter() {
821            let config = &self.schema.resolutions[idx];
822            if let Some(shape) = self.get_shape_at_path(config, path)
823                && satisfied_shapes.iter().any(|s| core::ptr::eq(*s, shape))
824            {
825                new_candidates.insert(idx);
826            }
827        }
828        self.candidates = new_candidates;
829
830        match self.candidates.len() {
831            0 => SatisfyResult::NoMatch,
832            1 => {
833                let idx = self.candidates.first().unwrap();
834                SatisfyResult::Solved(self.handle(idx))
835            }
836            _ => SatisfyResult::Continue,
837        }
838    }
839
840    /// Get the current candidate resolutions.
841    pub fn candidates(&self) -> Vec<ResolutionHandle<'a>> {
842        self.candidates.iter().map(|idx| self.handle(idx)).collect()
843    }
844
845    /// Get the seen keys.
846    /// Get the seen keys.
847    pub const fn seen_keys(&self) -> &BTreeSet<FieldKey<'a>> {
848        &self.seen_keys
849    }
850
851    /// Check if a field name was seen (regardless of category for DOM format).
852    pub fn was_field_seen(&self, field_name: &str) -> bool {
853        self.seen_keys.iter().any(|k| k.name() == field_name)
854    }
855
856    #[inline]
857    fn handle(&self, idx: usize) -> ResolutionHandle<'a> {
858        ResolutionHandle::from_schema(self.schema, idx)
859    }
860
861    /// Hint that a specific enum variant should be selected.
862    ///
863    /// This filters the candidates to only those resolutions where at least one
864    /// variant selection has the given variant name. This is useful for explicit
865    /// type disambiguation via annotations (e.g., type annotations in various formats).
866    ///
867    /// Returns `true` if at least one candidate remains after filtering, `false` if
868    /// no candidates match the variant name (in which case candidates are unchanged).
869    ///
870    /// # Example
871    ///
872    /// ```rust
873    /// use facet::Facet;
874    /// use facet_solver::{Schema, Solver};
875    ///
876    /// #[derive(Facet)]
877    /// struct HttpSource { url: String }
878    ///
879    /// #[derive(Facet)]
880    /// struct GitSource { url: String, branch: String }
881    ///
882    /// #[derive(Facet)]
883    /// #[repr(u8)]
884    /// enum SourceKind {
885    ///     Http(HttpSource),
886    ///     Git(GitSource),
887    /// }
888    ///
889    /// #[derive(Facet)]
890    /// struct Source {
891    ///     #[facet(flatten)]
892    ///     kind: SourceKind,
893    /// }
894    ///
895    /// let schema = Schema::build(Source::SHAPE).unwrap();
896    /// let mut solver = Solver::new(&schema);
897    ///
898    /// // Without hint, both variants are candidates
899    /// assert_eq!(solver.candidates().len(), 2);
900    ///
901    /// // Hint at Http variant
902    /// assert!(solver.hint_variant("Http"));
903    /// assert_eq!(solver.candidates().len(), 1);
904    /// ```
905    pub fn hint_variant(&mut self, variant_name: &str) -> bool {
906        // Build a set of configs that have this variant name
907        let mut matching = ResolutionSet::empty(self.schema.resolutions.len());
908
909        for idx in self.candidates.iter() {
910            let config = &self.schema.resolutions[idx];
911            // Check if any variant selection matches the given name
912            if config
913                .variant_selections()
914                .iter()
915                .any(|vs| vs.variant_name == variant_name)
916            {
917                matching.insert(idx);
918            }
919        }
920
921        if matching.is_empty() {
922            // No matches - keep candidates unchanged
923            false
924        } else {
925            self.candidates = matching;
926            true
927        }
928    }
929
930    /// Hint that a variant is selected, but only if the field is actually a tag field
931    /// for an internally-tagged enum.
932    ///
933    /// This is safer than `hint_variant` because it validates that `tag_field_name`
934    /// is actually the tag field for an internally-tagged enum in at least one
935    /// candidate resolution before applying the hint.
936    ///
937    /// Returns `true` if the hint was applied (field was a valid tag field and
938    /// at least one candidate matches), `false` otherwise.
939    pub fn hint_variant_for_tag(&mut self, tag_field_name: &str, variant_name: &str) -> bool {
940        // First check if any candidate has this field as an internally-tagged enum tag field
941        let is_tag_field = self.candidates.iter().any(|idx| {
942            let config = &self.schema.resolutions[idx];
943            // Look for a field with the given name that is a tag field
944            config.fields().values().any(|field| {
945                field.serialized_name == tag_field_name
946                    && field
947                        .value_shape
948                        .get_tag_attr()
949                        .is_some_and(|tag| tag == tag_field_name)
950                    && field.value_shape.get_content_attr().is_none()
951            })
952        });
953
954        if !is_tag_field {
955            return false;
956        }
957
958        // Now apply the variant hint
959        self.hint_variant(variant_name)
960    }
961
962    /// Mark a key as seen without filtering candidates.
963    ///
964    /// This is useful when the key is known to be present through means other than
965    /// parsing (e.g., type annotations). Call this after `hint_variant` to mark
966    /// the variant name as seen so that `finish()` doesn't report it as missing.
967    pub fn mark_seen(&mut self, key: impl Into<FieldKey<'a>>) {
968        self.seen_keys.insert(key.into());
969    }
970
971    /// Report a key at a nested path. Returns what to do next.
972    ///
973    /// This is the depth-aware version of `see_key`. Use this when probing
974    /// nested structures where disambiguation might require looking inside objects.
975    ///
976    /// - `path`: The ancestor keys (e.g., `["payload"]` when inside a payload object)
977    /// - `key`: The key found at this level (e.g., `"value"`)
978    ///
979    /// # Example
980    ///
981    /// ```rust
982    /// use facet::Facet;
983    /// use facet_solver::{Schema, Solver, KeyResult};
984    ///
985    /// #[derive(Facet)]
986    /// struct SmallPayload { value: u8 }
987    ///
988    /// #[derive(Facet)]
989    /// struct LargePayload { value: u16 }
990    ///
991    /// #[derive(Facet)]
992    /// #[repr(u8)]
993    /// enum PayloadKind {
994    ///     Small { payload: SmallPayload },
995    ///     Large { payload: LargePayload },
996    /// }
997    ///
998    /// #[derive(Facet)]
999    /// struct Container {
1000    ///     #[facet(flatten)]
1001    ///     inner: PayloadKind,
1002    /// }
1003    ///
1004    /// let schema = Schema::build(Container::SHAPE).unwrap();
1005    /// let mut solver = Solver::new(&schema);
1006    ///
1007    /// // "payload" exists in both - keep going
1008    /// solver.probe_key(&[], "payload");
1009    ///
1010    /// // "value" inside payload - both have it but different types!
1011    /// match solver.probe_key(&["payload"], "value") {
1012    ///     KeyResult::Ambiguous { fields } => {
1013    ///         // fields is Vec<(&FieldInfo, u64)> - field + specificity score
1014    ///         // Deserializer checks: 1000 fits u16 but not u8
1015    ///         // When multiple match, pick the one with lowest score (most specific)
1016    ///     }
1017    ///     _ => {}
1018    /// }
1019    /// ```
1020    pub fn probe_key(&mut self, path: &[&str], key: &str) -> KeyResult<'a> {
1021        // Build full path
1022        let mut full_path: Vec<&str> = path.to_vec();
1023        full_path.push(key);
1024
1025        // Filter candidates to only those that have this key path
1026        let mut new_candidates = ResolutionSet::empty(self.schema.resolutions.len());
1027        for idx in self.candidates.iter() {
1028            let config = &self.schema.resolutions[idx];
1029            if config.has_key_path(&full_path) {
1030                new_candidates.insert(idx);
1031            }
1032        }
1033        self.candidates = new_candidates;
1034
1035        if self.candidates.is_empty() {
1036            return KeyResult::Unknown;
1037        }
1038
1039        // Check if we've disambiguated to exactly one
1040        if self.candidates.len() == 1 {
1041            let idx = self.candidates.first().unwrap();
1042            return KeyResult::Solved(self.handle(idx));
1043        }
1044
1045        // Get the shape at this path for each remaining candidate
1046        // We need to traverse the type tree to find the actual field type
1047        let mut unique_shapes: Vec<(&'static Shape, usize)> = Vec::new(); // (shape, resolution_idx)
1048
1049        for idx in self.candidates.iter() {
1050            let config = &self.schema.resolutions[idx];
1051            if let Some(shape) = self.get_shape_at_path(config, &full_path) {
1052                // Deduplicate by shape pointer
1053                if !unique_shapes.iter().any(|(s, _)| core::ptr::eq(*s, shape)) {
1054                    unique_shapes.push((shape, idx));
1055                }
1056            }
1057        }
1058
1059        match unique_shapes.len() {
1060            0 => KeyResult::Unknown,
1061            1 => {
1062                // All candidates have the same type at this path - unambiguous
1063                KeyResult::Unambiguous {
1064                    shape: unique_shapes[0].0,
1065                }
1066            }
1067            _ => {
1068                // Different types at this path - need disambiguation
1069                // Build FieldInfo with scores for each unique shape
1070                let fields: Vec<(&'a FieldInfo, u64)> = unique_shapes
1071                    .iter()
1072                    .filter_map(|(shape, idx)| {
1073                        let config = &self.schema.resolutions[*idx];
1074                        // For nested paths, we need the parent field
1075                        // e.g., for ["payload", "value"], get the "payload" field
1076                        let field = if path.is_empty() {
1077                            config.field_by_name(key)
1078                        } else {
1079                            // Return the top-level field that contains this path
1080                            config.field_by_name(path[0])
1081                        }?;
1082                        Some((field, specificity_score(shape)))
1083                    })
1084                    .collect();
1085
1086                KeyResult::Ambiguous { fields }
1087            }
1088        }
1089    }
1090
1091    /// Get the shape at a nested path within a configuration.
1092    fn get_shape_at_path(&self, config: &'a Resolution, path: &[&str]) -> Option<&'static Shape> {
1093        if path.is_empty() {
1094            return None;
1095        }
1096
1097        // Start with the top-level field
1098        let top_field = config.field_by_name(path[0])?;
1099        let mut current_shape = top_field.value_shape;
1100
1101        // Navigate through nested structs
1102        for &key in &path[1..] {
1103            current_shape = self.get_field_shape(current_shape, key)?;
1104        }
1105
1106        Some(current_shape)
1107    }
1108
1109    /// Get the shape of a field within a struct shape.
1110    fn get_field_shape(&self, shape: &'static Shape, field_name: &str) -> Option<&'static Shape> {
1111        use facet_core::{StructType, Type, UserType};
1112
1113        match shape.ty {
1114            Type::User(UserType::Struct(StructType { fields, .. })) => {
1115                for field in fields {
1116                    if field.effective_name() == field_name {
1117                        return Some(field.shape());
1118                    }
1119                }
1120                None
1121            }
1122            _ => None,
1123        }
1124    }
1125
1126    /// Finish solving. Call this after all keys have been processed.
1127    ///
1128    /// This method is necessary because key-based filtering alone cannot disambiguate
1129    /// when one variant's required fields are a subset of another's.
1130    ///
1131    /// # Why not just use `see_key()` results?
1132    ///
1133    /// `see_key()` returns `Solved` when a key *excludes* candidates down to one.
1134    /// But when the input is a valid subset of multiple variants, no key excludes
1135    /// anything — you need `finish()` to check which candidates have all their
1136    /// required fields satisfied.
1137    ///
1138    /// # Example
1139    ///
1140    /// ```rust,ignore
1141    /// enum Source {
1142    ///     Http { url: String },                  // required: url
1143    ///     Git { url: String, branch: String },   // required: url, branch
1144    /// }
1145    /// ```
1146    ///
1147    /// | Input                  | `see_key` behavior                        | Resolution            |
1148    /// |------------------------|-------------------------------------------|-----------------------|
1149    /// | `{ "url", "branch" }`  | `branch` excludes `Http` → candidates = 1 | Early `Solved(Git)`   |
1150    /// | `{ "url" }`            | both have `url` → candidates = 2          | `finish()` → `Http`   |
1151    ///
1152    /// In the second case, no key ever excludes a candidate. Only `finish()` can
1153    /// determine that `Git` is missing its required `branch` field, leaving `Http`
1154    /// as the sole viable configuration.
1155    #[allow(clippy::result_large_err)] // SolverError intentionally contains detailed diagnostic info
1156    pub fn finish(self) -> Result<ResolutionHandle<'a>, SolverError> {
1157        let Solver {
1158            schema,
1159            candidates,
1160            seen_keys,
1161        } = self;
1162
1163        // Compute all known fields across all resolutions (for unknown field detection)
1164        let all_known_fields: BTreeSet<&'static str> = schema
1165            .resolutions
1166            .iter()
1167            .flat_map(|r| r.fields().values().map(|f| f.serialized_name))
1168            .collect();
1169
1170        // Find unknown fields (fields in input that don't exist in ANY resolution)
1171        let unknown_fields: Vec<String> = seen_keys
1172            .iter()
1173            .filter(|k| !all_known_fields.contains(k.name()))
1174            .map(|k| k.name().to_string())
1175            .collect();
1176
1177        // Compute suggestions for unknown fields
1178        let suggestions = compute_suggestions(&unknown_fields, &all_known_fields);
1179
1180        if candidates.is_empty() {
1181            // Build per-candidate failure info for all resolutions
1182            let mut candidate_failures: Vec<CandidateFailure> = schema
1183                .resolutions
1184                .iter()
1185                .map(|config| build_candidate_failure(config, &seen_keys))
1186                .collect();
1187
1188            // Sort by closeness (best match first)
1189            sort_candidates_by_closeness(&mut candidate_failures);
1190
1191            return Err(SolverError::NoMatch {
1192                input_fields: seen_keys.iter().map(|k| k.name().to_string()).collect(),
1193                missing_required: Vec::new(),
1194                missing_required_detailed: Vec::new(),
1195                unknown_fields,
1196                closest_resolution: None,
1197                candidate_failures,
1198                suggestions,
1199            });
1200        }
1201
1202        // Filter candidates to only those that have all required fields satisfied
1203        let viable: Vec<usize> = candidates
1204            .iter()
1205            .filter(|idx| {
1206                let config = &schema.resolutions[*idx];
1207                config
1208                    .required_field_names()
1209                    .iter()
1210                    .all(|f| seen_keys.iter().any(|k| k.name() == *f))
1211            })
1212            .collect();
1213
1214        match viable.len() {
1215            0 => {
1216                // No viable candidates - build per-candidate failure info
1217                let mut candidate_failures: Vec<CandidateFailure> = candidates
1218                    .iter()
1219                    .map(|idx| {
1220                        let config = &schema.resolutions[idx];
1221                        build_candidate_failure(config, &seen_keys)
1222                    })
1223                    .collect();
1224
1225                // Sort by closeness (best match first)
1226                sort_candidates_by_closeness(&mut candidate_failures);
1227
1228                // For backwards compatibility, also populate the "closest" fields
1229                // Now use the first (closest) candidate after sorting
1230                let closest_name = candidate_failures.first().map(|f| f.variant_name.clone());
1231                let closest_config = closest_name
1232                    .as_ref()
1233                    .and_then(|name| schema.resolutions.iter().find(|r| r.describe() == *name));
1234
1235                let (missing, missing_detailed, closest_resolution) =
1236                    if let Some(config) = closest_config {
1237                        let missing: Vec<_> = config
1238                            .required_field_names()
1239                            .iter()
1240                            .filter(|f| !seen_keys.iter().any(|k| k.name() == **f))
1241                            .copied()
1242                            .collect();
1243                        let missing_detailed: Vec<_> = missing
1244                            .iter()
1245                            .filter_map(|name| config.field_by_name(name))
1246                            .map(MissingFieldInfo::from_field_info)
1247                            .collect();
1248                        (missing, missing_detailed, Some(config.describe()))
1249                    } else {
1250                        (Vec::new(), Vec::new(), None)
1251                    };
1252
1253                Err(SolverError::NoMatch {
1254                    input_fields: seen_keys.iter().map(|s| s.to_string()).collect(),
1255                    missing_required: missing,
1256                    missing_required_detailed: missing_detailed,
1257                    unknown_fields,
1258                    closest_resolution,
1259                    candidate_failures,
1260                    suggestions,
1261                })
1262            }
1263            1 => {
1264                // Exactly one viable candidate - success!
1265                Ok(ResolutionHandle::from_schema(schema, viable[0]))
1266            }
1267            _ => {
1268                // Multiple viable candidates - ambiguous!
1269                let configs: Vec<_> = viable.iter().map(|&idx| &schema.resolutions[idx]).collect();
1270                let candidates: Vec<String> = configs.iter().map(|c| c.describe()).collect();
1271                let disambiguating_fields = find_disambiguating_fields(&configs);
1272
1273                Err(SolverError::Ambiguous {
1274                    candidates,
1275                    disambiguating_fields,
1276                })
1277            }
1278        }
1279    }
1280}
1281
1282/// Build a CandidateFailure for a resolution given the seen keys.
1283fn build_candidate_failure<'a>(
1284    config: &Resolution,
1285    seen_keys: &BTreeSet<FieldKey<'a>>,
1286) -> CandidateFailure {
1287    let missing_fields: Vec<MissingFieldInfo> = config
1288        .required_field_names()
1289        .iter()
1290        .filter(|f| !seen_keys.iter().any(|k| k.name() == **f))
1291        .filter_map(|f| config.field_by_name(f))
1292        .map(MissingFieldInfo::from_field_info)
1293        .collect();
1294
1295    let unknown_fields: Vec<String> = seen_keys
1296        .iter()
1297        .filter(|k| config.field_by_key(k).is_none())
1298        .map(|k| k.name().to_string())
1299        .collect();
1300
1301    // Compute closeness score for ranking
1302    let suggestion_matches = compute_closeness_score(&unknown_fields, &missing_fields, config);
1303
1304    CandidateFailure {
1305        variant_name: config.describe(),
1306        missing_fields,
1307        unknown_fields,
1308        suggestion_matches,
1309    }
1310}
1311
1312/// Compute a closeness score for ranking candidates.
1313/// Higher score = more likely the user intended this variant.
1314///
1315/// The score considers:
1316/// - Typo matches: unknown fields that are similar to known fields (weighted by similarity)
1317/// - Field coverage: if we fixed typos, would we have all required fields?
1318/// - Missing fields: fewer missing = better
1319/// - Unknown fields: fewer truly unknown (no suggestion) = better
1320#[cfg(feature = "suggestions")]
1321fn compute_closeness_score(
1322    unknown_fields: &[String],
1323    missing_fields: &[MissingFieldInfo],
1324    config: &Resolution,
1325) -> usize {
1326    const SIMILARITY_THRESHOLD: f64 = 0.6;
1327
1328    // Score components (scaled to integers for easy comparison)
1329    let mut typo_score: usize = 0;
1330    let mut fields_that_would_match: usize = 0;
1331
1332    // For each unknown field, find best matching known field
1333    for unknown in unknown_fields {
1334        let mut best_similarity = 0.0f64;
1335        let mut best_match: Option<&str> = None;
1336
1337        for info in config.fields().values() {
1338            let similarity = strsim::jaro_winkler(unknown, info.serialized_name);
1339            if similarity >= SIMILARITY_THRESHOLD && similarity > best_similarity {
1340                best_similarity = similarity;
1341                best_match = Some(info.serialized_name);
1342            }
1343        }
1344
1345        if let Some(_matched_field) = best_match {
1346            // Weight by similarity: 0.6 -> 60 points, 1.0 -> 100 points
1347            typo_score += (best_similarity * 100.0) as usize;
1348            fields_that_would_match += 1;
1349        }
1350    }
1351
1352    // Calculate how many required fields would be satisfied if typos were fixed
1353    let required_count = config.required_field_names().len();
1354    let currently_missing = missing_fields.len();
1355    let would_be_missing = currently_missing.saturating_sub(fields_that_would_match);
1356
1357    // Coverage score: percentage of required fields that would be present
1358    let coverage_score = if required_count > 0 {
1359        ((required_count - would_be_missing) * 100) / required_count
1360    } else {
1361        100 // No required fields = perfect coverage
1362    };
1363
1364    // Penalty for truly unknown fields (no typo suggestion)
1365    let truly_unknown = unknown_fields.len().saturating_sub(fields_that_would_match);
1366    let unknown_penalty = truly_unknown * 10;
1367
1368    // Combine scores: typo matches are most important, then coverage, then penalties
1369    // Each typo match can give up to 100 points, so scale coverage to match
1370    typo_score + coverage_score.saturating_sub(unknown_penalty)
1371}
1372
1373/// Compute closeness score (no-op without suggestions feature).
1374#[cfg(not(feature = "suggestions"))]
1375fn compute_closeness_score(
1376    _unknown_fields: &[String],
1377    _missing_fields: &[MissingFieldInfo],
1378    _config: &Resolution,
1379) -> usize {
1380    0
1381}
1382
1383/// Sort candidate failures by closeness (best match first).
1384fn sort_candidates_by_closeness(failures: &mut [CandidateFailure]) {
1385    failures.sort_by(|a, b| {
1386        // Higher suggestion_matches (closeness score) first
1387        b.suggestion_matches.cmp(&a.suggestion_matches)
1388    });
1389}
1390
1391/// Compute "did you mean?" suggestions for unknown fields.
1392#[cfg(feature = "suggestions")]
1393fn compute_suggestions(
1394    unknown_fields: &[String],
1395    all_known_fields: &BTreeSet<&'static str>,
1396) -> Vec<FieldSuggestion> {
1397    const SIMILARITY_THRESHOLD: f64 = 0.6;
1398
1399    let mut suggestions = Vec::new();
1400
1401    for unknown in unknown_fields {
1402        let mut best_match: Option<(&'static str, f64)> = None;
1403
1404        for known in all_known_fields {
1405            let similarity = strsim::jaro_winkler(unknown, known);
1406            if similarity >= SIMILARITY_THRESHOLD
1407                && best_match.is_none_or(|(_, best_sim)| similarity > best_sim)
1408            {
1409                best_match = Some((known, similarity));
1410            }
1411        }
1412
1413        if let Some((suggestion, similarity)) = best_match {
1414            suggestions.push(FieldSuggestion {
1415                unknown: unknown.clone(),
1416                suggestion,
1417                similarity,
1418            });
1419        }
1420    }
1421
1422    suggestions
1423}
1424
1425/// Compute "did you mean?" suggestions for unknown fields (no-op without strsim).
1426#[cfg(not(feature = "suggestions"))]
1427fn compute_suggestions(
1428    _unknown_fields: &[String],
1429    _all_known_fields: &BTreeSet<&'static str>,
1430) -> Vec<FieldSuggestion> {
1431    Vec::new()
1432}
1433
1434// ============================================================================
1435// Probing Solver (Depth-Aware)
1436// ============================================================================
1437
1438/// Result of reporting a key to the probing solver.
1439#[derive(Debug)]
1440pub enum ProbeResult<'a> {
1441    /// Keep reporting keys - not yet disambiguated
1442    KeepGoing,
1443    /// Solved! Use this configuration
1444    Solved(&'a Resolution),
1445    /// No configuration matches the observed keys
1446    NoMatch,
1447}
1448
1449/// Depth-aware probing solver for streaming deserialization.
1450///
1451/// Unlike the batch solver, this solver accepts
1452/// key reports at arbitrary depths. It's designed for the "peek" strategy:
1453///
1454/// 1. Deserializer scans keys (without parsing values) and reports them
1455/// 2. Solver filters candidates based on which configs have that key path
1456/// 3. Once one candidate remains, solver returns `Solved`
1457/// 4. Deserializer rewinds and parses into the resolved type
1458///
1459/// # Example
1460///
1461/// ```rust
1462/// use facet::Facet;
1463/// use facet_solver::{Schema, ProbingSolver, ProbeResult};
1464///
1465/// #[derive(Facet)]
1466/// struct TextPayload { content: String }
1467///
1468/// #[derive(Facet)]
1469/// struct BinaryPayload { bytes: Vec<u8> }
1470///
1471/// #[derive(Facet)]
1472/// #[repr(u8)]
1473/// enum MessageKind {
1474///     Text { payload: TextPayload },
1475///     Binary { payload: BinaryPayload },
1476/// }
1477///
1478/// #[derive(Facet)]
1479/// struct Message {
1480///     id: String,
1481///     #[facet(flatten)]
1482///     kind: MessageKind,
1483/// }
1484///
1485/// let schema = Schema::build(Message::SHAPE).unwrap();
1486/// let mut solver = ProbingSolver::new(&schema);
1487///
1488/// // "id" exists in both configs - keep going
1489/// assert!(matches!(solver.probe_key(&[], "id"), ProbeResult::KeepGoing));
1490///
1491/// // "payload" exists in both configs - keep going
1492/// assert!(matches!(solver.probe_key(&[], "payload"), ProbeResult::KeepGoing));
1493///
1494/// // "content" inside payload only exists in Text - solved!
1495/// match solver.probe_key(&["payload"], "content") {
1496///     ProbeResult::Solved(config) => {
1497///         assert!(config.has_key_path(&["payload", "content"]));
1498///     }
1499///     _ => panic!("expected Solved"),
1500/// }
1501/// ```
1502#[derive(Debug)]
1503pub struct ProbingSolver<'a> {
1504    /// Remaining candidate resolutions
1505    candidates: Vec<&'a Resolution>,
1506}
1507
1508impl<'a> ProbingSolver<'a> {
1509    /// Create a new probing solver from a schema.
1510    pub fn new(schema: &'a Schema) -> Self {
1511        Self {
1512            candidates: schema.resolutions.iter().collect(),
1513        }
1514    }
1515
1516    /// Create a new probing solver from resolutions directly.
1517    pub fn from_resolutions(configs: &'a [Resolution]) -> Self {
1518        Self {
1519            candidates: configs.iter().collect(),
1520        }
1521    }
1522
1523    /// Report a key found at a path during probing.
1524    ///
1525    /// - `path`: The ancestor keys (e.g., `["payload"]` when inside the payload object)
1526    /// - `key`: The key found at this level (e.g., `"content"`)
1527    ///
1528    /// Returns what to do next.
1529    pub fn probe_key(&mut self, path: &[&str], key: &str) -> ProbeResult<'a> {
1530        // Build the full key path (runtime strings, compared against static schema)
1531        let mut full_path: Vec<&str> = path.to_vec();
1532        full_path.push(key);
1533
1534        // Filter to candidates that have this key path
1535        self.candidates.retain(|c| c.has_key_path(&full_path));
1536
1537        match self.candidates.len() {
1538            0 => ProbeResult::NoMatch,
1539            1 => ProbeResult::Solved(self.candidates[0]),
1540            _ => ProbeResult::KeepGoing,
1541        }
1542    }
1543
1544    /// Get the current candidate resolutions.
1545    pub fn candidates(&self) -> &[&'a Resolution] {
1546        &self.candidates
1547    }
1548
1549    /// Finish probing - returns Solved if exactly one candidate remains.
1550    pub fn finish(&self) -> ProbeResult<'a> {
1551        match self.candidates.len() {
1552            0 => ProbeResult::NoMatch,
1553            1 => ProbeResult::Solved(self.candidates[0]),
1554            _ => ProbeResult::KeepGoing, // Still ambiguous
1555        }
1556    }
1557}
1558
1559// ============================================================================
1560// Variant Format Classification
1561// ============================================================================
1562
1563/// Classification of an enum variant's expected serialized format.
1564///
1565/// This is used by deserializers to determine how to parse untagged enum variants
1566/// based on the YAML/JSON/etc. value type they encounter.
1567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1568pub enum VariantFormat {
1569    /// Unit variant: no fields, serializes as the variant name or nothing for untagged
1570    Unit,
1571
1572    /// Newtype variant wrapping a scalar type (String, numbers, bool, etc.)
1573    /// Serializes as just the scalar value for untagged enums.
1574    NewtypeScalar {
1575        /// The shape of the inner scalar type
1576        inner_shape: &'static Shape,
1577    },
1578
1579    /// Newtype variant wrapping a struct
1580    /// Serializes as a mapping for untagged enums.
1581    NewtypeStruct {
1582        /// The shape of the inner struct type
1583        inner_shape: &'static Shape,
1584    },
1585
1586    /// Newtype variant wrapping a tuple struct/tuple
1587    /// Serializes as a sequence for untagged enums.
1588    NewtypeTuple {
1589        /// The shape of the inner tuple type
1590        inner_shape: &'static Shape,
1591        /// Number of elements in the inner tuple
1592        arity: usize,
1593    },
1594
1595    /// Newtype variant wrapping a sequence type (Vec, Array, Slice, Set)
1596    /// Serializes as a sequence for untagged enums.
1597    NewtypeSequence {
1598        /// The shape of the inner sequence type
1599        inner_shape: &'static Shape,
1600    },
1601
1602    /// Newtype variant wrapping another type (enum, map, etc.)
1603    NewtypeOther {
1604        /// The shape of the inner type
1605        inner_shape: &'static Shape,
1606    },
1607
1608    /// Tuple variant with multiple fields
1609    /// Serializes as a sequence for untagged enums.
1610    Tuple {
1611        /// Number of fields in the tuple
1612        arity: usize,
1613    },
1614
1615    /// Struct variant with named fields
1616    /// Serializes as a mapping for untagged enums.
1617    Struct,
1618}
1619
1620impl VariantFormat {
1621    /// Classify a variant's expected serialized format.
1622    pub fn from_variant(variant: &'static Variant) -> Self {
1623        use facet_core::StructKind;
1624
1625        let fields = variant.data.fields;
1626        let kind = variant.data.kind;
1627
1628        match kind {
1629            StructKind::Unit => VariantFormat::Unit,
1630            // TupleStruct and Tuple are both used for tuple-like variants
1631            // depending on how they're defined. Handle them the same way.
1632            StructKind::TupleStruct | StructKind::Tuple => {
1633                if fields.len() == 1 {
1634                    // Newtype variant - classify by inner type
1635                    let field_shape = fields[0].shape();
1636                    // Dereference through pointers to get the actual inner type
1637                    let inner_shape = deref_pointer(field_shape);
1638
1639                    // Check if this is a metadata container (like Spanned<T>) and unwrap it for classification
1640                    // This allows untagged enum variants containing Spanned<String> etc.
1641                    // to match scalar values transparently
1642                    let classification_shape = if let Some(inner) =
1643                        facet_reflect::get_metadata_container_value_shape(field_shape)
1644                    {
1645                        inner
1646                    } else {
1647                        field_shape
1648                    };
1649
1650                    if is_scalar_shape(classification_shape)
1651                        || is_unit_enum_shape(classification_shape)
1652                    {
1653                        // Scalars and unit-only enums both serialize as primitive values
1654                        // Store the classification shape (unwrapped from Spanned if needed)
1655                        // so that type matching works correctly for multi-variant untagged enums
1656                        VariantFormat::NewtypeScalar {
1657                            inner_shape: classification_shape,
1658                        }
1659                    } else if let Some(arity) = tuple_struct_arity(classification_shape) {
1660                        VariantFormat::NewtypeTuple { inner_shape, arity }
1661                    } else if is_named_struct_shape(classification_shape)
1662                        || is_map_shape(classification_shape)
1663                    {
1664                        VariantFormat::NewtypeStruct { inner_shape }
1665                    } else if is_sequence_shape(classification_shape) {
1666                        VariantFormat::NewtypeSequence { inner_shape }
1667                    } else {
1668                        VariantFormat::NewtypeOther { inner_shape }
1669                    }
1670                } else {
1671                    // Multi-field tuple variant
1672                    VariantFormat::Tuple {
1673                        arity: fields.len(),
1674                    }
1675                }
1676            }
1677            StructKind::Struct => VariantFormat::Struct,
1678        }
1679    }
1680
1681    /// Returns true if this variant expects a scalar value in untagged format.
1682    pub const fn expects_scalar(&self) -> bool {
1683        matches!(self, VariantFormat::NewtypeScalar { .. })
1684    }
1685
1686    /// Returns true if this variant expects a sequence in untagged format.
1687    pub const fn expects_sequence(&self) -> bool {
1688        matches!(
1689            self,
1690            VariantFormat::Tuple { .. }
1691                | VariantFormat::NewtypeTuple { .. }
1692                | VariantFormat::NewtypeSequence { .. }
1693        )
1694    }
1695
1696    /// Returns true if this variant expects a mapping in untagged format.
1697    pub const fn expects_mapping(&self) -> bool {
1698        matches!(
1699            self,
1700            VariantFormat::Struct | VariantFormat::NewtypeStruct { .. }
1701        )
1702    }
1703
1704    /// Returns true if this is a unit variant (no data).
1705    pub const fn is_unit(&self) -> bool {
1706        matches!(self, VariantFormat::Unit)
1707    }
1708}
1709
1710/// Dereference through pointer types (like `Box<T>`) to get the pointee shape.
1711/// Returns the original shape if it's not a pointer.
1712fn deref_pointer(shape: &'static Shape) -> &'static Shape {
1713    use facet_core::Def;
1714
1715    match shape.def {
1716        Def::Pointer(pointer_def) => {
1717            if let Some(pointee) = pointer_def.pointee() {
1718                // Recursively dereference in case of nested pointers
1719                deref_pointer(pointee)
1720            } else {
1721                // Opaque pointer - can't dereference
1722                shape
1723            }
1724        }
1725        _ => shape,
1726    }
1727}
1728
1729/// Check if a shape represents a scalar type.
1730/// Transparently handles pointer types like `Box<i32>`.
1731fn is_scalar_shape(shape: &'static Shape) -> bool {
1732    let shape = deref_pointer(shape);
1733    shape.scalar_type().is_some()
1734}
1735
1736/// Returns the arity of a tuple struct/tuple shape, if applicable.
1737/// Transparently handles pointer types like `Box<(i32, i32)>`.
1738fn tuple_struct_arity(shape: &'static Shape) -> Option<usize> {
1739    use facet_core::{StructKind, Type, UserType};
1740
1741    let shape = deref_pointer(shape);
1742    match shape.ty {
1743        Type::User(UserType::Struct(struct_type)) => match struct_type.kind {
1744            StructKind::Tuple | StructKind::TupleStruct => Some(struct_type.fields.len()),
1745            _ => None,
1746        },
1747        _ => None,
1748    }
1749}
1750
1751/// Returns true if the shape is a named struct (non-tuple).
1752/// Transparently handles pointer types like `Box<MyStruct>`.
1753fn is_named_struct_shape(shape: &'static Shape) -> bool {
1754    use facet_core::{StructKind, Type, UserType};
1755
1756    let shape = deref_pointer(shape);
1757    matches!(
1758        shape.ty,
1759        Type::User(UserType::Struct(struct_type)) if matches!(struct_type.kind, StructKind::Struct)
1760    )
1761}
1762
1763/// Returns true if the shape is a sequence type (List, Array, Slice, Set).
1764/// These types serialize as arrays/sequences in formats like TOML, JSON, YAML.
1765/// Transparently handles pointer types like `Box<Vec<i32>>`.
1766fn is_sequence_shape(shape: &'static Shape) -> bool {
1767    use facet_core::Def;
1768
1769    let shape = deref_pointer(shape);
1770    matches!(
1771        shape.def,
1772        Def::List(_) | Def::Array(_) | Def::Slice(_) | Def::Set(_)
1773    )
1774}
1775
1776/// Check if a shape represents a map type (HashMap, BTreeMap, IndexMap, etc.)
1777fn is_map_shape(shape: &'static Shape) -> bool {
1778    use facet_core::Def;
1779
1780    let shape = deref_pointer(shape);
1781    matches!(shape.def, Def::Map(_))
1782}
1783
1784/// Returns true if the shape is a unit-only enum.
1785/// Unit-only enums serialize as strings in most formats (TOML, JSON, YAML).
1786/// Transparently handles pointer types like `Box<UnitEnum>`.
1787fn is_unit_enum_shape(shape: &'static Shape) -> bool {
1788    use facet_core::{Type, UserType};
1789
1790    let shape = deref_pointer(shape);
1791    match shape.ty {
1792        Type::User(UserType::Enum(enum_type)) => {
1793            // Check if all variants are unit variants
1794            enum_type.variants.iter().all(|v| v.data.fields.is_empty())
1795        }
1796        _ => false,
1797    }
1798}
1799
1800/// Information about variants grouped by their expected format.
1801///
1802/// Used by deserializers to efficiently dispatch untagged enum parsing
1803/// based on the type of value encountered.
1804#[derive(Debug, Default)]
1805pub struct VariantsByFormat {
1806    /// Variants that expect a scalar value (newtype wrapping String, i32, etc.)
1807    ///
1808    /// **Deprecated:** Use the type-specific fields below for better type matching.
1809    /// This field contains all scalar variants regardless of type.
1810    pub scalar_variants: Vec<(&'static Variant, &'static Shape)>,
1811
1812    /// Variants that expect a boolean value (newtype wrapping bool)
1813    pub bool_variants: Vec<(&'static Variant, &'static Shape)>,
1814
1815    /// Variants that expect an integer value (newtype wrapping i8, u8, i32, u64, etc.)
1816    pub int_variants: Vec<(&'static Variant, &'static Shape)>,
1817
1818    /// Variants that expect a float value (newtype wrapping f32, f64)
1819    pub float_variants: Vec<(&'static Variant, &'static Shape)>,
1820
1821    /// Variants that expect a string value (newtype wrapping String, `&str`, `Cow<str>`)
1822    pub string_variants: Vec<(&'static Variant, &'static Shape)>,
1823
1824    /// Variants that expect a sequence (tuple variants)
1825    /// Grouped by arity for efficient matching.
1826    pub tuple_variants: Vec<(&'static Variant, usize)>,
1827
1828    /// Variants that expect a mapping (struct variants, newtype wrapping struct)
1829    pub struct_variants: Vec<&'static Variant>,
1830
1831    /// Unit variants (no data)
1832    pub unit_variants: Vec<&'static Variant>,
1833
1834    /// Other variants that don't fit the above categories
1835    pub other_variants: Vec<&'static Variant>,
1836}
1837
1838impl VariantsByFormat {
1839    /// Build variant classification for an enum shape.
1840    ///
1841    /// Returns None if the shape is not an enum.
1842    pub fn from_shape(shape: &'static Shape) -> Option<Self> {
1843        use facet_core::{Type, UserType};
1844
1845        let enum_type = match shape.ty {
1846            Type::User(UserType::Enum(e)) => e,
1847            _ => return None,
1848        };
1849
1850        let mut result = Self::default();
1851
1852        for variant in enum_type.variants {
1853            match VariantFormat::from_variant(variant) {
1854                VariantFormat::Unit => {
1855                    result.unit_variants.push(variant);
1856                }
1857                VariantFormat::NewtypeScalar { inner_shape } => {
1858                    // Add to general scalar_variants (for backward compatibility)
1859                    result.scalar_variants.push((variant, inner_shape));
1860
1861                    // Classify by specific scalar type for better type matching
1862                    // Dereference through pointers (Box, &, etc.) to get the actual scalar type
1863                    use facet_core::ScalarType;
1864                    let scalar_shape = deref_pointer(inner_shape);
1865                    match scalar_shape.scalar_type() {
1866                        Some(ScalarType::Bool) => {
1867                            result.bool_variants.push((variant, inner_shape));
1868                        }
1869                        Some(
1870                            ScalarType::U8
1871                            | ScalarType::U16
1872                            | ScalarType::U32
1873                            | ScalarType::U64
1874                            | ScalarType::U128
1875                            | ScalarType::USize
1876                            | ScalarType::I8
1877                            | ScalarType::I16
1878                            | ScalarType::I32
1879                            | ScalarType::I64
1880                            | ScalarType::I128
1881                            | ScalarType::ISize,
1882                        ) => {
1883                            result.int_variants.push((variant, inner_shape));
1884                        }
1885                        Some(ScalarType::F32 | ScalarType::F64) => {
1886                            result.float_variants.push((variant, inner_shape));
1887                        }
1888                        #[cfg(feature = "alloc")]
1889                        Some(ScalarType::String | ScalarType::CowStr) => {
1890                            result.string_variants.push((variant, inner_shape));
1891                        }
1892                        Some(ScalarType::Str | ScalarType::Char) => {
1893                            result.string_variants.push((variant, inner_shape));
1894                        }
1895                        _ => {
1896                            // Other scalar types (Unit, SocketAddr, IpAddr, etc.) - leave in general scalar_variants only
1897                        }
1898                    }
1899                }
1900                VariantFormat::NewtypeStruct { .. } => {
1901                    result.struct_variants.push(variant);
1902                }
1903                VariantFormat::NewtypeTuple { arity, .. } => {
1904                    result.tuple_variants.push((variant, arity));
1905                }
1906                VariantFormat::NewtypeSequence { .. } => {
1907                    // Sequences like Vec<T> are variable-length, so we use arity 0
1908                    // to indicate "accepts any array" (not an exact match requirement)
1909                    result.tuple_variants.push((variant, 0));
1910                }
1911                VariantFormat::NewtypeOther { .. } => {
1912                    result.other_variants.push(variant);
1913                }
1914                VariantFormat::Tuple { arity } => {
1915                    result.tuple_variants.push((variant, arity));
1916                }
1917                VariantFormat::Struct => {
1918                    result.struct_variants.push(variant);
1919                }
1920            }
1921        }
1922
1923        Some(result)
1924    }
1925
1926    /// Get tuple variants with a specific arity.
1927    pub fn tuple_variants_with_arity(&self, arity: usize) -> Vec<&'static Variant> {
1928        self.tuple_variants
1929            .iter()
1930            .filter(|(_, a)| *a == arity)
1931            .map(|(v, _)| *v)
1932            .collect()
1933    }
1934
1935    /// Check if there are any scalar-expecting variants.
1936    pub const fn has_scalar_variants(&self) -> bool {
1937        !self.scalar_variants.is_empty()
1938    }
1939
1940    /// Check if there are any tuple-expecting variants.
1941    pub const fn has_tuple_variants(&self) -> bool {
1942        !self.tuple_variants.is_empty()
1943    }
1944
1945    /// Check if there are any struct-expecting variants.
1946    pub const fn has_struct_variants(&self) -> bool {
1947        !self.struct_variants.is_empty()
1948    }
1949}
1950
1951// ============================================================================
1952// Schema Builder
1953// ============================================================================
1954
1955/// How enum variants are represented in the serialized format.
1956#[derive(Debug, Clone, PartialEq, Eq, Default)]
1957pub enum EnumRepr {
1958    /// Variant fields are flattened to the same level as other fields.
1959    /// Also used for `#[facet(untagged)]` enums where there's no tag at all.
1960    /// Used by formats like TOML where all fields appear at one level.
1961    /// Example: `{"name": "...", "host": "...", "port": 8080}`
1962    #[default]
1963    Flattened,
1964
1965    /// Variant name is a key, variant content is nested under it.
1966    /// This is the default serde representation for enums.
1967    /// Example: `{"name": "...", "Tcp": {"host": "...", "port": 8080}}`
1968    ExternallyTagged,
1969
1970    /// Tag field is inside the content, alongside variant fields.
1971    /// Used with `#[facet(tag = "type")]`.
1972    /// Example: `{"type": "Tcp", "host": "...", "port": 8080}`
1973    InternallyTagged {
1974        /// The name of the tag field (e.g., "type")
1975        tag: &'static str,
1976    },
1977
1978    /// Tag and content are adjacent fields at the same level.
1979    /// Used with `#[facet(tag = "t", content = "c")]`.
1980    /// Example: `{"t": "Tcp", "c": {"host": "...", "port": 8080}}`
1981    AdjacentlyTagged {
1982        /// The name of the tag field (e.g., "t")
1983        tag: &'static str,
1984        /// The name of the content field (e.g., "c")
1985        content: &'static str,
1986    },
1987}
1988
1989impl EnumRepr {
1990    /// Detect the enum representation from a Shape's attributes.
1991    ///
1992    /// Returns:
1993    /// - `Flattened` if `#[facet(untagged)]`
1994    /// - `InternallyTagged` if `#[facet(tag = "...")]` without content
1995    /// - `AdjacentlyTagged` if both `#[facet(tag = "...", content = "...")]`
1996    /// - `ExternallyTagged` if no attributes (the default enum representation)
1997    pub const fn from_shape(shape: &'static Shape) -> Self {
1998        let tag = shape.get_tag_attr();
1999        let content = shape.get_content_attr();
2000        let untagged = shape.is_untagged();
2001
2002        match (tag, content, untagged) {
2003            // Untagged explicitly requested
2004            (_, _, true) => EnumRepr::Flattened,
2005            // Both tag and content specified → adjacently tagged
2006            (Some(t), Some(c), false) => EnumRepr::AdjacentlyTagged { tag: t, content: c },
2007            // Only tag specified → internally tagged
2008            (Some(t), None, false) => EnumRepr::InternallyTagged { tag: t },
2009            // No attributes → default to externally tagged (variant name as key)
2010            (None, None, false) => EnumRepr::ExternallyTagged,
2011            // Content without tag is invalid, treat as externally tagged
2012            (None, Some(_), false) => EnumRepr::ExternallyTagged,
2013        }
2014    }
2015}
2016
2017impl Schema {
2018    /// Build a schema for the given shape with flattened enum representation.
2019    ///
2020    /// Returns an error if the type definition contains conflicts, such as
2021    /// duplicate field names from parent and flattened structs.
2022    ///
2023    /// Note: This defaults to `Flattened` representation. For auto-detection
2024    /// based on `#[facet(tag = "...")]` attributes, use [`Schema::build_auto`].
2025    pub fn build(shape: &'static Shape) -> Result<Self, SchemaError> {
2026        Self::build_with_repr(shape, EnumRepr::Flattened)
2027    }
2028
2029    /// Build a schema with auto-detected enum representation based on each enum's attributes.
2030    ///
2031    /// This inspects each flattened enum's shape attributes to determine its representation:
2032    /// - `#[facet(untagged)]` → Flattened
2033    /// - `#[facet(tag = "type")]` → InternallyTagged
2034    /// - `#[facet(tag = "t", content = "c")]` → AdjacentlyTagged
2035    /// - No attributes → Flattened (for flatten solver behavior)
2036    ///
2037    /// For externally-tagged enums (variant name as key), use [`Schema::build_externally_tagged`].
2038    pub fn build_auto(shape: &'static Shape) -> Result<Self, SchemaError> {
2039        let builder = SchemaBuilder::new(shape, EnumRepr::Flattened).with_auto_detect();
2040        builder.into_schema()
2041    }
2042
2043    /// Build a schema for externally-tagged enum representation (e.g., JSON).
2044    ///
2045    /// In this representation, the variant name appears as a key and the
2046    /// variant's content is nested under it. The solver will only expect
2047    /// to see the variant name as a top-level key, not the variant's fields.
2048    pub fn build_externally_tagged(shape: &'static Shape) -> Result<Self, SchemaError> {
2049        Self::build_with_repr(shape, EnumRepr::ExternallyTagged)
2050    }
2051
2052    /// Build a schema with the specified enum representation.
2053    pub fn build_with_repr(shape: &'static Shape, repr: EnumRepr) -> Result<Self, SchemaError> {
2054        let builder = SchemaBuilder::new(shape, repr);
2055        builder.into_schema()
2056    }
2057
2058    /// Get the resolutions for this schema.
2059    pub fn resolutions(&self) -> &[Resolution] {
2060        &self.resolutions
2061    }
2062
2063    /// Get the format this schema was built for.
2064    pub const fn format(&self) -> Format {
2065        self.format
2066    }
2067
2068    /// Build a schema for DOM format (XML, HTML) with auto-detected enum representation.
2069    ///
2070    /// In DOM format, fields are categorized as attributes, elements, or text content.
2071    /// The solver uses `see_attribute()`, `see_element()`, etc. to report fields.
2072    pub fn build_dom(shape: &'static Shape) -> Result<Self, SchemaError> {
2073        let builder = SchemaBuilder::new(shape, EnumRepr::Flattened)
2074            .with_auto_detect()
2075            .with_format(Format::Dom);
2076        builder.into_schema()
2077    }
2078
2079    /// Build a schema with a specific format.
2080    pub fn build_with_format(shape: &'static Shape, format: Format) -> Result<Self, SchemaError> {
2081        let builder = SchemaBuilder::new(shape, EnumRepr::Flattened)
2082            .with_auto_detect()
2083            .with_format(format);
2084        builder.into_schema()
2085    }
2086}
2087
2088struct SchemaBuilder {
2089    shape: &'static Shape,
2090    enum_repr: EnumRepr,
2091    /// If true, detect enum representation from each enum's shape attributes.
2092    /// If false, use `enum_repr` for all enums.
2093    auto_detect_enum_repr: bool,
2094    /// The format to build the schema for.
2095    format: Format,
2096}
2097
2098impl SchemaBuilder {
2099    const fn new(shape: &'static Shape, enum_repr: EnumRepr) -> Self {
2100        Self {
2101            shape,
2102            enum_repr,
2103            auto_detect_enum_repr: false,
2104            format: Format::Flat,
2105        }
2106    }
2107
2108    const fn with_auto_detect(mut self) -> Self {
2109        self.auto_detect_enum_repr = true;
2110        self
2111    }
2112
2113    const fn with_format(mut self, format: Format) -> Self {
2114        self.format = format;
2115        self
2116    }
2117
2118    fn analyze(&self) -> Result<Vec<Resolution>, SchemaError> {
2119        self.analyze_shape(self.shape, FieldPath::empty(), Vec::new())
2120    }
2121
2122    /// Analyze a shape and return all possible resolutions.
2123    /// Returns a Vec because enums create multiple resolutions.
2124    ///
2125    /// - `current_path`: The internal field path (for FieldInfo)
2126    /// - `key_prefix`: The serialized key path prefix (for known_paths)
2127    fn analyze_shape(
2128        &self,
2129        shape: &'static Shape,
2130        current_path: FieldPath,
2131        key_prefix: KeyPath,
2132    ) -> Result<Vec<Resolution>, SchemaError> {
2133        match shape.ty {
2134            Type::User(UserType::Struct(struct_type)) => {
2135                self.analyze_struct(struct_type, current_path, key_prefix)
2136            }
2137            Type::User(UserType::Enum(enum_type)) => {
2138                // Enum at root level: create one configuration per variant
2139                self.analyze_enum(shape, enum_type, current_path, key_prefix)
2140            }
2141            _ => {
2142                // For non-struct types at root level, return single empty config
2143                Ok(vec![Resolution::new()])
2144            }
2145        }
2146    }
2147
2148    /// Analyze an enum and return one configuration per variant.
2149    ///
2150    /// - `current_path`: The internal field path (for FieldInfo)
2151    /// - `key_prefix`: The serialized key path prefix (for known_paths)
2152    fn analyze_enum(
2153        &self,
2154        shape: &'static Shape,
2155        enum_type: facet_core::EnumType,
2156        current_path: FieldPath,
2157        key_prefix: KeyPath,
2158    ) -> Result<Vec<Resolution>, SchemaError> {
2159        let enum_name = shape.type_identifier;
2160        let mut result = Vec::new();
2161
2162        for variant in enum_type.variants {
2163            let mut config = Resolution::new();
2164
2165            // Record this variant selection
2166            config.add_variant_selection(current_path.clone(), enum_name, variant.name);
2167
2168            let variant_path = current_path.push_variant("", variant.name);
2169
2170            // Get resolutions from the variant's content
2171            let variant_configs =
2172                self.analyze_variant_content(variant, &variant_path, &key_prefix)?;
2173
2174            // Merge each variant config into the base
2175            for variant_config in variant_configs {
2176                let mut final_config = config.clone();
2177                final_config.merge(&variant_config)?;
2178                result.push(final_config);
2179            }
2180        }
2181
2182        Ok(result)
2183    }
2184
2185    /// Analyze a struct and return all possible resolutions.
2186    ///
2187    /// - `current_path`: The internal field path (for FieldInfo)
2188    /// - `key_prefix`: The serialized key path prefix (for known_paths)
2189    fn analyze_struct(
2190        &self,
2191        struct_type: StructType,
2192        current_path: FieldPath,
2193        key_prefix: KeyPath,
2194    ) -> Result<Vec<Resolution>, SchemaError> {
2195        // Start with one empty configuration
2196        let mut configs = vec![Resolution::new()];
2197
2198        // Process each field, potentially multiplying resolutions
2199        for field in struct_type.fields {
2200            configs =
2201                self.analyze_field_into_configs(field, &current_path, &key_prefix, configs)?;
2202        }
2203
2204        Ok(configs)
2205    }
2206
2207    /// Process a field and return updated resolutions.
2208    /// If the field is a flattened enum, this may multiply the number of configs.
2209    ///
2210    /// - `parent_path`: The internal field path to the parent (for FieldInfo)
2211    /// - `key_prefix`: The serialized key path prefix (for known_paths)
2212    fn analyze_field_into_configs(
2213        &self,
2214        field: &'static Field,
2215        parent_path: &FieldPath,
2216        key_prefix: &KeyPath,
2217        mut configs: Vec<Resolution>,
2218    ) -> Result<Vec<Resolution>, SchemaError> {
2219        let is_flatten = field.is_flattened();
2220
2221        if is_flatten {
2222            // Flattened: inner keys bubble up to current level (same key_prefix)
2223            self.analyze_flattened_field_into_configs(field, parent_path, key_prefix, configs)
2224        } else {
2225            // Regular field: add to ALL current configs
2226            let field_path = parent_path.push_field(field.name);
2227            let required = !field.has_default() && !is_option_type(field.shape());
2228
2229            // Build the key path for this field (uses effective_name for wire format)
2230            let mut field_key_path = key_prefix.clone();
2231            field_key_path.push(field.effective_name());
2232
2233            let field_info = FieldInfo {
2234                serialized_name: field.effective_name(),
2235                path: field_path,
2236                required,
2237                value_shape: field.shape(),
2238                field,
2239                category: if self.format == Format::Dom {
2240                    FieldCategory::from_field_dom(field).unwrap_or(FieldCategory::Element)
2241                } else {
2242                    FieldCategory::Flat
2243                },
2244            };
2245
2246            for config in &mut configs {
2247                config.add_field(field_info.clone())?;
2248                // Add this field's key path
2249                config.add_key_path(field_key_path.clone());
2250            }
2251
2252            // If the field's value is a struct, recurse to collect nested key paths
2253            // (for probing, not for flattening - these are nested in serialized format)
2254            // This may fork resolutions if the nested struct contains flattened enums!
2255            configs =
2256                self.collect_nested_key_paths_for_shape(field.shape(), &field_key_path, configs)?;
2257
2258            Ok(configs)
2259        }
2260    }
2261
2262    /// Collect nested key paths from a shape into resolutions.
2263    /// This handles the case where a non-flattened field contains a struct with flattened enums.
2264    /// Returns updated resolutions (may fork if flattened enums are encountered).
2265    fn collect_nested_key_paths_for_shape(
2266        &self,
2267        shape: &'static Shape,
2268        key_prefix: &KeyPath,
2269        configs: Vec<Resolution>,
2270    ) -> Result<Vec<Resolution>, SchemaError> {
2271        match shape.ty {
2272            Type::User(UserType::Struct(struct_type)) => {
2273                self.collect_nested_key_paths_for_struct(struct_type, key_prefix, configs)
2274            }
2275            _ => Ok(configs),
2276        }
2277    }
2278
2279    /// Collect nested key paths from a struct, potentially forking for flattened enums.
2280    fn collect_nested_key_paths_for_struct(
2281        &self,
2282        struct_type: StructType,
2283        key_prefix: &KeyPath,
2284        mut configs: Vec<Resolution>,
2285    ) -> Result<Vec<Resolution>, SchemaError> {
2286        for field in struct_type.fields {
2287            let is_flatten = field.is_flattened();
2288            let mut field_key_path = key_prefix.clone();
2289
2290            if is_flatten {
2291                // Flattened field: keys bubble up to current level, may fork configs
2292                configs =
2293                    self.collect_nested_key_paths_for_flattened(field, key_prefix, configs)?;
2294            } else {
2295                // Regular field: add key path and recurse
2296                field_key_path.push(field.effective_name());
2297
2298                for config in &mut configs {
2299                    config.add_key_path(field_key_path.clone());
2300                }
2301
2302                // Recurse into nested structs
2303                configs = self.collect_nested_key_paths_for_shape(
2304                    field.shape(),
2305                    &field_key_path,
2306                    configs,
2307                )?;
2308            }
2309        }
2310        Ok(configs)
2311    }
2312
2313    /// Handle flattened fields when collecting nested key paths.
2314    /// This may fork resolutions for flattened enums.
2315    fn collect_nested_key_paths_for_flattened(
2316        &self,
2317        field: &'static Field,
2318        key_prefix: &KeyPath,
2319        configs: Vec<Resolution>,
2320    ) -> Result<Vec<Resolution>, SchemaError> {
2321        let shape = field.shape();
2322
2323        match shape.ty {
2324            Type::User(UserType::Struct(struct_type)) => {
2325                // Flattened struct: recurse with same key_prefix
2326                self.collect_nested_key_paths_for_struct(struct_type, key_prefix, configs)
2327            }
2328            Type::User(UserType::Enum(enum_type)) => {
2329                // Flattened enum: fork resolutions
2330                // We need to match each config to its corresponding variant
2331                let mut result = Vec::new();
2332
2333                for config in configs {
2334                    // Find which variant this config has selected for this field
2335                    let selected_variant = config
2336                        .variant_selections()
2337                        .iter()
2338                        .find(|vs| {
2339                            // Match by the field name in the path
2340                            vs.path.segments().last() == Some(&PathSegment::Field(field.name))
2341                        })
2342                        .map(|vs| vs.variant_name);
2343
2344                    if let Some(variant_name) = selected_variant {
2345                        // Find the variant and collect its key paths
2346                        if let Some(variant) =
2347                            enum_type.variants.iter().find(|v| v.name == variant_name)
2348                        {
2349                            let mut updated_config = config;
2350                            updated_config = self.collect_variant_key_paths(
2351                                variant,
2352                                key_prefix,
2353                                updated_config,
2354                            )?;
2355                            result.push(updated_config);
2356                        } else {
2357                            result.push(config);
2358                        }
2359                    } else {
2360                        result.push(config);
2361                    }
2362                }
2363                Ok(result)
2364            }
2365            _ => Ok(configs),
2366        }
2367    }
2368
2369    /// Collect key paths from an enum variant's content.
2370    fn collect_variant_key_paths(
2371        &self,
2372        variant: &'static Variant,
2373        key_prefix: &KeyPath,
2374        mut config: Resolution,
2375    ) -> Result<Resolution, SchemaError> {
2376        // Check if this is a newtype variant (single unnamed field)
2377        if variant.data.fields.len() == 1 && variant.data.fields[0].name == "0" {
2378            let inner_field = &variant.data.fields[0];
2379            let inner_shape = inner_field.shape();
2380
2381            // If the inner type is a struct, flatten its fields
2382            if let Type::User(UserType::Struct(inner_struct)) = inner_shape.ty {
2383                let configs = self.collect_nested_key_paths_for_struct(
2384                    inner_struct,
2385                    key_prefix,
2386                    vec![config],
2387                )?;
2388                return Ok(configs.into_iter().next().unwrap_or_else(Resolution::new));
2389            }
2390        }
2391
2392        // Named fields - process each
2393        for variant_field in variant.data.fields {
2394            let is_flatten = variant_field.is_flattened();
2395
2396            if is_flatten {
2397                let configs = self.collect_nested_key_paths_for_flattened(
2398                    variant_field,
2399                    key_prefix,
2400                    vec![config],
2401                )?;
2402                config = configs.into_iter().next().unwrap_or_else(Resolution::new);
2403            } else {
2404                let mut field_key_path = key_prefix.clone();
2405                field_key_path.push(variant_field.effective_name());
2406                config.add_key_path(field_key_path.clone());
2407
2408                let configs = self.collect_nested_key_paths_for_shape(
2409                    variant_field.shape(),
2410                    &field_key_path,
2411                    vec![config],
2412                )?;
2413                config = configs.into_iter().next().unwrap_or_else(Resolution::new);
2414            }
2415        }
2416        Ok(config)
2417    }
2418
2419    /// Collect ONLY key paths from a variant's content (no fields added).
2420    /// Used for externally-tagged enums where variant content is nested and
2421    /// will be parsed separately by the deserializer.
2422    fn collect_variant_key_paths_only(
2423        &self,
2424        variant: &'static Variant,
2425        key_prefix: &KeyPath,
2426        config: &mut Resolution,
2427    ) -> Result<(), SchemaError> {
2428        Self::collect_variant_fields_key_paths_only(variant, key_prefix, config);
2429        Ok(())
2430    }
2431
2432    /// Recursively collect key paths from a struct (no fields added).
2433    fn collect_struct_key_paths_only(
2434        struct_type: StructType,
2435        key_prefix: &KeyPath,
2436        config: &mut Resolution,
2437    ) {
2438        for field in struct_type.fields {
2439            let is_flatten = field.is_flattened();
2440
2441            if is_flatten {
2442                // Flattened field: keys bubble up to current level
2443                Self::collect_shape_key_paths_only(field.shape(), key_prefix, config);
2444            } else {
2445                // Regular field: add its key path
2446                let mut field_key_path = key_prefix.clone();
2447                field_key_path.push(field.effective_name());
2448                config.add_key_path(field_key_path.clone());
2449
2450                // Recurse into nested types
2451                Self::collect_shape_key_paths_only(field.shape(), &field_key_path, config);
2452            }
2453        }
2454    }
2455
2456    /// Recursively collect key paths from a shape (struct or enum).
2457    fn collect_shape_key_paths_only(
2458        shape: &'static Shape,
2459        key_prefix: &KeyPath,
2460        config: &mut Resolution,
2461    ) {
2462        match shape.ty {
2463            Type::User(UserType::Struct(inner_struct)) => {
2464                Self::collect_struct_key_paths_only(inner_struct, key_prefix, config);
2465            }
2466            Type::User(UserType::Enum(enum_type)) => {
2467                // For enums, collect key paths from ALL variants
2468                // (we don't know which variant will be selected)
2469                for variant in enum_type.variants {
2470                    Self::collect_variant_fields_key_paths_only(variant, key_prefix, config);
2471                }
2472            }
2473            _ => {}
2474        }
2475    }
2476
2477    /// Collect key paths from a variant's fields (not the variant itself).
2478    fn collect_variant_fields_key_paths_only(
2479        variant: &'static Variant,
2480        key_prefix: &KeyPath,
2481        config: &mut Resolution,
2482    ) {
2483        // Check if this is a newtype variant (single unnamed field)
2484        if variant.data.fields.len() == 1 && variant.data.fields[0].name == "0" {
2485            let inner_field = &variant.data.fields[0];
2486            Self::collect_shape_key_paths_only(inner_field.shape(), key_prefix, config);
2487            return;
2488        }
2489
2490        // Named fields - add key paths for each
2491        for variant_field in variant.data.fields {
2492            let mut field_key_path = key_prefix.clone();
2493            field_key_path.push(variant_field.effective_name());
2494            config.add_key_path(field_key_path.clone());
2495
2496            // Recurse into nested types
2497            Self::collect_shape_key_paths_only(variant_field.shape(), &field_key_path, config);
2498        }
2499    }
2500
2501    /// Process a flattened field, potentially forking resolutions for enums.
2502    ///
2503    /// For flattened fields, the inner keys bubble up to the current level,
2504    /// so we pass the same key_prefix (not key_prefix + field.name).
2505    ///
2506    /// If the field is `Option<T>`, we unwrap to get T and mark all resulting
2507    /// fields as optional (since the entire flattened block can be omitted).
2508    fn analyze_flattened_field_into_configs(
2509        &self,
2510        field: &'static Field,
2511        parent_path: &FieldPath,
2512        key_prefix: &KeyPath,
2513        configs: Vec<Resolution>,
2514    ) -> Result<Vec<Resolution>, SchemaError> {
2515        let field_path = parent_path.push_field(field.name);
2516        let original_shape = field.shape();
2517
2518        // Check if this is Option<T> - if so, unwrap and mark all fields optional
2519        let (shape, is_optional_flatten) = match unwrap_option_type(original_shape) {
2520            Some(inner) => (inner, true),
2521            None => (original_shape, false),
2522        };
2523
2524        match shape.ty {
2525            Type::User(UserType::Struct(struct_type)) => {
2526                // Flatten a struct: get its resolutions and merge into each of ours
2527                // Key prefix stays the same - inner keys bubble up
2528                let mut struct_configs =
2529                    self.analyze_struct(struct_type, field_path, key_prefix.clone())?;
2530
2531                // If the flatten field was Option<T>, mark all inner fields as optional
2532                if is_optional_flatten {
2533                    for config in &mut struct_configs {
2534                        config.mark_all_optional();
2535                    }
2536                }
2537
2538                // Each of our configs combines with each struct config
2539                // (usually struct_configs has 1 element unless it contains enums)
2540                let mut result = Vec::new();
2541                for base_config in configs {
2542                    for struct_config in &struct_configs {
2543                        let mut merged = base_config.clone();
2544                        merged.merge(struct_config)?;
2545                        result.push(merged);
2546                    }
2547                }
2548                Ok(result)
2549            }
2550            Type::User(UserType::Enum(enum_type)) => {
2551                // Fork: each existing config × each variant
2552                let mut result = Vec::new();
2553                let enum_name = shape.type_identifier;
2554
2555                // Determine enum representation:
2556                // - If auto_detect_enum_repr is enabled, detect from the enum's shape attributes
2557                // - Otherwise, use the global enum_repr setting
2558                let enum_repr = if self.auto_detect_enum_repr {
2559                    EnumRepr::from_shape(shape)
2560                } else {
2561                    self.enum_repr.clone()
2562                };
2563
2564                for base_config in configs {
2565                    for variant in enum_type.variants {
2566                        let mut forked = base_config.clone();
2567                        forked.add_variant_selection(field_path.clone(), enum_name, variant.name);
2568
2569                        let variant_path = field_path.push_variant(field.name, variant.name);
2570
2571                        match &enum_repr {
2572                            EnumRepr::ExternallyTagged => {
2573                                // For externally tagged enums, the variant name is a key
2574                                // at the current level, and its content is nested underneath.
2575                                let mut variant_key_prefix = key_prefix.clone();
2576                                variant_key_prefix.push(variant.name);
2577
2578                                // Add the variant name itself as a known key path
2579                                forked.add_key_path(variant_key_prefix.clone());
2580
2581                                // Add the variant name as a field (the key that selects this variant)
2582                                let variant_field_info = FieldInfo {
2583                                    serialized_name: variant.name,
2584                                    path: variant_path.clone(),
2585                                    required: !is_optional_flatten,
2586                                    value_shape: shape, // The enum shape
2587                                    field,              // The original flatten field
2588                                    category: FieldCategory::Element, // Variant selector is like an element
2589                                };
2590                                forked.add_field(variant_field_info)?;
2591
2592                                // For externally-tagged enums, we do NOT add the variant's
2593                                // inner fields to required fields. They're nested and will
2594                                // be parsed separately by the deserializer.
2595                                // Only add them to known_paths for depth-aware probing.
2596                                self.collect_variant_key_paths_only(
2597                                    variant,
2598                                    &variant_key_prefix,
2599                                    &mut forked,
2600                                )?;
2601
2602                                result.push(forked);
2603                            }
2604                            EnumRepr::Flattened => {
2605                                // For flattened/untagged enums, the variant's fields appear at the
2606                                // same level as other fields. The variant name is NOT a key;
2607                                // only the variant's inner fields are keys.
2608
2609                                // Get resolutions from the variant's content
2610                                // Key prefix stays the same - inner keys bubble up
2611                                let mut variant_configs = self.analyze_variant_content(
2612                                    variant,
2613                                    &variant_path,
2614                                    key_prefix,
2615                                )?;
2616
2617                                // If the flatten field was Option<T>, mark all inner fields as optional
2618                                if is_optional_flatten {
2619                                    for config in &mut variant_configs {
2620                                        config.mark_all_optional();
2621                                    }
2622                                }
2623
2624                                // Merge each variant config into the forked base
2625                                for variant_config in variant_configs {
2626                                    let mut final_config = forked.clone();
2627                                    final_config.merge(&variant_config)?;
2628                                    result.push(final_config);
2629                                }
2630                            }
2631                            EnumRepr::InternallyTagged { tag } => {
2632                                // For internally tagged enums, the tag field appears at the
2633                                // same level as the variant's fields.
2634                                // Example: {"type": "Tcp", "host": "...", "port": 8080}
2635
2636                                // Add the tag field as a known key path
2637                                let mut tag_key_path = key_prefix.clone();
2638                                tag_key_path.push(tag);
2639                                forked.add_key_path(tag_key_path);
2640
2641                                // Add the tag field info - the tag discriminates the variant
2642                                // We use a synthetic field for the tag
2643                                let tag_field_info = FieldInfo {
2644                                    serialized_name: tag,
2645                                    path: variant_path.clone(),
2646                                    required: !is_optional_flatten,
2647                                    value_shape: shape, // The enum shape
2648                                    field,              // The original flatten field
2649                                    category: FieldCategory::Element, // Tag is a key field
2650                                };
2651                                forked.add_field(tag_field_info)?;
2652
2653                                // Get resolutions from the variant's content
2654                                // Key prefix stays the same - inner keys are at the same level
2655                                let mut variant_configs = self.analyze_variant_content(
2656                                    variant,
2657                                    &variant_path,
2658                                    key_prefix,
2659                                )?;
2660
2661                                // If the flatten field was Option<T>, mark all inner fields as optional
2662                                if is_optional_flatten {
2663                                    for config in &mut variant_configs {
2664                                        config.mark_all_optional();
2665                                    }
2666                                }
2667
2668                                // Merge each variant config into the forked base
2669                                for variant_config in variant_configs {
2670                                    let mut final_config = forked.clone();
2671                                    final_config.merge(&variant_config)?;
2672                                    result.push(final_config);
2673                                }
2674                            }
2675                            EnumRepr::AdjacentlyTagged { tag, content } => {
2676                                // For adjacently tagged enums, both tag and content fields
2677                                // appear at the same level. Content contains the variant's fields.
2678                                // Example: {"t": "Tcp", "c": {"host": "...", "port": 8080}}
2679
2680                                // Add the tag field as a known key path
2681                                let mut tag_key_path = key_prefix.clone();
2682                                tag_key_path.push(tag);
2683                                forked.add_key_path(tag_key_path);
2684
2685                                // Add the tag field info
2686                                let tag_field_info = FieldInfo {
2687                                    serialized_name: tag,
2688                                    path: variant_path.clone(),
2689                                    required: !is_optional_flatten,
2690                                    value_shape: shape, // The enum shape
2691                                    field,              // The original flatten field
2692                                    category: FieldCategory::Element, // Tag is a key field
2693                                };
2694                                forked.add_field(tag_field_info)?;
2695
2696                                // Add the content field as a known key path
2697                                let mut content_key_prefix = key_prefix.clone();
2698                                content_key_prefix.push(content);
2699                                forked.add_key_path(content_key_prefix.clone());
2700
2701                                // The variant's fields are nested under the content key
2702                                // Collect key paths for probing
2703                                self.collect_variant_key_paths_only(
2704                                    variant,
2705                                    &content_key_prefix,
2706                                    &mut forked,
2707                                )?;
2708
2709                                result.push(forked);
2710                            }
2711                        }
2712                    }
2713                }
2714                Ok(result)
2715            }
2716            _ => {
2717                // Check if this is a Map type - if so, it becomes a catch-all for unknown fields
2718                if let Def::Map(_) = &shape.def {
2719                    // Any map type can serve as a catch-all. Whether the key type can actually
2720                    // be deserialized from field name strings is the deserializer's problem,
2721                    // not the solver's.
2722                    let field_info = FieldInfo {
2723                        serialized_name: field.effective_name(),
2724                        path: field_path,
2725                        required: false, // Catch-all maps are never required
2726                        value_shape: shape,
2727                        field,
2728                        // For DOM format, determine if this catches attributes or elements
2729                        // based on the field's attributes
2730                        category: if self.format == Format::Dom {
2731                            if field.is_attribute() {
2732                                FieldCategory::Attribute
2733                            } else {
2734                                FieldCategory::Element
2735                            }
2736                        } else {
2737                            FieldCategory::Flat
2738                        },
2739                    };
2740
2741                    let mut result = configs;
2742                    for config in &mut result {
2743                        config.set_catch_all_map(field_info.category, field_info.clone());
2744                    }
2745                    return Ok(result);
2746                }
2747
2748                // Check if this is a DynamicValue type (like facet_value::Value) - also a catch-all
2749                if matches!(&shape.def, Def::DynamicValue(_)) {
2750                    let field_info = FieldInfo {
2751                        serialized_name: field.effective_name(),
2752                        path: field_path,
2753                        required: false, // Catch-all dynamic values are never required
2754                        value_shape: shape,
2755                        field,
2756                        category: if self.format == Format::Dom {
2757                            if field.is_attribute() {
2758                                FieldCategory::Attribute
2759                            } else {
2760                                FieldCategory::Element
2761                            }
2762                        } else {
2763                            FieldCategory::Flat
2764                        },
2765                    };
2766
2767                    let mut result = configs;
2768                    for config in &mut result {
2769                        config.set_catch_all_map(field_info.category, field_info.clone());
2770                    }
2771                    return Ok(result);
2772                }
2773
2774                // Can't flatten other types - treat as regular field
2775                // For Option<T> flatten, also consider optionality from the wrapper
2776                let required =
2777                    !field.has_default() && !is_option_type(shape) && !is_optional_flatten;
2778
2779                // For non-flattenable types, add the field with its key path
2780                let mut field_key_path = key_prefix.clone();
2781                field_key_path.push(field.effective_name());
2782
2783                let field_info = FieldInfo {
2784                    serialized_name: field.effective_name(),
2785                    path: field_path,
2786                    required,
2787                    value_shape: shape,
2788                    field,
2789                    category: if self.format == Format::Dom {
2790                        FieldCategory::from_field_dom(field).unwrap_or(FieldCategory::Element)
2791                    } else {
2792                        FieldCategory::Flat
2793                    },
2794                };
2795
2796                let mut result = configs;
2797                for config in &mut result {
2798                    config.add_field(field_info.clone())?;
2799                    config.add_key_path(field_key_path.clone());
2800                }
2801                Ok(result)
2802            }
2803        }
2804    }
2805
2806    /// Analyze a variant's content and return resolutions.
2807    ///
2808    /// - `variant_path`: The internal field path (for FieldInfo)
2809    /// - `key_prefix`: The serialized key path prefix (for known_paths)
2810    fn analyze_variant_content(
2811        &self,
2812        variant: &'static Variant,
2813        variant_path: &FieldPath,
2814        key_prefix: &KeyPath,
2815    ) -> Result<Vec<Resolution>, SchemaError> {
2816        // Check if this is a newtype variant (single unnamed field like `Foo(Bar)`)
2817        if variant.data.fields.len() == 1 && variant.data.fields[0].name == "0" {
2818            let inner_field = &variant.data.fields[0];
2819            let inner_shape = inner_field.shape();
2820
2821            // If the inner type is a struct, treat the newtype wrapper as transparent.
2822            //
2823            // Previously we pushed a synthetic `"0"` segment onto the path. That made the
2824            // solver think there was an extra field between the variant and the inner
2825            // struct (e.g., `backend.backend::Local.0.cache`). Format-specific flattening does not
2826            // expose that tuple wrapper, so the deserializer would try to open a field
2827            // named `"0"` on the inner struct/enum, causing "no such field" errors when
2828            // navigating paths like `backend::Local.cache`.
2829            //
2830            // Keep the synthetic `"0"` segment so the solver/reflect layer walks through
2831            // the tuple wrapper that Rust generates for newtype variants.
2832
2833            // For untagged enum variant resolution, we need to look at the "effective"
2834            // shape that determines the serialization format. This unwraps:
2835            // 1. Transparent wrappers (shape.inner) - e.g., `Curve64(GCurve<f64, f64>)`
2836            // 2. Proxy types (shape.proxy) - e.g., `GCurve` uses `GCurveProxy` for ser/de
2837            //
2838            // This ensures that `{"x":..., "y":...}` correctly matches `Linear(Curve64)`
2839            // where Curve64 is transparent around GCurve which has a proxy with x,y fields.
2840            let effective_shape = unwrap_to_effective_shape(inner_shape);
2841
2842            if let Type::User(UserType::Struct(inner_struct)) = effective_shape.ty {
2843                let inner_path = variant_path.push_field("0");
2844                return self.analyze_struct(inner_struct, inner_path, key_prefix.clone());
2845            }
2846        }
2847
2848        // Named fields or multiple fields - analyze as a pseudo-struct
2849        let mut configs = vec![Resolution::new()];
2850        for variant_field in variant.data.fields {
2851            configs =
2852                self.analyze_field_into_configs(variant_field, variant_path, key_prefix, configs)?;
2853        }
2854        Ok(configs)
2855    }
2856
2857    fn into_schema(self) -> Result<Schema, SchemaError> {
2858        let resolutions = self.analyze()?;
2859        let num_resolutions = resolutions.len();
2860
2861        // Build inverted index: field_name → bitmask of config indices (for Flat format)
2862        let mut field_to_resolutions: BTreeMap<&'static str, ResolutionSet> = BTreeMap::new();
2863        for (idx, config) in resolutions.iter().enumerate() {
2864            for field_info in config.fields().values() {
2865                field_to_resolutions
2866                    .entry(field_info.serialized_name)
2867                    .or_insert_with(|| ResolutionSet::empty(num_resolutions))
2868                    .insert(idx);
2869            }
2870        }
2871
2872        // Build DOM inverted index: (category, name) → bitmask of config indices
2873        let mut dom_field_to_resolutions: BTreeMap<(FieldCategory, &'static str), ResolutionSet> =
2874            BTreeMap::new();
2875        if self.format == Format::Dom {
2876            for (idx, config) in resolutions.iter().enumerate() {
2877                for field_info in config.fields().values() {
2878                    dom_field_to_resolutions
2879                        .entry((field_info.category, field_info.serialized_name))
2880                        .or_insert_with(|| ResolutionSet::empty(num_resolutions))
2881                        .insert(idx);
2882                }
2883            }
2884        }
2885
2886        Ok(Schema {
2887            shape: self.shape,
2888            format: self.format,
2889            resolutions,
2890            field_to_resolutions,
2891            dom_field_to_resolutions,
2892        })
2893    }
2894}
2895
2896/// Check if a shape represents an Option type.
2897const fn is_option_type(shape: &'static Shape) -> bool {
2898    matches!(shape.def, Def::Option(_))
2899}
2900
2901/// If shape is `Option<T>`, returns `Some(T's shape)`. Otherwise returns `None`.
2902const fn unwrap_option_type(shape: &'static Shape) -> Option<&'static Shape> {
2903    match shape.def {
2904        Def::Option(option_def) => Some(option_def.t),
2905        _ => None,
2906    }
2907}
2908
2909/// Unwrap transparent wrappers and proxies to get the effective shape for field matching.
2910///
2911/// When determining which untagged enum variant matches a set of fields, we need to
2912/// look at the "effective" shape that determines the serialization format:
2913///
2914/// 1. Transparent wrappers (shape.inner): e.g., `Curve64` wraps `GCurve<f64, f64>`
2915///    - The wrapper has no serialization presence; it serializes as its inner type
2916///
2917/// 2. Proxy types (shape.proxy): e.g., `GCurve` uses `GCurveProxy` for ser/de
2918///    - The proxy's fields are what appear in the serialized format
2919///
2920/// This function recursively unwraps these layers to find the shape whose fields
2921/// should be used for variant matching. For example:
2922/// - `Curve64` (transparent) → `GCurve<f64, f64>` (has proxy) → `GCurveProxy<f64, f64>`
2923fn unwrap_to_effective_shape(shape: &'static Shape) -> &'static Shape {
2924    // First, unwrap transparent wrappers
2925    let shape = unwrap_transparent(shape);
2926
2927    // Then, if there's a proxy, use its shape instead
2928    if let Some(proxy_def) = shape.proxy {
2929        // Recursively unwrap in case the proxy is also transparent or has its own proxy
2930        unwrap_to_effective_shape(proxy_def.shape)
2931    } else {
2932        shape
2933    }
2934}
2935
2936/// Recursively unwrap transparent wrappers to get to the innermost type.
2937fn unwrap_transparent(shape: &'static Shape) -> &'static Shape {
2938    if let Some(inner) = shape.inner {
2939        unwrap_transparent(inner)
2940    } else {
2941        shape
2942    }
2943}