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