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