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(variant_node.content.value_kind()),
343        })?;
344
345        VariantPath::parse(s).map(Some).map_err(|_| ParseError {
346            node_id: variant_node_id,
347            kind: ParseErrorKind::InvalidVariantPath(s.to_string()),
348        })
349    }
350
351    /// Register a variant with short-circuit semantics (default).
352    ///
353    /// When this variant matches in untagged mode, parsing succeeds immediately
354    /// without checking other variants. Use definition order to express priority.
355    pub fn variant<P: DocumentParser<'doc, Output = T, Error = E>>(
356        mut self,
357        name: &str,
358        f: P,
359    ) -> Self {
360        self.try_variant(name, f, true);
361        self
362    }
363
364    /// Register a variant with short-circuit semantics using FromEure.
365    pub fn parse_variant<V: FromEure<'doc, Error = E>>(
366        mut self,
367        name: &str,
368        mut then: impl FnMut(V) -> Result<T, E>,
369    ) -> Self {
370        self.try_variant(
371            name,
372            move |ctx: &ParseContext<'doc>| {
373                let v = V::parse(ctx)?;
374                then(v)
375            },
376            true,
377        );
378        self
379    }
380
381    /// Register a variant with unambiguous semantics.
382    ///
383    /// All unambiguous variants are tried to detect conflicts.
384    /// If multiple unambiguous variants match, an AmbiguousUnion error is returned.
385    /// Use for catch-all variants or when you need conflict detection.
386    pub fn variant_unambiguous<P: DocumentParser<'doc, Output = T, Error = E>>(
387        mut self,
388        name: &str,
389        f: P,
390    ) -> Self {
391        self.try_variant(name, f, false);
392        self
393    }
394
395    /// Register a variant with unambiguous semantics using FromEure.
396    pub fn parse_variant_unambiguous<V: FromEure<'doc, Error = E>>(
397        mut self,
398        name: &str,
399        mut then: impl FnMut(V) -> Result<T, E>,
400    ) -> Self {
401        self.try_variant(
402            name,
403            move |ctx: &ParseContext<'doc>| {
404                let v = V::parse(ctx)?;
405                then(v)
406            },
407            false,
408        );
409        self
410    }
411
412    /// Internal helper for variant/other logic.
413    fn try_variant<P: DocumentParser<'doc, Output = T, Error = E>>(
414        &mut self,
415        name: &str,
416        mut f: P,
417        is_priority: bool,
418    ) {
419        // 1. If variant is determined, only try matching variant
420        if let Some((ref v_name, ref v_ctx, ref rest)) = self.variant {
421            if v_name == name && self.variant_result.is_none() {
422                let child_ctx = v_ctx.with_variant_rest(rest.clone());
423                let result = f.parse(&child_ctx);
424                // Variant explicitly specified - no rollback needed on failure,
425                // error propagates directly. Changes kept if success.
426                self.variant_result = Some(result);
427            }
428            return;
429        }
430
431        // 2. Untagged mode: try all variants
432
433        // Skip if already have priority result
434        if self.priority_result.is_some() {
435            return;
436        }
437
438        let child_ctx = self.ctx.with_variant_rest(None);
439        match f.parse(&child_ctx) {
440            Ok(value) => {
441                if is_priority {
442                    // Priority variant succeeded - keep the changes
443                    // (snapshot will be popped in parse())
444                    self.priority_result = Some(value);
445                } else {
446                    // Other variant succeeded - capture state before restoring
447                    // We need to try more variants, so restore for next attempt
448                    if let Some(ref fc) = self.flatten_ctx {
449                        let captured = fc.capture_current_state();
450                        fc.restore_to_current_snapshot();
451                        self.other_results.push((name.to_string(), value, captured));
452                    } else {
453                        // No flatten context - no state to capture
454                        self.other_results.push((
455                            name.to_string(),
456                            value,
457                            (Default::default(), Default::default()),
458                        ));
459                    }
460                }
461            }
462            Err(e) => {
463                // Variant failed - restore to snapshot
464                if let Some(ref fc) = self.flatten_ctx {
465                    fc.restore_to_current_snapshot();
466                }
467                self.failures.push((name.to_string(), e));
468            }
469        }
470    }
471
472    /// Execute the union parse with oneOf semantics.
473    pub fn parse(self) -> Result<T, E> {
474        let node_id = self.ctx.node_id();
475
476        // 1. Variant determined - return its result
477        // When variant is explicitly specified via $variant, we don't use snapshot/rollback.
478        // The accessed fields from parsing are kept (success) or don't matter (error propagates).
479        if let Some((v_name, _, _)) = self.variant {
480            let result = self.variant_result.unwrap_or_else(|| {
481                Err(ParseError {
482                    node_id,
483                    kind: ParseErrorKind::UnknownVariant(v_name),
484                }
485                .into())
486            });
487            // Pop the snapshot - if success, keep changes; if error, doesn't matter
488            if let Some(ref fc) = self.flatten_ctx {
489                match &result {
490                    Ok(_) => fc.pop_without_restore(),
491                    Err(_) => fc.pop_and_restore(),
492                }
493            }
494            return result;
495        }
496
497        // 2. Priority result - success, keep changes
498        if let Some(value) = self.priority_result {
499            if let Some(ref fc) = self.flatten_ctx {
500                fc.pop_without_restore();
501            }
502            return Ok(value);
503        }
504
505        // 3. Check other_results
506        match self.other_results.len() {
507            0 => {
508                // No match - rollback and return error
509                if let Some(ref fc) = self.flatten_ctx {
510                    fc.pop_and_restore();
511                }
512                Err(self.no_match_error(node_id))
513            }
514            1 => {
515                // Single match - restore to captured state (from successful variant)
516                let (_, value, captured_state) = self.other_results.into_iter().next().unwrap();
517                if let Some(ref fc) = self.flatten_ctx {
518                    fc.restore_to_state(captured_state);
519                    fc.pop_without_restore();
520                }
521                Ok(value)
522            }
523            _ => {
524                // Ambiguous - rollback all changes
525                if let Some(ref fc) = self.flatten_ctx {
526                    fc.pop_and_restore();
527                }
528                Err(ParseError {
529                    node_id,
530                    kind: ParseErrorKind::AmbiguousUnion(
531                        self.other_results
532                            .into_iter()
533                            .map(|(name, _, _)| name)
534                            .collect(),
535                    ),
536                }
537                .into())
538            }
539        }
540    }
541
542    /// Create an error for when no variant matches.
543    fn no_match_error(self, node_id: crate::document::NodeId) -> E {
544        self.failures
545            .into_iter()
546            .next()
547            .map(|(_, e)| e)
548            .unwrap_or_else(|| {
549                ParseError {
550                    node_id,
551                    kind: ParseErrorKind::NoMatchingVariant { variant: None },
552                }
553                .into()
554            })
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::eure;
562    use crate::parse::AlwaysParser;
563    use crate::parse::DocumentParserExt as _;
564
565    #[derive(Debug, PartialEq, Clone)]
566    enum TestEnum {
567        Foo,
568        Bar,
569    }
570
571    #[test]
572    fn test_union_single_match() {
573        let doc = eure!({ = "foo" });
574        let root_id = doc.get_root_id();
575        let ctx = doc.parse_context(root_id);
576
577        let result: TestEnum = ctx
578            .parse_union(VariantRepr::default())
579            .unwrap()
580            .variant("foo", |ctx: &ParseContext<'_>| {
581                let s: &str = ctx.parse()?;
582                if s == "foo" {
583                    Ok(TestEnum::Foo)
584                } else {
585                    Err(ParseError {
586                        node_id: ctx.node_id(),
587                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
588                    })
589                }
590            })
591            .variant("bar", |ctx: &ParseContext<'_>| {
592                let s: &str = ctx.parse()?;
593                if s == "bar" {
594                    Ok(TestEnum::Bar)
595                } else {
596                    Err(ParseError {
597                        node_id: ctx.node_id(),
598                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
599                    })
600                }
601            })
602            .parse()
603            .unwrap();
604
605        assert_eq!(result, TestEnum::Foo);
606    }
607
608    #[test]
609    fn test_union_priority_short_circuit() {
610        let doc = eure!({ = "value" });
611        let root_id = doc.get_root_id();
612        let ctx = doc.parse_context(root_id);
613
614        // Both variants would match, but first one wins due to priority
615        let result: String = ctx
616            .parse_union(VariantRepr::default())
617            .unwrap()
618            .variant("first", String::parse)
619            .variant("second", String::parse)
620            .parse()
621            .unwrap();
622
623        assert_eq!(result, "value");
624    }
625
626    #[test]
627    fn test_union_no_match() {
628        let doc = eure!({ = "baz" });
629        let root_id = doc.get_root_id();
630        let ctx = doc.parse_context(root_id);
631
632        let result: Result<TestEnum, ParseError> = ctx
633            .parse_union(VariantRepr::default())
634            .unwrap()
635            .variant("foo", |ctx: &ParseContext<'_>| {
636                let s: &str = ctx.parse()?;
637                if s == "foo" {
638                    Ok(TestEnum::Foo)
639                } else {
640                    Err(ParseError {
641                        node_id: ctx.node_id(),
642                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
643                    })
644                }
645            })
646            .parse();
647
648        assert!(result.is_err());
649    }
650
651    // --- $variant extension tests ---
652
653    #[test]
654    fn test_variant_extension_match_success() {
655        // $variant = "baz" specified, matches other("baz")
656        // All parsers always succeed
657        let doc = eure!({ %variant = "baz", = "anything" });
658        let root_id = doc.get_root_id();
659        let ctx = doc.parse_context(root_id);
660
661        let result: TestEnum = ctx
662            .parse_union(VariantRepr::default())
663            .unwrap()
664            .variant(
665                "foo",
666                AlwaysParser::<TestEnum, ParseError>::new(TestEnum::Foo),
667            )
668            .variant_unambiguous("baz", AlwaysParser::new(TestEnum::Bar))
669            .parse()
670            .unwrap();
671
672        assert_eq!(result, TestEnum::Bar);
673    }
674
675    #[test]
676    fn test_variant_extension_unknown() {
677        // $variant = "unknown" specified, but "unknown" is not registered
678        // All parsers always succeed
679        let doc = eure!({ %variant = "unknown", = "anything" });
680        let root_id = doc.get_root_id();
681        let ctx = doc.parse_context(root_id);
682
683        let err: ParseError = ctx
684            .parse_union(VariantRepr::default())
685            .unwrap()
686            .variant("foo", AlwaysParser::new(TestEnum::Foo))
687            .variant_unambiguous("baz", AlwaysParser::new(TestEnum::Bar))
688            .parse()
689            .unwrap_err();
690
691        assert_eq!(err.node_id, root_id);
692        assert_eq!(
693            err.kind,
694            ParseErrorKind::UnknownVariant("unknown".to_string())
695        );
696    }
697
698    #[test]
699    fn test_variant_extension_match_parse_failure() {
700        // $variant = "baz" specified, "baz" parser fails
701        let doc = eure!({ %variant = "baz", = "anything" });
702        let root_id = doc.get_root_id();
703        let ctx = doc.parse_context(root_id);
704
705        let err = ctx
706            .parse_union(VariantRepr::default())
707            .unwrap()
708            .variant("foo", AlwaysParser::new(TestEnum::Foo))
709            .variant_unambiguous("baz", |ctx: &ParseContext<'_>| {
710                Err(ParseError {
711                    node_id: ctx.node_id(),
712                    kind: ParseErrorKind::MissingField("test".to_string()),
713                })
714            })
715            .parse()
716            .unwrap_err();
717
718        // Parser's error is returned directly
719        assert_eq!(err.node_id, root_id);
720        assert_eq!(err.kind, ParseErrorKind::MissingField("test".to_string()));
721    }
722
723    // --- nested variant tests ---
724
725    #[derive(Debug, PartialEq, Clone)]
726    enum Outer {
727        A(Inner),
728        B(i32),
729    }
730
731    #[derive(Debug, PartialEq, Clone)]
732    enum Inner {
733        X,
734        Y,
735    }
736
737    fn parse_inner(ctx: &ParseContext<'_>) -> Result<Inner, ParseError> {
738        ctx.parse_union(VariantRepr::default())
739            .unwrap()
740            .variant("x", AlwaysParser::new(Inner::X))
741            .variant("y", AlwaysParser::new(Inner::Y))
742            .parse()
743    }
744
745    #[test]
746    fn test_variant_nested_single_segment() {
747        // $variant = "a" - matches "a", rest is None -> Inner defaults to X
748        let doc = eure!({ %variant = "a", = "value" });
749        let root_id = doc.get_root_id();
750        let ctx = doc.parse_context(root_id);
751
752        let result: Outer = ctx
753            .parse_union(VariantRepr::default())
754            .unwrap()
755            .variant("a", parse_inner.map(Outer::A))
756            .variant("b", AlwaysParser::new(Outer::B(42)))
757            .parse()
758            .unwrap();
759
760        assert_eq!(result, Outer::A(Inner::X));
761    }
762
763    #[test]
764    fn test_variant_nested_multi_segment() {
765        // $variant = "a.y" - matches "a", rest is Some("y")
766        let doc = eure!({ %variant = "a.y", = "value" });
767        let root_id = doc.get_root_id();
768        let ctx = doc.parse_context(root_id);
769
770        let result: Outer = ctx
771            .parse_union(VariantRepr::default())
772            .unwrap()
773            .variant("a", parse_inner.map(Outer::A))
774            .variant("b", AlwaysParser::new(Outer::B(42)))
775            .parse()
776            .unwrap();
777
778        assert_eq!(result, Outer::A(Inner::Y));
779    }
780
781    #[test]
782    fn test_variant_nested_invalid_inner() {
783        // $variant = "a.z" - matches "a", but "z" is not valid for Inner
784        let doc = eure!({ %variant = "a.z", = "value" });
785        let root_id = doc.get_root_id();
786        let ctx = doc.parse_context(root_id);
787
788        let err = ctx
789            .parse_union(VariantRepr::default())
790            .unwrap()
791            .variant("a", parse_inner.map(Outer::A))
792            .variant("b", AlwaysParser::new(Outer::B(42)))
793            .parse()
794            .unwrap_err();
795
796        assert_eq!(err.kind, ParseErrorKind::UnknownVariant("z".to_string()));
797    }
798
799    #[test]
800    fn test_variant_non_nested_with_nested_path() {
801        // $variant = "b.x" but "b" parser doesn't expect nested path
802        // The child context will have variant_path = Some("x")
803        // If the "b" parser is a non-union type, it should error on unexpected variant path
804        let doc = eure!({ %variant = "b.x", = "value" });
805        let root_id = doc.get_root_id();
806        let ctx = doc.parse_context(root_id);
807
808        // "b" is registered as a variant but if called with "b.x",
809        // the closure gets ctx with variant_path = Some("x")
810        // The simple parser Ok(Outer::B(42)) doesn't check variant path,
811        // but a proper impl would use ctx.parse_primitive() which errors
812        let err = ctx
813            .parse_union(VariantRepr::default())
814            .unwrap()
815            .variant("a", parse_inner.map(Outer::A))
816            .variant("b", |ctx: &ParseContext<'_>| {
817                // Simulate parsing a primitive that checks variant path
818                ctx.parse_primitive()?;
819                Ok(Outer::B(42))
820            })
821            .parse()
822            .unwrap_err();
823
824        // parse_primitive should error because variant path "x" remains
825        assert!(matches!(err.kind, ParseErrorKind::UnexpectedVariantPath(_)));
826    }
827
828    // --- invalid $variant tests ---
829
830    use crate::value::ValueKind;
831
832    #[test]
833    fn test_invalid_variant_type_errors() {
834        // $variant = 123 (integer, not string) - should error at parse_union()
835        // Note: eure! macro can't create invalid $variant types, so we use manual construction
836        use crate::document::node::NodeValue;
837        use crate::value::PrimitiveValue;
838        use num_bigint::BigInt;
839
840        // Create base doc with eure! and then add invalid integer $variant
841        let mut doc = eure!({ = "foo" });
842        let root_id = doc.get_root_id();
843        let variant_node_id = doc
844            .add_extension("variant".parse().unwrap(), root_id)
845            .unwrap()
846            .node_id;
847        doc.node_mut(variant_node_id).content =
848            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(123)));
849
850        let ctx = doc.parse_context(root_id);
851
852        let Err(err) = ctx.parse_union::<TestEnum, ParseError>(VariantRepr::default()) else {
853            panic!("Expected error");
854        };
855        assert_eq!(
856            err,
857            ParseError {
858                node_id: variant_node_id,
859                kind: ParseErrorKind::InvalidVariantType(ValueKind::Integer),
860            }
861        );
862    }
863
864    #[test]
865    fn test_invalid_variant_path_syntax_errors() {
866        // $variant = "foo..bar" (invalid path syntax) - should error at parse_union()
867        let doc = eure!({ %variant = "foo..bar", = "foo" });
868        let root_id = doc.get_root_id();
869        let variant_node_id = *doc.node(root_id).extensions.get(&VARIANT).unwrap();
870        let ctx = doc.parse_context(root_id);
871
872        let Err(err) = ctx.parse_union::<TestEnum, ParseError>(VariantRepr::default()) else {
873            panic!("Expected error");
874        };
875        assert_eq!(
876            err,
877            ParseError {
878                node_id: variant_node_id,
879                kind: ParseErrorKind::InvalidVariantPath("foo..bar".to_string()),
880            }
881        );
882    }
883
884    // --- VariantRepr tests ---
885
886    #[derive(Debug, PartialEq)]
887    enum ReprTestEnum {
888        A { value: i64 },
889        B { name: String },
890    }
891
892    fn parse_repr_test_enum(
893        ctx: &ParseContext<'_>,
894        repr: VariantRepr,
895    ) -> Result<ReprTestEnum, ParseError> {
896        ctx.parse_union(repr)?
897            .variant("a", |ctx: &ParseContext<'_>| {
898                let rec = ctx.parse_record()?;
899                let value: i64 = rec.parse_field("value")?;
900                rec.deny_unknown_fields()?;
901                Ok(ReprTestEnum::A { value })
902            })
903            .variant("b", |ctx: &ParseContext<'_>| {
904                let rec = ctx.parse_record()?;
905                let name: String = rec.parse_field("name")?;
906                rec.deny_unknown_fields()?;
907                Ok(ReprTestEnum::B { name })
908            })
909            .parse()
910    }
911
912    #[test]
913    fn test_internal_repr_success() {
914        // { type = "a", value = 42 } with Internal { tag: "type" }
915        // Using Repr mode to enable repr-based variant resolution
916        let doc = eure!({ type = "a", value = 42 });
917        let root_id = doc.get_root_id();
918        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
919
920        let result = parse_repr_test_enum(
921            &ctx,
922            VariantRepr::Internal {
923                tag: "type".to_string(),
924            },
925        );
926        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
927    }
928
929    #[test]
930    fn test_external_repr_success() {
931        // { a = { value = 42 } } with External
932        // Using Repr mode to enable repr-based variant resolution
933        let doc = eure!({ a { value = 42 } });
934        let root_id = doc.get_root_id();
935        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
936
937        let result = parse_repr_test_enum(&ctx, VariantRepr::External);
938        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
939    }
940
941    #[test]
942    fn test_adjacent_repr_success() {
943        // { type = "a", content = { value = 42 } } with Adjacent { tag: "type", content: "content" }
944        // Using Repr mode to enable repr-based variant resolution
945        let doc = eure!({ type = "a", content { value = 42 } });
946        let root_id = doc.get_root_id();
947        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
948
949        let result = parse_repr_test_enum(
950            &ctx,
951            VariantRepr::Adjacent {
952                tag: "type".to_string(),
953                content: "content".to_string(),
954            },
955        );
956        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
957    }
958
959    #[test]
960    fn test_repr_mode_ignores_variant_extension() {
961        // In Repr mode, $variant extension is ignored - only repr pattern is used
962        // $variant = "b" would conflict, but repr extracts "a" and is used
963        let doc = eure!({ %variant = "b", type = "a", value = 42 });
964        let root_id = doc.get_root_id();
965        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
966
967        let result = parse_repr_test_enum(
968            &ctx,
969            VariantRepr::Internal {
970                tag: "type".to_string(),
971            },
972        );
973        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
974    }
975
976    #[test]
977    fn test_eure_mode_ignores_repr() {
978        // In Eure mode (default), repr is ignored - only $variant or untagged matching is used
979        let doc = eure!({ type = "a", value = 42 });
980        let root_id = doc.get_root_id();
981
982        // Default mode is Eure, which ignores repr
983        let ctx = doc.parse_context(root_id);
984
985        // Since there's no $variant and repr is ignored, this becomes untagged matching
986        // Both variants will be tried, and "a" has a "value" field so it should match
987        let result = ctx
988            .parse_union::<_, ParseError>(VariantRepr::Internal {
989                tag: "type".to_string(),
990            })
991            .unwrap()
992            .variant("a", |ctx: &ParseContext<'_>| {
993                let rec = ctx.parse_record()?;
994                let value: i64 = rec.parse_field("value")?;
995                rec.allow_unknown_fields()?;
996                Ok(ReprTestEnum::A { value })
997            })
998            .variant("b", |ctx: &ParseContext<'_>| {
999                let rec = ctx.parse_record()?;
1000                let name: String = rec.parse_field("name")?;
1001                rec.deny_unknown_fields()?;
1002                Ok(ReprTestEnum::B { name })
1003            })
1004            .parse();
1005
1006        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
1007    }
1008
1009    #[test]
1010    fn test_internal_repr_unknown_variant_name() {
1011        // { type = "unknown", value = 42 } - "unknown" is not a registered variant
1012        // Using Repr mode to enable repr-based variant resolution
1013        let doc = eure!({ type = "unknown", value = 42 });
1014        let root_id = doc.get_root_id();
1015        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
1016
1017        let result = parse_repr_test_enum(
1018            &ctx,
1019            VariantRepr::Internal {
1020                tag: "type".to_string(),
1021            },
1022        );
1023
1024        // Should get UnknownVariant error since repr extracts "unknown"
1025        let err = result.unwrap_err();
1026        assert_eq!(
1027            err.kind,
1028            ParseErrorKind::UnknownVariant("unknown".to_string())
1029        );
1030    }
1031
1032    #[test]
1033    fn test_repr_not_extracted_falls_back_to_untagged() {
1034        // Document has 2 keys, so External repr (requires exactly 1 key) won't match
1035        // Falls back to Untagged parsing
1036        let doc = eure!({ value = 100, extra = "ignored" });
1037        let root_id = doc.get_root_id();
1038        let ctx = doc.parse_context(root_id);
1039
1040        // External repr won't match (2 keys), so Untagged will try each variant
1041        let result = ctx
1042            .parse_union::<_, ParseError>(VariantRepr::External)
1043            .unwrap()
1044            .variant("a", |ctx: &ParseContext<'_>| {
1045                let rec = ctx.parse_record()?;
1046                let value: i64 = rec.parse_field("value")?;
1047                // Don't deny_unknown_fields - we have "extra"
1048                Ok(ReprTestEnum::A { value })
1049            })
1050            .variant("b", |ctx: &ParseContext<'_>| {
1051                let rec = ctx.parse_record()?;
1052                let name: String = rec.parse_field("name")?;
1053                rec.deny_unknown_fields()?;
1054                Ok(ReprTestEnum::B { name })
1055            })
1056            .parse();
1057
1058        // Untagged parsing should succeed with variant "a"
1059        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 100 });
1060    }
1061
1062    #[test]
1063    fn test_external_repr_single_key_extracts_variant() {
1064        // Document has exactly 1 key, so External repr extracts it as variant name
1065        // Using Repr mode to enable repr-based variant resolution
1066        let doc = eure!({ value = 100 });
1067        let root_id = doc.get_root_id();
1068        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
1069
1070        // External repr extracts "value" as variant name
1071        // Since "value" is not a registered variant, we get UnknownVariant
1072        let err: ParseError = ctx
1073            .parse_union(VariantRepr::External)
1074            .unwrap()
1075            .variant("a", |ctx: &ParseContext<'_>| {
1076                let rec = ctx.parse_record()?;
1077                let value: i64 = rec.parse_field("value")?;
1078                rec.deny_unknown_fields()?;
1079                Ok(ReprTestEnum::A { value })
1080            })
1081            .variant("b", |ctx: &ParseContext<'_>| {
1082                let rec = ctx.parse_record()?;
1083                let name: String = rec.parse_field("name")?;
1084                rec.deny_unknown_fields()?;
1085                Ok(ReprTestEnum::B { name })
1086            })
1087            .parse()
1088            .unwrap_err();
1089
1090        assert_eq!(
1091            err.kind,
1092            ParseErrorKind::UnknownVariant("value".to_string())
1093        );
1094    }
1095
1096    // --- Corner case tests for resolve_variant ---
1097
1098    #[test]
1099    fn test_internal_repr_tag_is_integer_errors() {
1100        // { type = 123, value = 42 } - tag field is integer, not string
1101        // Using Repr mode to enable repr-based variant resolution
1102        // Note: eure! macro doesn't support integer field values for tag fields,
1103        // so we use manual construction to test this edge case
1104        use crate::document::EureDocument;
1105        use crate::document::node::NodeValue;
1106        use crate::value::{ObjectKey, PrimitiveValue};
1107        use num_bigint::BigInt;
1108
1109        let mut doc = EureDocument::new();
1110        let root_id = doc.get_root_id();
1111        doc.node_mut(root_id).content = NodeValue::Map(Default::default());
1112
1113        let type_node_id = doc
1114            .add_map_child(ObjectKey::String("type".to_string()), root_id)
1115            .unwrap()
1116            .node_id;
1117        doc.node_mut(type_node_id).content =
1118            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(123)));
1119
1120        let value_node_id = doc
1121            .add_map_child(ObjectKey::String("value".to_string()), root_id)
1122            .unwrap()
1123            .node_id;
1124        doc.node_mut(value_node_id).content =
1125            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
1126
1127        let ctx = ParseContext::with_union_tag_mode(&doc, root_id, UnionTagMode::Repr);
1128
1129        // Internal repr should error because tag field is not a string
1130        let Err(err) = ctx.parse_union::<ReprTestEnum, ParseError>(VariantRepr::Internal {
1131            tag: "type".to_string(),
1132        }) else {
1133            panic!("Expected error");
1134        };
1135
1136        // Error should point to the tag node
1137        assert_eq!(err.node_id, type_node_id);
1138    }
1139
1140    #[test]
1141    fn test_adjacent_repr_missing_content_falls_back_to_untagged() {
1142        // { type = "a", value = 42 } - has tag but no "content" field
1143        // Adjacent repr should not match, falls back to Untagged
1144        let doc = eure!({ type = "a", value = 42 });
1145        let root_id = doc.get_root_id();
1146        let ctx = doc.parse_context(root_id);
1147
1148        // Adjacent repr won't match (no "content" key), so Untagged parsing
1149        let result = ctx
1150            .parse_union::<_, ParseError>(VariantRepr::Adjacent {
1151                tag: "type".to_string(),
1152                content: "content".to_string(),
1153            })
1154            .unwrap()
1155            .variant("a", |ctx: &ParseContext<'_>| {
1156                let rec = ctx.parse_record()?;
1157                let value: i64 = rec.parse_field("value")?;
1158                // Don't deny_unknown_fields - we have "type"
1159                Ok(ReprTestEnum::A { value })
1160            })
1161            .variant("b", |ctx: &ParseContext<'_>| {
1162                let rec = ctx.parse_record()?;
1163                let name: String = rec.parse_field("name")?;
1164                rec.deny_unknown_fields()?;
1165                Ok(ReprTestEnum::B { name })
1166            })
1167            .parse();
1168
1169        // Untagged parsing should succeed with variant "a"
1170        assert_eq!(result.unwrap(), ReprTestEnum::A { value: 42 });
1171    }
1172
1173    #[test]
1174    fn test_external_repr_non_string_key_falls_back_to_untagged() {
1175        // { 123 => { value = 42 } } - key is integer, not string
1176        // Note: eure! macro doesn't support integer keys, so we use manual construction
1177        use crate::document::EureDocument;
1178        use crate::document::node::NodeValue;
1179        use crate::value::{ObjectKey, PrimitiveValue};
1180        use num_bigint::BigInt;
1181
1182        let mut doc = EureDocument::new();
1183        let root_id = doc.get_root_id();
1184        doc.node_mut(root_id).content = NodeValue::Map(Default::default());
1185
1186        // Add integer key
1187        let variant_node_id = doc
1188            .add_map_child(ObjectKey::Number(BigInt::from(123)), root_id)
1189            .unwrap()
1190            .node_id;
1191        doc.node_mut(variant_node_id).content = NodeValue::Map(Default::default());
1192
1193        // Add "value" field inside
1194        let value_node_id = doc
1195            .add_map_child(ObjectKey::String("value".to_string()), variant_node_id)
1196            .unwrap()
1197            .node_id;
1198        doc.node_mut(value_node_id).content =
1199            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
1200
1201        let ctx = doc.parse_context(root_id);
1202
1203        // External repr won't match (key is not string), but since it has 1 key,
1204        // it will still try External extraction which fails due to non-string key,
1205        // then fall back to Untagged parsing which also fails (no matching variant)
1206        let err: ParseError = ctx
1207            .parse_union(VariantRepr::External)
1208            .unwrap()
1209            .variant("a", |ctx: &ParseContext<'_>| {
1210                let rec = ctx.parse_record()?;
1211                let value: i64 = rec.parse_field("value")?;
1212                rec.deny_unknown_fields()?;
1213                Ok(ReprTestEnum::A { value })
1214            })
1215            .variant("b", |ctx: &ParseContext<'_>| {
1216                let rec = ctx.parse_record()?;
1217                let name: String = rec.parse_field("name")?;
1218                rec.deny_unknown_fields()?;
1219                Ok(ReprTestEnum::B { name })
1220            })
1221            .parse()
1222            .unwrap_err();
1223
1224        // Falls back to Untagged, variant "a" tried but "value" not at root level
1225        assert_eq!(err.kind, ParseErrorKind::MissingField("value".to_string()));
1226    }
1227
1228    #[test]
1229    fn test_eure_mode_uses_variant_extension_over_repr() {
1230        // In Eure mode (default), $variant extension is used and repr is ignored
1231        // Internal repr would extract "a", but $variant = "b" takes precedence
1232        let doc = eure!({ %variant = "b", type = "a", value = 42 });
1233        let root_id = doc.get_root_id();
1234        let ctx = doc.parse_context(root_id);
1235
1236        // In Eure mode, $variant = "b" is used (repr is ignored)
1237        // Since "b" expects a "name" field and doc has "value", this fails
1238        let err: ParseError = ctx
1239            .parse_union(VariantRepr::Internal {
1240                tag: "type".to_string(),
1241            })
1242            .unwrap()
1243            .variant("a", |ctx: &ParseContext<'_>| {
1244                let rec = ctx.parse_record()?;
1245                let value: i64 = rec.parse_field("value")?;
1246                rec.allow_unknown_fields()?;
1247                Ok(ReprTestEnum::A { value })
1248            })
1249            .variant("b", |ctx: &ParseContext<'_>| {
1250                let rec = ctx.parse_record()?;
1251                let name: String = rec.parse_field("name")?;
1252                rec.deny_unknown_fields()?;
1253                Ok(ReprTestEnum::B { name })
1254            })
1255            .parse()
1256            .unwrap_err();
1257
1258        // In Eure mode, $variant = "b" is used, which expects "name" field
1259        assert_eq!(err.kind, ParseErrorKind::MissingField("name".to_string()));
1260    }
1261
1262    #[test]
1263    fn test_variant_path_empty_uses_untagged() {
1264        // When variant_path is Some but empty (consumed by parent), use Untagged
1265        // This is tested indirectly through nested unions after consuming the path
1266        let doc = eure!({ = "value" });
1267        let root_id = doc.get_root_id();
1268        let ctx = doc.parse_context(root_id);
1269
1270        // Simulate a context where variant_path was set but is now empty
1271        let child_ctx = ctx.with_variant_rest(Some(VariantPath::empty()));
1272
1273        // With empty variant_path, should use Untagged parsing
1274        let result: String = child_ctx
1275            .parse_union(VariantRepr::default())
1276            .unwrap()
1277            .variant("first", String::parse)
1278            .variant("second", String::parse)
1279            .parse()
1280            .unwrap();
1281
1282        // Priority variant "first" wins in Untagged mode
1283        assert_eq!(result, "value");
1284    }
1285
1286    // =============================================================================
1287    // Nested union tests (low-level, without derive macro)
1288    // =============================================================================
1289
1290    /// Nested enum for testing: outer level
1291    #[derive(Debug, PartialEq, Clone)]
1292    enum OuterUnion {
1293        Normal(InnerUnion),
1294        List(Vec<InnerUnion>),
1295    }
1296
1297    /// Nested enum for testing: inner level
1298    #[derive(Debug, PartialEq, Clone)]
1299    enum InnerUnion {
1300        Text(String),
1301        Number(i64),
1302    }
1303
1304    fn parse_inner_union(ctx: &ParseContext<'_>) -> Result<InnerUnion, ParseError> {
1305        ctx.parse_union(VariantRepr::default())?
1306            .variant("text", |ctx: &ParseContext<'_>| {
1307                let s: String = ctx.parse()?;
1308                Ok(InnerUnion::Text(s))
1309            })
1310            .variant("number", |ctx: &ParseContext<'_>| {
1311                let n: i64 = ctx.parse()?;
1312                Ok(InnerUnion::Number(n))
1313            })
1314            .parse()
1315    }
1316
1317    fn parse_outer_union(ctx: &ParseContext<'_>) -> Result<OuterUnion, ParseError> {
1318        use crate::document::node::NodeArray;
1319
1320        ctx.parse_union(VariantRepr::default())?
1321            .variant("normal", |ctx: &ParseContext<'_>| {
1322                let inner = parse_inner_union(ctx)?;
1323                Ok(OuterUnion::Normal(inner))
1324            })
1325            .variant("list", |ctx: &ParseContext<'_>| {
1326                // Parse array of InnerUnion using NodeArray
1327                let arr: &NodeArray = ctx.parse()?;
1328                let items: Result<Vec<InnerUnion>, _> = arr
1329                    .iter()
1330                    .map(|&node_id| parse_inner_union(&ctx.at(node_id)))
1331                    .collect();
1332                Ok(OuterUnion::List(items?))
1333            })
1334            .parse()
1335    }
1336
1337    #[test]
1338    fn test_nested_union_basic_text() {
1339        // Simple string -> OuterUnion::Normal(InnerUnion::Text)
1340        let doc = eure!({ = "hello" });
1341        let root_id = doc.get_root_id();
1342        let ctx = doc.parse_context(root_id);
1343
1344        let result = parse_outer_union(&ctx).unwrap();
1345        assert_eq!(
1346            result,
1347            OuterUnion::Normal(InnerUnion::Text("hello".to_string()))
1348        );
1349    }
1350
1351    #[test]
1352    fn test_nested_union_basic_number() {
1353        let doc = eure!({ = 42 });
1354        let root_id = doc.get_root_id();
1355        let ctx = doc.parse_context(root_id);
1356        let result = parse_outer_union(&ctx).unwrap();
1357        assert_eq!(result, OuterUnion::Normal(InnerUnion::Number(42)));
1358    }
1359
1360    #[test]
1361    fn test_nested_union_variant_path_propagation() {
1362        // $variant = "normal.text" should propagate through nested unions
1363        let doc = eure!({ %variant = "normal.text", = "test value" });
1364        let root_id = doc.get_root_id();
1365        let ctx = doc.parse_context(root_id);
1366
1367        let result = parse_outer_union(&ctx).unwrap();
1368        assert_eq!(
1369            result,
1370            OuterUnion::Normal(InnerUnion::Text("test value".to_string()))
1371        );
1372    }
1373
1374    #[test]
1375    fn test_nested_union_variant_path_number() {
1376        // $variant = "normal.number" - number variant explicitly selected
1377        let doc = eure!({ %variant = "normal.number", = 99 });
1378        let root_id = doc.get_root_id();
1379        let ctx = doc.parse_context(root_id);
1380        let result = parse_outer_union(&ctx).unwrap();
1381        assert_eq!(result, OuterUnion::Normal(InnerUnion::Number(99)));
1382    }
1383
1384    #[test]
1385    fn test_nested_union_inner_fails_outer_recovers() {
1386        // When inner union fails, outer should try next variant
1387        // Create a document that doesn't match "normal" variant's inner union
1388        // but could match "list" variant
1389        let doc = eure!({ = ["a", "b"] });
1390        let root_id = doc.get_root_id();
1391        let ctx = doc.parse_context(root_id);
1392
1393        let result = parse_outer_union(&ctx).unwrap();
1394        assert_eq!(
1395            result,
1396            OuterUnion::List(alloc::vec![
1397                InnerUnion::Text("a".to_string()),
1398                InnerUnion::Text("b".to_string()),
1399            ])
1400        );
1401    }
1402
1403    // =============================================================================
1404    // Triple nested union tests
1405    // =============================================================================
1406
1407    #[derive(Debug, PartialEq, Clone)]
1408    enum Level1 {
1409        A(Level2Union),
1410        B(String),
1411    }
1412
1413    #[derive(Debug, PartialEq, Clone)]
1414    enum Level2Union {
1415        X(Level3),
1416        Y(i64),
1417    }
1418
1419    #[derive(Debug, PartialEq, Clone)]
1420    enum Level3 {
1421        Leaf(String),
1422    }
1423
1424    fn parse_level3(ctx: &ParseContext<'_>) -> Result<Level3, ParseError> {
1425        ctx.parse_union(VariantRepr::default())?
1426            .variant("leaf", |ctx: &ParseContext<'_>| {
1427                let s: String = ctx.parse()?;
1428                Ok(Level3::Leaf(s))
1429            })
1430            .parse()
1431    }
1432
1433    fn parse_level2(ctx: &ParseContext<'_>) -> Result<Level2Union, ParseError> {
1434        ctx.parse_union(VariantRepr::default())?
1435            .variant("x", |ctx: &ParseContext<'_>| {
1436                let inner = parse_level3(ctx)?;
1437                Ok(Level2Union::X(inner))
1438            })
1439            .variant("y", |ctx: &ParseContext<'_>| {
1440                let n: i64 = ctx.parse()?;
1441                Ok(Level2Union::Y(n))
1442            })
1443            .parse()
1444    }
1445
1446    fn parse_level1(ctx: &ParseContext<'_>) -> Result<Level1, ParseError> {
1447        ctx.parse_union(VariantRepr::default())?
1448            .variant("a", |ctx: &ParseContext<'_>| {
1449                let inner = parse_level2(ctx)?;
1450                Ok(Level1::A(inner))
1451            })
1452            .variant("b", |ctx: &ParseContext<'_>| {
1453                let s: String = ctx.parse()?;
1454                Ok(Level1::B(s))
1455            })
1456            .parse()
1457    }
1458
1459    #[test]
1460    fn test_nested_union_three_levels_untagged() {
1461        // String input should match: Level1::A -> Level2Union::X -> Level3::Leaf
1462        // (first variant at each level wins in untagged mode)
1463        let doc = eure!({ = "deep value" });
1464        let root_id = doc.get_root_id();
1465        let ctx = doc.parse_context(root_id);
1466
1467        let result = parse_level1(&ctx).unwrap();
1468        assert_eq!(
1469            result,
1470            Level1::A(Level2Union::X(Level3::Leaf("deep value".to_string())))
1471        );
1472    }
1473
1474    #[test]
1475    fn test_nested_union_three_levels_variant_path() {
1476        // $variant = "a.x.leaf" - explicitly select through three levels
1477        let doc = eure!({ %variant = "a.x.leaf", = "explicit deep" });
1478        let root_id = doc.get_root_id();
1479        let ctx = doc.parse_context(root_id);
1480
1481        let result = parse_level1(&ctx).unwrap();
1482        assert_eq!(
1483            result,
1484            Level1::A(Level2Union::X(Level3::Leaf("explicit deep".to_string())))
1485        );
1486    }
1487
1488    #[test]
1489    fn test_nested_union_three_levels_variant_path_partial() {
1490        // $variant = "a.y" - select a.y, inner uses untagged
1491        let doc = eure!({ %variant = "a.y", = 123 });
1492        let root_id = doc.get_root_id();
1493        let ctx = doc.parse_context(root_id);
1494        let result = parse_level1(&ctx).unwrap();
1495        assert_eq!(result, Level1::A(Level2Union::Y(123)));
1496    }
1497
1498    #[test]
1499    fn test_nested_union_invalid_inner_variant_path() {
1500        // $variant = "a.x.invalid" - "invalid" doesn't exist in Level3
1501        let doc = eure!({ %variant = "a.x.invalid", = "value" });
1502        let root_id = doc.get_root_id();
1503        let ctx = doc.parse_context(root_id);
1504
1505        let err = parse_level1(&ctx).unwrap_err();
1506        assert_eq!(
1507            err.kind,
1508            ParseErrorKind::UnknownVariant("invalid".to_string())
1509        );
1510    }
1511
1512    // =============================================================================
1513    // Flatten with nested union - accessed field tracking tests
1514    // =============================================================================
1515
1516    #[test]
1517    fn test_flatten_nested_union_accessed_fields_basic() {
1518        use crate::parse::AccessedSet;
1519        use crate::parse::FlattenContext;
1520        use crate::parse::ParserScope;
1521
1522        // Test that accessed fields are properly tracked through nested unions
1523        let doc = eure!({
1524            field_a = "value_a"
1525            field_b = "value_b"
1526        });
1527        let root_id = doc.get_root_id();
1528
1529        // Create flatten context to track field access
1530        let flatten_ctx = FlattenContext::new(AccessedSet::new(), ParserScope::Record);
1531        let ctx =
1532            ParseContext::with_flatten_ctx(&doc, root_id, flatten_ctx.clone(), UnionTagMode::Eure);
1533
1534        // Parse a union that accesses field_a
1535        let record = ctx.parse_record().unwrap();
1536        let _field_a: String = record.parse_field("field_a").unwrap();
1537
1538        // field_a should be marked as accessed
1539        let (accessed, _) = flatten_ctx.capture_current_state();
1540        assert!(accessed.contains("field_a"));
1541        assert!(!accessed.contains("field_b"));
1542    }
1543}