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