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, ¤t_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}