Skip to main content

eure_document/parse/
union.rs

1//! UnionParser for parsing union types from Eure documents.
2//!
3//! Implements oneOf semantics with priority-based ambiguity resolution.
4
5extern crate alloc;
6
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9
10use crate::data_model::VariantRepr;
11use crate::document::node::NodeValue;
12use crate::document::{EureDocument, NodeId};
13use crate::identifier::Identifier;
14use crate::parse::{DocumentParser, FromEure};
15use crate::value::ObjectKey;
16
17use super::variant_path::VariantPath;
18use super::{
19    AccessedSnapshot, FlattenContext, ParseContext, ParseError, ParseErrorKind, ParserScope,
20    UnionTagMode,
21};
22
23/// The `$variant` extension identifier.
24pub const VARIANT: Identifier = Identifier::new_unchecked("variant");
25
26// =============================================================================
27// Shared variant extraction helpers (used by both parsing and validation)
28// =============================================================================
29
30/// Extract variant name and content node from repr pattern.
31///
32/// Returns:
33/// - `Ok(Some((name, content_node_id)))` - pattern matched
34/// - `Ok(None)` - pattern did not match (not a map, wrong structure, etc.)
35/// - `Err(...)` - tag field exists but has invalid type
36pub fn extract_repr_variant(
37    doc: &EureDocument,
38    node_id: NodeId,
39    repr: &VariantRepr,
40) -> Result<Option<(String, NodeId)>, ParseError> {
41    match repr {
42        VariantRepr::Untagged => Ok(None),
43        VariantRepr::External => Ok(try_extract_external(doc, node_id)),
44        VariantRepr::Internal { tag } => try_extract_internal(doc, node_id, tag),
45        VariantRepr::Adjacent { tag, content } => try_extract_adjacent(doc, node_id, tag, content),
46    }
47}
48
49/// Try to extract External repr: `{ variant_name = content }`
50fn try_extract_external(doc: &EureDocument, node_id: NodeId) -> Option<(String, NodeId)> {
51    let node = doc.node(node_id);
52    let NodeValue::Map(map) = &node.content else {
53        return None;
54    };
55
56    if map.len() != 1 {
57        return None;
58    }
59
60    let (key, &content_node_id) = map.iter().next()?;
61    let ObjectKey::String(variant_name) = key else {
62        return None;
63    };
64    Some((variant_name.clone(), content_node_id))
65}
66
67/// Try to extract Internal repr: `{ type = "variant_name", ...fields... }`
68///
69/// Returns the same node_id as content - the tag field should be excluded during record parsing/validation.
70fn try_extract_internal(
71    doc: &EureDocument,
72    node_id: NodeId,
73    tag: &str,
74) -> Result<Option<(String, NodeId)>, ParseError> {
75    let node = doc.node(node_id);
76    let NodeValue::Map(map) = &node.content else {
77        return Ok(None);
78    };
79
80    let tag_key = ObjectKey::String(tag.to_string());
81    let Some(tag_node_id) = map.get(&tag_key) else {
82        return Ok(None);
83    };
84
85    let variant_name: &str = doc.parse(*tag_node_id)?;
86    Ok(Some((variant_name.to_string(), node_id)))
87}
88
89/// Try to extract Adjacent repr: `{ type = "variant_name", content = {...} }`
90fn try_extract_adjacent(
91    doc: &EureDocument,
92    node_id: NodeId,
93    tag: &str,
94    content: &str,
95) -> Result<Option<(String, NodeId)>, ParseError> {
96    let node = doc.node(node_id);
97    let NodeValue::Map(map) = &node.content else {
98        return Ok(None);
99    };
100
101    let tag_key = ObjectKey::String(tag.to_string());
102    let Some(tag_node_id) = map.get(&tag_key) else {
103        return Ok(None);
104    };
105
106    let variant_name: &str = doc.parse(*tag_node_id)?;
107
108    let content_key = ObjectKey::String(content.to_string());
109    let Some(content_node_id) = map.get(&content_key) else {
110        return Ok(None);
111    };
112
113    Ok(Some((variant_name.to_string(), *content_node_id)))
114}
115
116// =============================================================================
117// UnionParser
118// =============================================================================
119
120/// Helper for parsing union types from Eure documents.
121///
122/// Implements oneOf semantics:
123/// - Exactly one variant must match
124/// - Multiple matches resolved by registration order (priority)
125/// - Short-circuits on first priority variant match
126/// - When `$variant` extension or repr is specified, matches by name directly
127///
128/// # Variant Resolution
129///
130/// Variant is determined by combining `$variant` extension and `VariantRepr`:
131/// - Both agree on same name → use repr's context (with tag excluded for Internal)
132/// - `$variant` only (repr didn't extract) → use original context
133/// - Repr only → use repr's context
134/// - Conflict (different names) → `ConflictingVariantTags` error
135/// - Neither → Untagged parsing (try all variants)
136///
137/// # Example
138///
139/// ```ignore
140/// impl<'doc> FromEure<'doc> for Description {
141///     fn parse(ctx: &ParseContext<'doc>) -> Result<Self, ParseError> {
142///         ctx.parse_union(VariantRepr::default())?
143///             .variant("string", |ctx| {
144///                 let text: String = ctx.parse()?;
145///                 Ok(Description::String(text))
146///             })
147///             .variant("markdown", |ctx| {
148///                 let text: String = ctx.parse()?;
149///                 Ok(Description::Markdown(text))
150///             })
151///             .parse()
152///     }
153/// }
154/// ```
155pub struct UnionParser<'doc, 'ctx, T, E = ParseError> {
156    ctx: &'ctx ParseContext<'doc>,
157    /// Unified variant: (name, context, rest_path)
158    /// - name: variant name to match
159    /// - context: ParseContext for the variant content
160    /// - rest_path: remaining variant path for nested unions
161    variant: Option<(String, ParseContext<'doc>, Option<VariantPath>)>,
162    /// Result when variant matches
163    variant_result: Option<Result<T, E>>,
164    /// First matching priority variant (short-circuit result)
165    priority_result: Option<T>,
166    /// Matching non-priority variants, with their captured accessed state.
167    /// The AccessedSnapshot is captured after successful parse, before restoring.
168    other_results: Vec<(String, T, AccessedSnapshot)>,
169    /// Failed variants (for error reporting)
170    failures: Vec<(String, E)>,
171    /// Flatten context for snapshot/rollback (if flattened parsing).
172    flatten_ctx: Option<FlattenContext>,
173}
174
175impl<'doc, 'ctx, T, E> UnionParser<'doc, 'ctx, T, E>
176where
177    E: From<ParseError>,
178{
179    /// Create a new UnionParser for the given context and repr.
180    ///
181    /// Returns error if:
182    /// - `$variant` extension has invalid type or syntax
183    /// - `$variant` and repr extract conflicting variant names
184    pub(crate) fn new(
185        ctx: &'ctx ParseContext<'doc>,
186        repr: VariantRepr,
187    ) -> Result<Self, ParseError> {
188        let variant = Self::resolve_variant(ctx, &repr)?;
189
190        // Push snapshot for rollback if flatten context exists
191        let flatten_ctx = ctx.flatten_ctx().cloned();
192        if let Some(ref fc) = flatten_ctx {
193            fc.push_snapshot();
194        }
195
196        Ok(Self {
197            ctx,
198            variant,
199            variant_result: None,
200            priority_result: None,
201            other_results: Vec::new(),
202            failures: Vec::new(),
203            flatten_ctx,
204        })
205    }
206
207    /// Resolve the unified variant from `$variant` extension and repr.
208    ///
209    /// Returns:
210    /// - `Some((name, ctx, rest))` if variant is determined
211    /// - `None` for Untagged parsing
212    ///
213    /// The behavior depends on `UnionTagMode`:
214    /// - `Eure`: Use `$variant` extension or untagged matching (ignore repr)
215    /// - `Repr`: Use only repr patterns (ignore `$variant`, no untagged fallback)
216    fn resolve_variant(
217        ctx: &ParseContext<'doc>,
218        repr: &VariantRepr,
219    ) -> Result<Option<(String, ParseContext<'doc>, Option<VariantPath>)>, ParseError> {
220        match ctx.union_tag_mode() {
221            UnionTagMode::Eure => Self::resolve_variant_eure_mode(ctx),
222            UnionTagMode::Repr => Self::resolve_variant_repr_mode(ctx, repr),
223        }
224    }
225
226    /// Resolve variant in Eure mode: `$variant` extension or untagged matching.
227    ///
228    /// In this mode:
229    /// - If `$variant` extension is present, use it to determine the variant
230    /// - Otherwise, use untagged matching (try all variants)
231    /// - `VariantRepr` is ignored
232    fn resolve_variant_eure_mode(
233        ctx: &ParseContext<'doc>,
234    ) -> Result<Option<(String, ParseContext<'doc>, Option<VariantPath>)>, ParseError> {
235        // Check if variant path is already set in context (from parent union)
236        let explicit_variant = match ctx.variant_path() {
237            Some(vp) if !vp.is_empty() => Some(vp.clone()),
238            Some(_) => None, // Empty path = variant consumed, use Untagged
239            None => {
240                let variant = Self::extract_explicit_variant(ctx)?;
241                if variant.is_some() {
242                    // Mark $variant extension as accessed so deny_unknown_extensions() won't fail
243                    ctx.accessed().add_ext(VARIANT.clone());
244                }
245                variant
246            }
247        };
248
249        match explicit_variant {
250            // $variant present → use original context
251            Some(ev) => {
252                let name = ev
253                    .first()
254                    .map(|i| i.as_ref().to_string())
255                    .unwrap_or_default();
256                let rest = ev.rest().unwrap_or_else(VariantPath::empty);
257                Ok(Some((name, ctx.clone(), Some(rest))))
258            }
259            // No $variant → Untagged
260            None => Ok(None),
261        }
262    }
263
264    /// Resolve variant in Repr mode: use only `VariantRepr` patterns.
265    ///
266    /// In this mode:
267    /// - Extract variant tag using `VariantRepr` (External, Internal, Adjacent)
268    /// - `$variant` extension is ignored
269    /// - If repr doesn't extract a tag, return `None` (will result in NoMatchingVariant error)
270    fn resolve_variant_repr_mode(
271        ctx: &ParseContext<'doc>,
272        repr: &VariantRepr,
273    ) -> Result<Option<(String, ParseContext<'doc>, Option<VariantPath>)>, ParseError> {
274        // Extract repr_variant using shared helper
275        let repr_variant = extract_repr_variant(ctx.doc(), ctx.node_id(), repr)?;
276
277        match repr_variant {
278            // Repr extracted a tag → use repr's context
279            Some((name, content_node_id)) => {
280                let content_ctx = Self::make_content_context(ctx, repr, content_node_id);
281                Ok(Some((name, content_ctx, Some(VariantPath::empty()))))
282            }
283            // Repr didn't extract → no tag (will be handled as untagged, but in repr mode
284            // this should result in an error for non-Untagged reprs)
285            None => {
286                // For non-Untagged reprs, the structure doesn't match the expected pattern
287                // Return None to trigger untagged parsing, which will fail if no variant matches
288                Ok(None)
289            }
290        }
291    }
292
293    /// Create ParseContext for variant content based on repr type.
294    fn make_content_context(
295        ctx: &ParseContext<'doc>,
296        repr: &VariantRepr,
297        content_node_id: NodeId,
298    ) -> ParseContext<'doc> {
299        match repr {
300            // Internal repr: mark tag field as accessed in shared context
301            // This way deny_unknown_fields won't complain about the tag
302            VariantRepr::Internal { tag } => {
303                // Get or create flatten context, add tag to accessed fields
304                let flatten_ctx = match ctx.flatten_ctx() {
305                    Some(fc) => {
306                        fc.add_field(tag);
307                        fc.clone()
308                    }
309                    None => {
310                        let fc = super::FlattenContext::new(
311                            super::AccessedSet::new(),
312                            ParserScope::Record,
313                        );
314                        fc.add_field(tag);
315                        fc
316                    }
317                };
318                ParseContext::with_flatten_ctx(
319                    ctx.doc(),
320                    content_node_id,
321                    flatten_ctx,
322                    ctx.union_tag_mode(),
323                )
324            }
325            // Other reprs: just use the content node
326            _ => ctx.at(content_node_id),
327        }
328    }
329
330    /// Extract the `$variant` extension value from the node.
331    fn extract_explicit_variant(
332        ctx: &ParseContext<'doc>,
333    ) -> Result<Option<VariantPath>, ParseError> {
334        let node = ctx.node();
335        let Some(&variant_node_id) = node.extensions.get(&VARIANT) else {
336            return Ok(None);
337        };
338
339        let variant_node = ctx.doc().node(variant_node_id);
340        let s: &str = ctx.doc().parse(variant_node_id).map_err(|_| ParseError {
341            node_id: variant_node_id,
342            kind: ParseErrorKind::InvalidVariantType(
343                variant_node
344                    .content
345                    .value_kind()
346                    .unwrap_or(crate::value::ValueKind::Null),
347            ),
348        })?;
349
350        VariantPath::parse(s).map(Some).map_err(|_| ParseError {
351            node_id: variant_node_id,
352            kind: ParseErrorKind::InvalidVariantPath(s.to_string()),
353        })
354    }
355
356    /// Register a variant with short-circuit semantics (default).
357    ///
358    /// When this variant matches in untagged mode, parsing succeeds immediately
359    /// without checking other variants. Use definition order to express priority.
360    pub fn variant<P: DocumentParser<'doc, Output = T, Error = E>>(
361        mut self,
362        name: &str,
363        f: P,
364    ) -> Self {
365        self.try_variant(name, f, true);
366        self
367    }
368
369    /// Register a variant with short-circuit semantics using FromEure.
370    pub fn parse_variant<V: FromEure<'doc, Error = E>>(
371        mut self,
372        name: &str,
373        mut then: impl FnMut(V) -> Result<T, E>,
374    ) -> Self {
375        self.try_variant(
376            name,
377            move |ctx: &ParseContext<'doc>| {
378                let v = V::parse(ctx)?;
379                then(v)
380            },
381            true,
382        );
383        self
384    }
385
386    /// Register a variant with unambiguous semantics.
387    ///
388    /// All unambiguous variants are tried to detect conflicts.
389    /// If multiple unambiguous variants match, an AmbiguousUnion error is returned.
390    /// Use for catch-all variants or when you need conflict detection.
391    pub fn variant_unambiguous<P: DocumentParser<'doc, Output = T, Error = E>>(
392        mut self,
393        name: &str,
394        f: P,
395    ) -> Self {
396        self.try_variant(name, f, false);
397        self
398    }
399
400    /// Register a variant with unambiguous semantics using FromEure.
401    pub fn parse_variant_unambiguous<V: FromEure<'doc, Error = E>>(
402        mut self,
403        name: &str,
404        mut then: impl FnMut(V) -> Result<T, E>,
405    ) -> Self {
406        self.try_variant(
407            name,
408            move |ctx: &ParseContext<'doc>| {
409                let v = V::parse(ctx)?;
410                then(v)
411            },
412            false,
413        );
414        self
415    }
416
417    /// Internal helper for variant/other logic.
418    fn try_variant<P: DocumentParser<'doc, Output = T, Error = E>>(
419        &mut self,
420        name: &str,
421        mut f: P,
422        is_priority: bool,
423    ) {
424        // 1. If variant is determined, only try matching variant
425        if let Some((ref v_name, ref v_ctx, ref rest)) = self.variant {
426            if v_name == name && self.variant_result.is_none() {
427                let child_ctx = v_ctx.with_variant_rest(rest.clone());
428                let result = f.parse(&child_ctx);
429                // Variant explicitly specified - no rollback needed on failure,
430                // error propagates directly. Changes kept if success.
431                self.variant_result = Some(result);
432            }
433            return;
434        }
435
436        // 2. Untagged mode: try all variants
437
438        // Skip if already have priority result
439        if self.priority_result.is_some() {
440            return;
441        }
442
443        let child_ctx = self.ctx.with_variant_rest(None);
444        match f.parse(&child_ctx) {
445            Ok(value) => {
446                if is_priority {
447                    // Priority variant succeeded - keep the changes
448                    // (snapshot will be popped in parse())
449                    self.priority_result = Some(value);
450                } else {
451                    // Other variant succeeded - capture state before restoring
452                    // We need to try more variants, so restore for next attempt
453                    if let Some(ref fc) = self.flatten_ctx {
454                        let captured = fc.capture_current_state();
455                        fc.restore_to_current_snapshot();
456                        self.other_results.push((name.to_string(), value, captured));
457                    } else {
458                        // No flatten context - no state to capture
459                        self.other_results.push((
460                            name.to_string(),
461                            value,
462                            (Default::default(), Default::default()),
463                        ));
464                    }
465                }
466            }
467            Err(e) => {
468                // Variant failed - restore to snapshot
469                if let Some(ref fc) = self.flatten_ctx {
470                    fc.restore_to_current_snapshot();
471                }
472                self.failures.push((name.to_string(), e));
473            }
474        }
475    }
476
477    /// Execute the union parse with oneOf semantics.
478    pub fn parse(self) -> Result<T, E> {
479        let node_id = self.ctx.node_id();
480
481        // 1. Variant determined - return its result
482        // When variant is explicitly specified via $variant, we don't use snapshot/rollback.
483        // The accessed fields from parsing are kept (success) or don't matter (error propagates).
484        if let Some((v_name, _, _)) = self.variant {
485            let result = self.variant_result.unwrap_or_else(|| {
486                Err(ParseError {
487                    node_id,
488                    kind: ParseErrorKind::UnknownVariant(v_name),
489                }
490                .into())
491            });
492            // Pop the snapshot - if success, keep changes; if error, doesn't matter
493            if let Some(ref fc) = self.flatten_ctx {
494                match &result {
495                    Ok(_) => fc.pop_without_restore(),
496                    Err(_) => fc.pop_and_restore(),
497                }
498            }
499            return result;
500        }
501
502        // 2. Priority result - success, keep changes
503        if let Some(value) = self.priority_result {
504            if let Some(ref fc) = self.flatten_ctx {
505                fc.pop_without_restore();
506            }
507            return Ok(value);
508        }
509
510        // 3. Check other_results
511        match self.other_results.len() {
512            0 => {
513                // No match - rollback and return error
514                if let Some(ref fc) = self.flatten_ctx {
515                    fc.pop_and_restore();
516                }
517                Err(self.no_match_error(node_id))
518            }
519            1 => {
520                // Single match - restore to captured state (from successful variant)
521                let (_, value, captured_state) = self.other_results.into_iter().next().unwrap();
522                if let Some(ref fc) = self.flatten_ctx {
523                    fc.restore_to_state(captured_state);
524                    fc.pop_without_restore();
525                }
526                Ok(value)
527            }
528            _ => {
529                // Ambiguous - rollback all changes
530                if let Some(ref fc) = self.flatten_ctx {
531                    fc.pop_and_restore();
532                }
533                Err(ParseError {
534                    node_id,
535                    kind: ParseErrorKind::AmbiguousUnion(
536                        self.other_results
537                            .into_iter()
538                            .map(|(name, _, _)| name)
539                            .collect(),
540                    ),
541                }
542                .into())
543            }
544        }
545    }
546
547    /// Create an error for when no variant matches.
548    fn no_match_error(self, node_id: crate::document::NodeId) -> E {
549        self.failures
550            .into_iter()
551            .next()
552            .map(|(_, e)| e)
553            .unwrap_or_else(|| {
554                ParseError {
555                    node_id,
556                    kind: ParseErrorKind::NoMatchingVariant { variant: None },
557                }
558                .into()
559            })
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::eure;
567    use crate::parse::AlwaysParser;
568    use crate::parse::DocumentParserExt as _;
569
570    #[derive(Debug, PartialEq, Clone)]
571    enum TestEnum {
572        Foo,
573        Bar,
574    }
575
576    #[test]
577    fn test_union_single_match() {
578        let doc = eure!({ = "foo" });
579        let root_id = doc.get_root_id();
580        let ctx = doc.parse_context(root_id);
581
582        let result: TestEnum = ctx
583            .parse_union(VariantRepr::default())
584            .unwrap()
585            .variant("foo", |ctx: &ParseContext<'_>| {
586                let s: &str = ctx.parse()?;
587                if s == "foo" {
588                    Ok(TestEnum::Foo)
589                } else {
590                    Err(ParseError {
591                        node_id: ctx.node_id(),
592                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
593                    })
594                }
595            })
596            .variant("bar", |ctx: &ParseContext<'_>| {
597                let s: &str = ctx.parse()?;
598                if s == "bar" {
599                    Ok(TestEnum::Bar)
600                } else {
601                    Err(ParseError {
602                        node_id: ctx.node_id(),
603                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
604                    })
605                }
606            })
607            .parse()
608            .unwrap();
609
610        assert_eq!(result, TestEnum::Foo);
611    }
612
613    #[test]
614    fn test_union_priority_short_circuit() {
615        let doc = eure!({ = "value" });
616        let root_id = doc.get_root_id();
617        let ctx = doc.parse_context(root_id);
618
619        // Both variants would match, but first one wins due to priority
620        let result: String = ctx
621            .parse_union(VariantRepr::default())
622            .unwrap()
623            .variant("first", String::parse)
624            .variant("second", String::parse)
625            .parse()
626            .unwrap();
627
628        assert_eq!(result, "value");
629    }
630
631    #[test]
632    fn test_union_no_match() {
633        let doc = eure!({ = "baz" });
634        let root_id = doc.get_root_id();
635        let ctx = doc.parse_context(root_id);
636
637        let result: Result<TestEnum, ParseError> = ctx
638            .parse_union(VariantRepr::default())
639            .unwrap()
640            .variant("foo", |ctx: &ParseContext<'_>| {
641                let s: &str = ctx.parse()?;
642                if s == "foo" {
643                    Ok(TestEnum::Foo)
644                } else {
645                    Err(ParseError {
646                        node_id: ctx.node_id(),
647                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
648                    })
649                }
650            })
651            .parse();
652
653        assert!(result.is_err());
654    }
655
656    // --- $variant extension tests ---
657
658    #[test]
659    fn test_variant_extension_match_success() {
660        // $variant = "baz" specified, matches other("baz")
661        // All parsers always succeed
662        let doc = eure!({ %variant = "baz", = "anything" });
663        let root_id = doc.get_root_id();
664        let ctx = doc.parse_context(root_id);
665
666        let result: TestEnum = ctx
667            .parse_union(VariantRepr::default())
668            .unwrap()
669            .variant(
670                "foo",
671                AlwaysParser::<TestEnum, ParseError>::new(TestEnum::Foo),
672            )
673            .variant_unambiguous("baz", AlwaysParser::new(TestEnum::Bar))
674            .parse()
675            .unwrap();
676
677        assert_eq!(result, TestEnum::Bar);
678    }
679
680    #[test]
681    fn test_variant_extension_unknown() {
682        // $variant = "unknown" specified, but "unknown" is not registered
683        // All parsers always succeed
684        let doc = eure!({ %variant = "unknown", = "anything" });
685        let root_id = doc.get_root_id();
686        let ctx = doc.parse_context(root_id);
687
688        let err: ParseError = ctx
689            .parse_union(VariantRepr::default())
690            .unwrap()
691            .variant("foo", AlwaysParser::new(TestEnum::Foo))
692            .variant_unambiguous("baz", AlwaysParser::new(TestEnum::Bar))
693            .parse()
694            .unwrap_err();
695
696        assert_eq!(err.node_id, root_id);
697        assert_eq!(
698            err.kind,
699            ParseErrorKind::UnknownVariant("unknown".to_string())
700        );
701    }
702
703    #[test]
704    fn test_variant_extension_match_parse_failure() {
705        // $variant = "baz" specified, "baz" parser fails
706        let doc = eure!({ %variant = "baz", = "anything" });
707        let root_id = doc.get_root_id();
708        let ctx = doc.parse_context(root_id);
709
710        let err = ctx
711            .parse_union(VariantRepr::default())
712            .unwrap()
713            .variant("foo", AlwaysParser::new(TestEnum::Foo))
714            .variant_unambiguous("baz", |ctx: &ParseContext<'_>| {
715                Err(ParseError {
716                    node_id: ctx.node_id(),
717                    kind: ParseErrorKind::MissingField("test".to_string()),
718                })
719            })
720            .parse()
721            .unwrap_err();
722
723        // Parser's error is returned directly
724        assert_eq!(err.node_id, root_id);
725        assert_eq!(err.kind, ParseErrorKind::MissingField("test".to_string()));
726    }
727
728    // --- nested variant tests ---
729
730    #[derive(Debug, PartialEq, Clone)]
731    enum Outer {
732        A(Inner),
733        B(i32),
734    }
735
736    #[derive(Debug, PartialEq, Clone)]
737    enum Inner {
738        X,
739        Y,
740    }
741
742    fn parse_inner(ctx: &ParseContext<'_>) -> Result<Inner, ParseError> {
743        ctx.parse_union(VariantRepr::default())
744            .unwrap()
745            .variant("x", AlwaysParser::new(Inner::X))
746            .variant("y", AlwaysParser::new(Inner::Y))
747            .parse()
748    }
749
750    #[test]
751    fn test_variant_nested_single_segment() {
752        // $variant = "a" - matches "a", rest is None -> Inner defaults to X
753        let doc = eure!({ %variant = "a", = "value" });
754        let root_id = doc.get_root_id();
755        let ctx = doc.parse_context(root_id);
756
757        let result: Outer = ctx
758            .parse_union(VariantRepr::default())
759            .unwrap()
760            .variant("a", parse_inner.map(Outer::A))
761            .variant("b", AlwaysParser::new(Outer::B(42)))
762            .parse()
763            .unwrap();
764
765        assert_eq!(result, Outer::A(Inner::X));
766    }
767
768    #[test]
769    fn test_variant_nested_multi_segment() {
770        // $variant = "a.y" - matches "a", rest is Some("y")
771        let doc = eure!({ %variant = "a.y", = "value" });
772        let root_id = doc.get_root_id();
773        let ctx = doc.parse_context(root_id);
774
775        let result: Outer = ctx
776            .parse_union(VariantRepr::default())
777            .unwrap()
778            .variant("a", parse_inner.map(Outer::A))
779            .variant("b", AlwaysParser::new(Outer::B(42)))
780            .parse()
781            .unwrap();
782
783        assert_eq!(result, Outer::A(Inner::Y));
784    }
785
786    #[test]
787    fn test_variant_nested_invalid_inner() {
788        // $variant = "a.z" - matches "a", but "z" is not valid for Inner
789        let doc = eure!({ %variant = "a.z", = "value" });
790        let root_id = doc.get_root_id();
791        let ctx = doc.parse_context(root_id);
792
793        let err = ctx
794            .parse_union(VariantRepr::default())
795            .unwrap()
796            .variant("a", parse_inner.map(Outer::A))
797            .variant("b", AlwaysParser::new(Outer::B(42)))
798            .parse()
799            .unwrap_err();
800
801        assert_eq!(err.kind, ParseErrorKind::UnknownVariant("z".to_string()));
802    }
803
804    #[test]
805    fn test_variant_non_nested_with_nested_path() {
806        // $variant = "b.x" but "b" parser doesn't expect nested path
807        // The child context will have variant_path = Some("x")
808        // If the "b" parser is a non-union type, it should error on unexpected variant path
809        let doc = eure!({ %variant = "b.x", = "value" });
810        let root_id = doc.get_root_id();
811        let ctx = doc.parse_context(root_id);
812
813        // "b" is registered as a variant but if called with "b.x",
814        // the closure gets ctx with variant_path = Some("x")
815        // The simple parser Ok(Outer::B(42)) doesn't check variant path,
816        // but a proper impl would use ctx.parse_primitive() which errors
817        let err = ctx
818            .parse_union(VariantRepr::default())
819            .unwrap()
820            .variant("a", parse_inner.map(Outer::A))
821            .variant("b", |ctx: &ParseContext<'_>| {
822                // Simulate parsing a primitive that checks variant path
823                ctx.parse_primitive()?;
824                Ok(Outer::B(42))
825            })
826            .parse()
827            .unwrap_err();
828
829        // parse_primitive should error because variant path "x" remains
830        assert!(matches!(err.kind, ParseErrorKind::UnexpectedVariantPath(_)));
831    }
832
833    // --- invalid $variant tests ---
834
835    use crate::value::ValueKind;
836
837    #[test]
838    fn test_invalid_variant_type_errors() {
839        // $variant = 123 (integer, not string) - should error at parse_union()
840        // Note: eure! macro can't create invalid $variant types, so we use manual construction
841        use crate::document::node::NodeValue;
842        use crate::value::PrimitiveValue;
843        use num_bigint::BigInt;
844
845        // Create base doc with eure! and then add invalid integer $variant
846        let mut doc = eure!({ = "foo" });
847        let root_id = doc.get_root_id();
848        let variant_node_id = doc
849            .add_extension("variant".parse().unwrap(), root_id)
850            .unwrap()
851            .node_id;
852        doc.node_mut(variant_node_id).content =
853            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(123)));
854
855        let ctx = doc.parse_context(root_id);
856
857        let Err(err) = ctx.parse_union::<TestEnum, ParseError>(VariantRepr::default()) else {
858            panic!("Expected error");
859        };
860        assert_eq!(
861            err,
862            ParseError {
863                node_id: variant_node_id,
864                kind: ParseErrorKind::InvalidVariantType(ValueKind::Integer),
865            }
866        );
867    }
868
869    #[test]
870    fn test_invalid_variant_path_syntax_errors() {
871        // $variant = "foo..bar" (invalid path syntax) - should error at parse_union()
872        let doc = eure!({ %variant = "foo..bar", = "foo" });
873        let root_id = doc.get_root_id();
874        let variant_node_id = *doc.node(root_id).extensions.get(&VARIANT).unwrap();
875        let ctx = doc.parse_context(root_id);
876
877        let Err(err) = ctx.parse_union::<TestEnum, ParseError>(VariantRepr::default()) else {
878            panic!("Expected error");
879        };
880        assert_eq!(
881            err,
882            ParseError {
883                node_id: variant_node_id,
884                kind: ParseErrorKind::InvalidVariantPath("foo..bar".to_string()),
885            }
886        );
887    }
888
889    // --- VariantRepr tests ---
890
891    #[derive(Debug, PartialEq)]
892    enum ReprTestEnum {
893        A { value: i64 },
894        B { name: String },
895    }
896
897    fn parse_repr_test_enum(
898        ctx: &ParseContext<'_>,
899        repr: VariantRepr,
900    ) -> Result<ReprTestEnum, ParseError> {
901        ctx.parse_union(repr)?
902            .variant("a", |ctx: &ParseContext<'_>| {
903                let rec = ctx.parse_record()?;
904                let value: i64 = rec.parse_field("value")?;
905                rec.deny_unknown_fields()?;
906                Ok(ReprTestEnum::A { value })
907            })
908            .variant("b", |ctx: &ParseContext<'_>| {
909                let rec = ctx.parse_record()?;
910                let name: String = rec.parse_field("name")?;
911                rec.deny_unknown_fields()?;
912                Ok(ReprTestEnum::B { name })
913            })
914            .parse()
915    }
916
917    #[test]
918    fn test_internal_repr_success() {
919        // { type = "a", value = 42 } with Internal { tag: "type" }
920        // Using Repr mode to enable repr-based variant resolution
921        let doc = eure!({ type = "a", value = 42 });
922        let root_id = doc.get_root_id();
923        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
924
925        let result = parse_repr_test_enum(
926            &ctx,
927            VariantRepr::Internal {
928                tag: "type".to_string(),
929            },
930        );
931        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
932    }
933
934    #[test]
935    fn test_external_repr_success() {
936        // { a = { value = 42 } } with External
937        // Using Repr mode to enable repr-based variant resolution
938        let doc = eure!({ a { value = 42 } });
939        let root_id = doc.get_root_id();
940        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
941
942        let result = parse_repr_test_enum(&ctx, VariantRepr::External);
943        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
944    }
945
946    #[test]
947    fn test_adjacent_repr_success() {
948        // { type = "a", content = { value = 42 } } with Adjacent { tag: "type", content: "content" }
949        // Using Repr mode to enable repr-based variant resolution
950        let doc = eure!({ type = "a", content { value = 42 } });
951        let root_id = doc.get_root_id();
952        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
953
954        let result = parse_repr_test_enum(
955            &ctx,
956            VariantRepr::Adjacent {
957                tag: "type".to_string(),
958                content: "content".to_string(),
959            },
960        );
961        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
962    }
963
964    #[test]
965    fn test_repr_mode_ignores_variant_extension() {
966        // In Repr mode, $variant extension is ignored - only repr pattern is used
967        // $variant = "b" would conflict, but repr extracts "a" and is used
968        let doc = eure!({ %variant = "b", type = "a", value = 42 });
969        let root_id = doc.get_root_id();
970        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
971
972        let result = parse_repr_test_enum(
973            &ctx,
974            VariantRepr::Internal {
975                tag: "type".to_string(),
976            },
977        );
978        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
979    }
980
981    #[test]
982    fn test_eure_mode_ignores_repr() {
983        // In Eure mode (default), repr is ignored - only $variant or untagged matching is used
984        let doc = eure!({ type = "a", value = 42 });
985        let root_id = doc.get_root_id();
986
987        // Default mode is Eure, which ignores repr
988        let ctx = doc.parse_context(root_id);
989
990        // Since there's no $variant and repr is ignored, this becomes untagged matching
991        // Both variants will be tried, and "a" has a "value" field so it should match
992        let result = ctx
993            .parse_union::<_, ParseError>(VariantRepr::Internal {
994                tag: "type".to_string(),
995            })
996            .unwrap()
997            .variant("a", |ctx: &ParseContext<'_>| {
998                let rec = ctx.parse_record()?;
999                let value: i64 = rec.parse_field("value")?;
1000                rec.allow_unknown_fields()?;
1001                Ok(ReprTestEnum::A { value })
1002            })
1003            .variant("b", |ctx: &ParseContext<'_>| {
1004                let rec = ctx.parse_record()?;
1005                let name: String = rec.parse_field("name")?;
1006                rec.deny_unknown_fields()?;
1007                Ok(ReprTestEnum::B { name })
1008            })
1009            .parse();
1010
1011        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
1012    }
1013
1014    #[test]
1015    fn test_internal_repr_unknown_variant_name() {
1016        // { type = "unknown", value = 42 } - "unknown" is not a registered variant
1017        // Using Repr mode to enable repr-based variant resolution
1018        let doc = eure!({ type = "unknown", value = 42 });
1019        let root_id = doc.get_root_id();
1020        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
1021
1022        let result = parse_repr_test_enum(
1023            &ctx,
1024            VariantRepr::Internal {
1025                tag: "type".to_string(),
1026            },
1027        );
1028
1029        // Should get UnknownVariant error since repr extracts "unknown"
1030        let err = result.unwrap_err();
1031        assert_eq!(
1032            err.kind,
1033            ParseErrorKind::UnknownVariant("unknown".to_string())
1034        );
1035    }
1036
1037    #[test]
1038    fn test_repr_not_extracted_falls_back_to_untagged() {
1039        // Document has 2 keys, so External repr (requires exactly 1 key) won't match
1040        // Falls back to Untagged parsing
1041        let doc = eure!({ value = 100, extra = "ignored" });
1042        let root_id = doc.get_root_id();
1043        let ctx = doc.parse_context(root_id);
1044
1045        // External repr won't match (2 keys), so Untagged will try each variant
1046        let result = ctx
1047            .parse_union::<_, ParseError>(VariantRepr::External)
1048            .unwrap()
1049            .variant("a", |ctx: &ParseContext<'_>| {
1050                let rec = ctx.parse_record()?;
1051                let value: i64 = rec.parse_field("value")?;
1052                // Don't deny_unknown_fields - we have "extra"
1053                Ok(ReprTestEnum::A { value })
1054            })
1055            .variant("b", |ctx: &ParseContext<'_>| {
1056                let rec = ctx.parse_record()?;
1057                let name: String = rec.parse_field("name")?;
1058                rec.deny_unknown_fields()?;
1059                Ok(ReprTestEnum::B { name })
1060            })
1061            .parse();
1062
1063        // Untagged parsing should succeed with variant "a"
1064        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 100 });
1065    }
1066
1067    #[test]
1068    fn test_external_repr_single_key_extracts_variant() {
1069        // Document has exactly 1 key, so External repr extracts it as variant name
1070        // Using Repr mode to enable repr-based variant resolution
1071        let doc = eure!({ value = 100 });
1072        let root_id = doc.get_root_id();
1073        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
1074
1075        // External repr extracts "value" as variant name
1076        // Since "value" is not a registered variant, we get UnknownVariant
1077        let err: ParseError = ctx
1078            .parse_union(VariantRepr::External)
1079            .unwrap()
1080            .variant("a", |ctx: &ParseContext<'_>| {
1081                let rec = ctx.parse_record()?;
1082                let value: i64 = rec.parse_field("value")?;
1083                rec.deny_unknown_fields()?;
1084                Ok(ReprTestEnum::A { value })
1085            })
1086            .variant("b", |ctx: &ParseContext<'_>| {
1087                let rec = ctx.parse_record()?;
1088                let name: String = rec.parse_field("name")?;
1089                rec.deny_unknown_fields()?;
1090                Ok(ReprTestEnum::B { name })
1091            })
1092            .parse()
1093            .unwrap_err();
1094
1095        assert_eq!(
1096            err.kind,
1097            ParseErrorKind::UnknownVariant("value".to_string())
1098        );
1099    }
1100
1101    // --- Corner case tests for resolve_variant ---
1102
1103    #[test]
1104    fn test_internal_repr_tag_is_integer_errors() {
1105        // { type = 123, value = 42 } - tag field is integer, not string
1106        // Using Repr mode to enable repr-based variant resolution
1107        // Note: eure! macro doesn't support integer field values for tag fields,
1108        // so we use manual construction to test this edge case
1109        use crate::document::EureDocument;
1110        use crate::document::node::NodeValue;
1111        use crate::value::{ObjectKey, PrimitiveValue};
1112        use num_bigint::BigInt;
1113
1114        let mut doc = EureDocument::new();
1115        let root_id = doc.get_root_id();
1116        doc.node_mut(root_id).content = NodeValue::Map(Default::default());
1117
1118        let type_node_id = doc
1119            .add_map_child(ObjectKey::String("type".to_string()), root_id)
1120            .unwrap()
1121            .node_id;
1122        doc.node_mut(type_node_id).content =
1123            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(123)));
1124
1125        let value_node_id = doc
1126            .add_map_child(ObjectKey::String("value".to_string()), root_id)
1127            .unwrap()
1128            .node_id;
1129        doc.node_mut(value_node_id).content =
1130            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
1131
1132        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
1133
1134        // Internal repr should error because tag field is not a string
1135        let Err(err) = ctx.parse_union::<ReprTestEnum, ParseError>(VariantRepr::Internal {
1136            tag: "type".to_string(),
1137        }) else {
1138            panic!("Expected error");
1139        };
1140
1141        // Error should point to the tag node
1142        assert_eq!(err.node_id, type_node_id);
1143    }
1144
1145    #[test]
1146    fn test_adjacent_repr_missing_content_falls_back_to_untagged() {
1147        // { type = "a", value = 42 } - has tag but no "content" field
1148        // Adjacent repr should not match, falls back to Untagged
1149        let doc = eure!({ type = "a", value = 42 });
1150        let root_id = doc.get_root_id();
1151        let ctx = doc.parse_context(root_id);
1152
1153        // Adjacent repr won't match (no "content" key), so Untagged parsing
1154        let result = ctx
1155            .parse_union::<_, ParseError>(VariantRepr::Adjacent {
1156                tag: "type".to_string(),
1157                content: "content".to_string(),
1158            })
1159            .unwrap()
1160            .variant("a", |ctx: &ParseContext<'_>| {
1161                let rec = ctx.parse_record()?;
1162                let value: i64 = rec.parse_field("value")?;
1163                // Don't deny_unknown_fields - we have "type"
1164                Ok(ReprTestEnum::A { value })
1165            })
1166            .variant("b", |ctx: &ParseContext<'_>| {
1167                let rec = ctx.parse_record()?;
1168                let name: String = rec.parse_field("name")?;
1169                rec.deny_unknown_fields()?;
1170                Ok(ReprTestEnum::B { name })
1171            })
1172            .parse();
1173
1174        // Untagged parsing should succeed with variant "a"
1175        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
1176    }
1177
1178    #[test]
1179    fn test_external_repr_non_string_key_falls_back_to_untagged() {
1180        // { 123 => { value = 42 } } - key is integer, not string
1181        // Note: eure! macro doesn't support integer keys, so we use manual construction
1182        use crate::document::EureDocument;
1183        use crate::document::node::NodeValue;
1184        use crate::value::{ObjectKey, PrimitiveValue};
1185        use num_bigint::BigInt;
1186
1187        let mut doc = EureDocument::new();
1188        let root_id = doc.get_root_id();
1189        doc.node_mut(root_id).content = NodeValue::Map(Default::default());
1190
1191        // Add integer key
1192        let variant_node_id = doc
1193            .add_map_child(ObjectKey::Number(BigInt::from(123)), root_id)
1194            .unwrap()
1195            .node_id;
1196        doc.node_mut(variant_node_id).content = NodeValue::Map(Default::default());
1197
1198        // Add "value" field inside
1199        let value_node_id = doc
1200            .add_map_child(ObjectKey::String("value".to_string()), variant_node_id)
1201            .unwrap()
1202            .node_id;
1203        doc.node_mut(value_node_id).content =
1204            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
1205
1206        let ctx = doc.parse_context(root_id);
1207
1208        // External repr won't match (key is not string), but since it has 1 key,
1209        // it will still try External extraction which fails due to non-string key,
1210        // then fall back to Untagged parsing which also fails (no matching variant)
1211        let err: ParseError = ctx
1212            .parse_union(VariantRepr::External)
1213            .unwrap()
1214            .variant("a", |ctx: &ParseContext<'_>| {
1215                let rec = ctx.parse_record()?;
1216                let value: i64 = rec.parse_field("value")?;
1217                rec.deny_unknown_fields()?;
1218                Ok(ReprTestEnum::A { value })
1219            })
1220            .variant("b", |ctx: &ParseContext<'_>| {
1221                let rec = ctx.parse_record()?;
1222                let name: String = rec.parse_field("name")?;
1223                rec.deny_unknown_fields()?;
1224                Ok(ReprTestEnum::B { name })
1225            })
1226            .parse()
1227            .unwrap_err();
1228
1229        // Falls back to Untagged, variant "a" tried but "value" not at root level
1230        assert_eq!(err.kind, ParseErrorKind::MissingField("value".to_string()));
1231    }
1232
1233    #[test]
1234    fn test_eure_mode_uses_variant_extension_over_repr() {
1235        // In Eure mode (default), $variant extension is used and repr is ignored
1236        // Internal repr would extract "a", but $variant = "b" takes precedence
1237        let doc = eure!({ %variant = "b", type = "a", value = 42 });
1238        let root_id = doc.get_root_id();
1239        let ctx = doc.parse_context(root_id);
1240
1241        // In Eure mode, $variant = "b" is used (repr is ignored)
1242        // Since "b" expects a "name" field and doc has "value", this fails
1243        let err: ParseError = ctx
1244            .parse_union(VariantRepr::Internal {
1245                tag: "type".to_string(),
1246            })
1247            .unwrap()
1248            .variant("a", |ctx: &ParseContext<'_>| {
1249                let rec = ctx.parse_record()?;
1250                let value: i64 = rec.parse_field("value")?;
1251                rec.allow_unknown_fields()?;
1252                Ok(ReprTestEnum::A { value })
1253            })
1254            .variant("b", |ctx: &ParseContext<'_>| {
1255                let rec = ctx.parse_record()?;
1256                let name: String = rec.parse_field("name")?;
1257                rec.deny_unknown_fields()?;
1258                Ok(ReprTestEnum::B { name })
1259            })
1260            .parse()
1261            .unwrap_err();
1262
1263        // In Eure mode, $variant = "b" is used, which expects "name" field
1264        assert_eq!(err.kind, ParseErrorKind::MissingField("name".to_string()));
1265    }
1266
1267    #[test]
1268    fn test_variant_path_empty_uses_untagged() {
1269        // When variant_path is Some but empty (consumed by parent), use Untagged
1270        // This is tested indirectly through nested unions after consuming the path
1271        let doc = eure!({ = "value" });
1272        let root_id = doc.get_root_id();
1273        let ctx = doc.parse_context(root_id);
1274
1275        // Simulate a context where variant_path was set but is now empty
1276        let child_ctx = ctx.with_variant_rest(Some(VariantPath::empty()));
1277
1278        // With empty variant_path, should use Untagged parsing
1279        let result: String = child_ctx
1280            .parse_union(VariantRepr::default())
1281            .unwrap()
1282            .variant("first", String::parse)
1283            .variant("second", String::parse)
1284            .parse()
1285            .unwrap();
1286
1287        // Priority variant "first" wins in Untagged mode
1288        assert_eq!(result, "value");
1289    }
1290
1291    // =============================================================================
1292    // Nested union tests (low-level, without derive macro)
1293    // =============================================================================
1294
1295    /// Nested enum for testing: outer level
1296    #[derive(Debug, PartialEq, Clone)]
1297    enum OuterUnion {
1298        Normal(InnerUnion),
1299        List(Vec<InnerUnion>),
1300    }
1301
1302    /// Nested enum for testing: inner level
1303    #[derive(Debug, PartialEq, Clone)]
1304    enum InnerUnion {
1305        Text(String),
1306        Number(i64),
1307    }
1308
1309    fn parse_inner_union(ctx: &ParseContext<'_>) -> Result<InnerUnion, ParseError> {
1310        ctx.parse_union(VariantRepr::default())?
1311            .variant("text", |ctx: &ParseContext<'_>| {
1312                let s: String = ctx.parse()?;
1313                Ok(InnerUnion::Text(s))
1314            })
1315            .variant("number", |ctx: &ParseContext<'_>| {
1316                let n: i64 = ctx.parse()?;
1317                Ok(InnerUnion::Number(n))
1318            })
1319            .parse()
1320    }
1321
1322    fn parse_outer_union(ctx: &ParseContext<'_>) -> Result<OuterUnion, ParseError> {
1323        use crate::document::node::NodeArray;
1324
1325        ctx.parse_union(VariantRepr::default())?
1326            .variant("normal", |ctx: &ParseContext<'_>| {
1327                let inner = parse_inner_union(ctx)?;
1328                Ok(OuterUnion::Normal(inner))
1329            })
1330            .variant("list", |ctx: &ParseContext<'_>| {
1331                // Parse array of InnerUnion using NodeArray
1332                let arr: &NodeArray = ctx.parse()?;
1333                let items: Result<Vec<InnerUnion>, _> = arr
1334                    .iter()
1335                    .map(|&node_id| parse_inner_union(&ctx.at(node_id)))
1336                    .collect();
1337                Ok(OuterUnion::List(items?))
1338            })
1339            .parse()
1340    }
1341
1342    #[test]
1343    fn test_nested_union_basic_text() {
1344        // Simple string -> OuterUnion::Normal(InnerUnion::Text)
1345        let doc = eure!({ = "hello" });
1346        let root_id = doc.get_root_id();
1347        let ctx = doc.parse_context(root_id);
1348
1349        let result = parse_outer_union(&ctx).unwrap();
1350        assert_eq!(
1351            result,
1352            OuterUnion::Normal(InnerUnion::Text("hello".to_string()))
1353        );
1354    }
1355
1356    #[test]
1357    fn test_nested_union_basic_number() {
1358        let doc = eure!({ = 42 });
1359        let root_id = doc.get_root_id();
1360        let ctx = doc.parse_context(root_id);
1361        let result = parse_outer_union(&ctx).unwrap();
1362        assert_eq!(result, OuterUnion::Normal(InnerUnion::Number(42)));
1363    }
1364
1365    #[test]
1366    fn test_nested_union_variant_path_propagation() {
1367        // $variant = "normal.text" should propagate through nested unions
1368        let doc = eure!({ %variant = "normal.text", = "test value" });
1369        let root_id = doc.get_root_id();
1370        let ctx = doc.parse_context(root_id);
1371
1372        let result = parse_outer_union(&ctx).unwrap();
1373        assert_eq!(
1374            result,
1375            OuterUnion::Normal(InnerUnion::Text("test value".to_string()))
1376        );
1377    }
1378
1379    #[test]
1380    fn test_nested_union_variant_path_number() {
1381        // $variant = "normal.number" - number variant explicitly selected
1382        let doc = eure!({ %variant = "normal.number", = 99 });
1383        let root_id = doc.get_root_id();
1384        let ctx = doc.parse_context(root_id);
1385        let result = parse_outer_union(&ctx).unwrap();
1386        assert_eq!(result, OuterUnion::Normal(InnerUnion::Number(99)));
1387    }
1388
1389    #[test]
1390    fn test_nested_union_inner_fails_outer_recovers() {
1391        // When inner union fails, outer should try next variant
1392        // Create a document that doesn't match "normal" variant's inner union
1393        // but could match "list" variant
1394        let doc = eure!({ = ["a", "b"] });
1395        let root_id = doc.get_root_id();
1396        let ctx = doc.parse_context(root_id);
1397
1398        let result = parse_outer_union(&ctx).unwrap();
1399        assert_eq!(
1400            result,
1401            OuterUnion::List(alloc::vec![
1402                InnerUnion::Text("a".to_string()),
1403                InnerUnion::Text("b".to_string()),
1404            ])
1405        );
1406    }
1407
1408    // =============================================================================
1409    // Triple nested union tests
1410    // =============================================================================
1411
1412    #[derive(Debug, PartialEq, Clone)]
1413    enum Level1 {
1414        A(Level2Union),
1415        B(String),
1416    }
1417
1418    #[derive(Debug, PartialEq, Clone)]
1419    enum Level2Union {
1420        X(Level3),
1421        Y(i64),
1422    }
1423
1424    #[derive(Debug, PartialEq, Clone)]
1425    enum Level3 {
1426        Leaf(String),
1427    }
1428
1429    fn parse_level3(ctx: &ParseContext<'_>) -> Result<Level3, ParseError> {
1430        ctx.parse_union(VariantRepr::default())?
1431            .variant("leaf", |ctx: &ParseContext<'_>| {
1432                let s: String = ctx.parse()?;
1433                Ok(Level3::Leaf(s))
1434            })
1435            .parse()
1436    }
1437
1438    fn parse_level2(ctx: &ParseContext<'_>) -> Result<Level2Union, ParseError> {
1439        ctx.parse_union(VariantRepr::default())?
1440            .variant("x", |ctx: &ParseContext<'_>| {
1441                let inner = parse_level3(ctx)?;
1442                Ok(Level2Union::X(inner))
1443            })
1444            .variant("y", |ctx: &ParseContext<'_>| {
1445                let n: i64 = ctx.parse()?;
1446                Ok(Level2Union::Y(n))
1447            })
1448            .parse()
1449    }
1450
1451    fn parse_level1(ctx: &ParseContext<'_>) -> Result<Level1, ParseError> {
1452        ctx.parse_union(VariantRepr::default())?
1453            .variant("a", |ctx: &ParseContext<'_>| {
1454                let inner = parse_level2(ctx)?;
1455                Ok(Level1::A(inner))
1456            })
1457            .variant("b", |ctx: &ParseContext<'_>| {
1458                let s: String = ctx.parse()?;
1459                Ok(Level1::B(s))
1460            })
1461            .parse()
1462    }
1463
1464    #[test]
1465    fn test_nested_union_three_levels_untagged() {
1466        // String input should match: Level1::A -> Level2Union::X -> Level3::Leaf
1467        // (first variant at each level wins in untagged mode)
1468        let doc = eure!({ = "deep value" });
1469        let root_id = doc.get_root_id();
1470        let ctx = doc.parse_context(root_id);
1471
1472        let result = parse_level1(&ctx).unwrap();
1473        assert_eq!(
1474            result,
1475            Level1::A(Level2Union::X(Level3::Leaf("deep value".to_string())))
1476        );
1477    }
1478
1479    #[test]
1480    fn test_nested_union_three_levels_variant_path() {
1481        // $variant = "a.x.leaf" - explicitly select through three levels
1482        let doc = eure!({ %variant = "a.x.leaf", = "explicit deep" });
1483        let root_id = doc.get_root_id();
1484        let ctx = doc.parse_context(root_id);
1485
1486        let result = parse_level1(&ctx).unwrap();
1487        assert_eq!(
1488            result,
1489            Level1::A(Level2Union::X(Level3::Leaf("explicit deep".to_string())))
1490        );
1491    }
1492
1493    #[test]
1494    fn test_nested_union_three_levels_variant_path_partial() {
1495        // $variant = "a.y" - select a.y, inner uses untagged
1496        let doc = eure!({ %variant = "a.y", = 123 });
1497        let root_id = doc.get_root_id();
1498        let ctx = doc.parse_context(root_id);
1499        let result = parse_level1(&ctx).unwrap();
1500        assert_eq!(result, Level1::A(Level2Union::Y(123)));
1501    }
1502
1503    #[test]
1504    fn test_nested_union_invalid_inner_variant_path() {
1505        // $variant = "a.x.invalid" - "invalid" doesn't exist in Level3
1506        let doc = eure!({ %variant = "a.x.invalid", = "value" });
1507        let root_id = doc.get_root_id();
1508        let ctx = doc.parse_context(root_id);
1509
1510        let err = parse_level1(&ctx).unwrap_err();
1511        assert_eq!(
1512            err.kind,
1513            ParseErrorKind::UnknownVariant("invalid".to_string())
1514        );
1515    }
1516
1517    // =============================================================================
1518    // Flatten with nested union - accessed field tracking tests
1519    // =============================================================================
1520
1521    #[test]
1522    fn test_flatten_nested_union_accessed_fields_basic() {
1523        use crate::parse::AccessedSet;
1524        use crate::parse::FlattenContext;
1525        use crate::parse::ParserScope;
1526
1527        // Test that accessed fields are properly tracked through nested unions
1528        let doc = eure!({
1529            field_a = "value_a"
1530            field_b = "value_b"
1531        });
1532        let root_id = doc.get_root_id();
1533
1534        // Create flatten context to track field access
1535        let flatten_ctx = FlattenContext::new(AccessedSet::new(), ParserScope::Record);
1536        let ctx =
1537            ParseContext::with_flatten_ctx(&doc, root_id, flatten_ctx.clone(), UnionTagMode::Eure);
1538
1539        // Parse a union that accesses field_a
1540        let record = ctx.parse_record().unwrap();
1541        let _field_a: String = record.parse_field("field_a").unwrap();
1542
1543        // field_a should be marked as accessed
1544        let (accessed, _) = flatten_ctx.capture_current_state();
1545        assert!(accessed.contains("field_a"));
1546        assert!(!accessed.contains("field_b"));
1547    }
1548}