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::boxed::Box;
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10
11use crate::document::{EureDocument, NodeId};
12use crate::identifier::Identifier;
13use crate::parse::{DocumentParser, FromEure};
14
15use super::variant_path::VariantPath;
16use super::{
17    AccessedSet, AccessedSnapshot, BestParseVariantMatch, ParseContext, ParseError, ParseErrorKind,
18    UnionParseError,
19};
20
21/// The `$variant` extension identifier.
22pub const VARIANT: Identifier = Identifier::new_unchecked("variant");
23
24/// Extract `$variant` extension as a parsed [`VariantPath`], if present.
25pub fn extract_explicit_variant_path(
26    doc: &EureDocument,
27    node_id: NodeId,
28) -> Result<Option<VariantPath>, ParseError> {
29    let node = doc.node(node_id);
30    let Some(&variant_node_id) = node.extensions.get(&VARIANT) else {
31        return Ok(None);
32    };
33
34    let variant_node = doc.node(variant_node_id);
35    let s: &str = doc.parse(variant_node_id).map_err(|_| ParseError {
36        node_id: variant_node_id,
37        kind: ParseErrorKind::InvalidVariantType(variant_node.content.value_kind()),
38    })?;
39
40    VariantPath::parse(s).map(Some).map_err(|_| ParseError {
41        node_id: variant_node_id,
42        kind: ParseErrorKind::InvalidVariantPath(s.to_string()),
43    })
44}
45
46/// Returns whether this node has any explicit union tag information.
47///
48pub fn has_explicit_variant_tag(doc: &EureDocument, node_id: NodeId) -> Result<bool, ParseError> {
49    Ok(extract_explicit_variant_path(doc, node_id)?.is_some())
50}
51
52// =============================================================================
53// UnionParser
54// =============================================================================
55
56/// Helper for parsing union types from Eure documents.
57///
58/// Implements oneOf semantics:
59/// - Exactly one variant must match
60/// - Multiple matches resolved by registration order (priority)
61/// - Short-circuits on first priority variant match
62/// - When `$variant` extension is specified, matches by name directly
63///
64/// # Variant Resolution
65///
66/// Variant is determined by `$variant` extension when present.
67/// Without `$variant`, the parser falls back to untagged matching.
68///
69/// # Example
70///
71/// ```ignore
72/// impl<'doc> FromEure<'doc> for Description {
73///     fn parse(ctx: &ParseContext<'doc>) -> Result<Self, ParseError> {
74///         ctx.parse_union()?
75///             .variant("string", |ctx| {
76///                 let text: String = ctx.parse()?;
77///                 Ok(Description::String(text))
78///             })
79///             .variant("markdown", |ctx| {
80///                 let text: String = ctx.parse()?;
81///                 Ok(Description::Markdown(text))
82///             })
83///             .parse()
84///     }
85/// }
86/// ```
87pub struct UnionParser<'doc, 'ctx, T, E = ParseError> {
88    ctx: &'ctx ParseContext<'doc>,
89    /// Unified variant: (name, context, rest_path)
90    /// - name: variant name to match
91    /// - context: ParseContext for the variant content
92    /// - rest_path: remaining variant path for nested unions
93    variant: Option<(String, ParseContext<'doc>, Option<VariantPath>)>,
94    /// Result when variant matches
95    variant_result: Option<Result<T, E>>,
96    /// First matching priority variant (short-circuit result)
97    priority_result: Option<T>,
98    /// Matching non-priority variants, with their captured accessed state.
99    /// The AccessedSnapshot is captured after successful parse, before restoring.
100    other_results: Vec<(String, T, AccessedSnapshot)>,
101    /// Failed variants (for error reporting)
102    failures: Vec<(String, E)>,
103    /// Access tracking with snapshot/rollback support for variant trials.
104    accessed: AccessedSet,
105}
106
107impl<'doc, 'ctx, T, E> UnionParser<'doc, 'ctx, T, E>
108where
109    E: UnionParseError,
110{
111    /// Create a new UnionParser for the given context.
112    ///
113    /// Returns error if `$variant` extension has invalid type or syntax.
114    pub(crate) fn new(ctx: &'ctx ParseContext<'doc>) -> Result<Self, ParseError> {
115        let variant = Self::resolve_variant(ctx)?;
116        let accessed = ctx.accessed().clone();
117        accessed.push_snapshot();
118
119        Ok(Self {
120            ctx,
121            variant,
122            variant_result: None,
123            priority_result: None,
124            other_results: Vec::new(),
125            failures: Vec::new(),
126            accessed,
127        })
128    }
129
130    /// Resolve the unified variant from `$variant` extension.
131    ///
132    /// Returns:
133    /// - `Some((name, ctx, rest))` if variant is determined
134    /// - `None` for Untagged parsing
135    ///
136    fn resolve_variant(
137        ctx: &ParseContext<'doc>,
138    ) -> Result<Option<(String, ParseContext<'doc>, Option<VariantPath>)>, ParseError> {
139        // Check if variant path is already set in context (from parent union)
140        let explicit_variant = match ctx.variant_path() {
141            Some(vp) if !vp.is_empty() => Some(vp.clone()),
142            Some(_) => None, // Empty path = variant consumed, use Untagged
143            None => {
144                let variant = Self::extract_explicit_variant(ctx)?;
145                if variant.is_some() {
146                    // Mark $variant extension as accessed so deny_unknown_extensions() won't fail
147                    ctx.accessed().add_ext(VARIANT.clone());
148                }
149                variant
150            }
151        };
152
153        match explicit_variant {
154            // $variant present → use original context
155            Some(ev) => {
156                let name = ev
157                    .first()
158                    .map(|i| i.as_ref().to_string())
159                    .unwrap_or_default();
160                let rest = ev.rest().unwrap_or_else(VariantPath::empty);
161                Ok(Some((name, ctx.clone(), Some(rest))))
162            }
163            // No $variant → Untagged
164            None => Ok(None),
165        }
166    }
167
168    /// Extract the `$variant` extension value from the node.
169    fn extract_explicit_variant(
170        ctx: &ParseContext<'doc>,
171    ) -> Result<Option<VariantPath>, ParseError> {
172        extract_explicit_variant_path(ctx.doc(), ctx.node_id())
173    }
174
175    /// Register a variant with short-circuit semantics (default).
176    ///
177    /// When this variant matches in untagged mode, parsing succeeds immediately
178    /// without checking other variants. Use definition order to express priority.
179    pub fn variant<P: DocumentParser<'doc, Output = T, Error = E>>(
180        mut self,
181        name: &str,
182        f: P,
183    ) -> Self {
184        self.try_variant(name, f, true);
185        self
186    }
187
188    /// Register a variant with short-circuit semantics using FromEure.
189    pub fn parse_variant<V: FromEure<'doc, Error = E>>(
190        mut self,
191        name: &str,
192        mut then: impl FnMut(V) -> Result<T, E>,
193    ) -> Self {
194        self.try_variant(
195            name,
196            move |ctx: &ParseContext<'doc>| {
197                let v = V::parse(ctx)?;
198                then(v)
199            },
200            true,
201        );
202        self
203    }
204
205    /// Register a variant with unambiguous semantics.
206    ///
207    /// All unambiguous variants are tried to detect conflicts.
208    /// If multiple unambiguous variants match, an AmbiguousUnion error is returned.
209    /// Use for catch-all variants or when you need conflict detection.
210    pub fn variant_unambiguous<P: DocumentParser<'doc, Output = T, Error = E>>(
211        mut self,
212        name: &str,
213        f: P,
214    ) -> Self {
215        self.try_variant(name, f, false);
216        self
217    }
218
219    /// Register a variant with unambiguous semantics using FromEure.
220    pub fn parse_variant_unambiguous<V: FromEure<'doc, Error = E>>(
221        mut self,
222        name: &str,
223        mut then: impl FnMut(V) -> Result<T, E>,
224    ) -> Self {
225        self.try_variant(
226            name,
227            move |ctx: &ParseContext<'doc>| {
228                let v = V::parse(ctx)?;
229                then(v)
230            },
231            false,
232        );
233        self
234    }
235
236    /// Internal helper for variant/other logic.
237    fn try_variant<P: DocumentParser<'doc, Output = T, Error = E>>(
238        &mut self,
239        name: &str,
240        mut f: P,
241        is_priority: bool,
242    ) {
243        // 1. If variant is determined, only try matching variant
244        if let Some((ref v_name, ref v_ctx, ref rest)) = self.variant {
245            if v_name == name && self.variant_result.is_none() {
246                let child_ctx = v_ctx.with_variant_rest(rest.clone());
247                let result = f.parse(&child_ctx);
248                self.variant_result = Some(result);
249            }
250            return;
251        }
252
253        // 2. Untagged mode: try all variants
254
255        // Skip if already have priority result
256        if self.priority_result.is_some() {
257            return;
258        }
259
260        let child_ctx = self.ctx.with_variant_rest(None);
261        match f.parse(&child_ctx) {
262            Ok(value) => {
263                if is_priority {
264                    self.priority_result = Some(value);
265                } else {
266                    let captured = self.accessed.capture_current_state();
267                    self.accessed.restore_to_current_snapshot();
268                    self.other_results.push((name.to_string(), value, captured));
269                }
270            }
271            Err(e) => {
272                self.accessed.restore_to_current_snapshot();
273                self.failures.push((name.to_string(), e));
274            }
275        }
276    }
277
278    /// Execute the union parse with oneOf semantics.
279    pub fn parse(self) -> Result<T, E> {
280        let node_id = self.ctx.node_id();
281
282        // 1. Variant determined - return its result
283        if let Some((v_name, _, _)) = self.variant {
284            let result = self.variant_result.unwrap_or_else(|| {
285                Err(ParseError {
286                    node_id,
287                    kind: ParseErrorKind::UnknownVariant(v_name),
288                }
289                .into())
290            });
291            match &result {
292                Ok(_) => self.accessed.pop_without_restore(),
293                Err(_) => self.accessed.pop_and_restore(),
294            }
295            return result;
296        }
297
298        // 2. Priority result - success, keep changes
299        if let Some(value) = self.priority_result {
300            self.accessed.pop_without_restore();
301            return Ok(value);
302        }
303
304        // 3. Check other_results
305        match self.other_results.len() {
306            0 => {
307                self.accessed.pop_and_restore();
308                Err(self.no_match_error(node_id))
309            }
310            1 => {
311                let (_, value, captured_state) = self.other_results.into_iter().next().unwrap();
312                self.accessed.restore_to_state(captured_state);
313                self.accessed.pop_without_restore();
314                Ok(value)
315            }
316            _ => {
317                self.accessed.pop_and_restore();
318                Err(ParseError {
319                    node_id,
320                    kind: ParseErrorKind::AmbiguousUnion(
321                        self.other_results
322                            .into_iter()
323                            .map(|(name, _, _)| name)
324                            .collect(),
325                    ),
326                }
327                .into())
328            }
329        }
330    }
331
332    /// Create an error for when no variant matches.
333    fn no_match_error(self, node_id: crate::document::NodeId) -> E {
334        E::from_no_matching_variant(
335            node_id,
336            None,
337            select_best_parse_variant_match(&self.failures),
338            &self.failures,
339        )
340    }
341}
342
343fn select_best_parse_variant_match<E>(failures: &[(String, E)]) -> Option<BestParseVariantMatch>
344where
345    E: UnionParseError,
346{
347    failures
348        .iter()
349        .filter_map(|(variant_name, error)| {
350            error
351                .as_parse_error()
352                .map(|parse_error| (variant_name, parse_error))
353        })
354        .max_by_key(|(_, parse_error)| parse_error_match_metrics(parse_error))
355        .map(|(variant_name, parse_error)| BestParseVariantMatch {
356            variant_name: variant_name.clone(),
357            error: Box::new(parse_error.clone()),
358        })
359}
360
361fn parse_error_match_metrics(error: &ParseError) -> (bool, usize, u8) {
362    parse_error_kind_metrics(&error.kind)
363}
364
365fn parse_error_kind_metrics(kind: &ParseErrorKind) -> (bool, usize, u8) {
366    match kind {
367        ParseErrorKind::Nested { source, .. } => {
368            let (structural, depth, priority) = parse_error_kind_metrics(source);
369            (structural, depth + 1, priority)
370        }
371        ParseErrorKind::NoMatchingVariant {
372            best_match: Some(best),
373            ..
374        } => {
375            let (structural, depth, priority) = parse_error_match_metrics(&best.error);
376            (structural, depth + 1, priority)
377        }
378        _ => (
379            is_structural_parse_mismatch(kind),
380            1,
381            parse_error_priority(kind),
382        ),
383    }
384}
385
386fn is_structural_parse_mismatch(kind: &ParseErrorKind) -> bool {
387    matches!(
388        kind,
389        ParseErrorKind::MissingField(_)
390            | ParseErrorKind::MissingExtension(_)
391            | ParseErrorKind::UnknownField(_)
392            | ParseErrorKind::UnknownExtension(_)
393            | ParseErrorKind::LiteralMismatch { .. }
394            | ParseErrorKind::InvalidPattern { .. }
395    )
396}
397
398fn parse_error_priority(kind: &ParseErrorKind) -> u8 {
399    match kind {
400        ParseErrorKind::UnknownField(_) | ParseErrorKind::UnknownExtension(_) => 4,
401        ParseErrorKind::MissingField(_) | ParseErrorKind::MissingExtension(_) => 3,
402        ParseErrorKind::UnknownVariant(_) | ParseErrorKind::UnexpectedVariantPath(_) => 2,
403        ParseErrorKind::LiteralMismatch { .. } | ParseErrorKind::InvalidPattern { .. } => 2,
404        ParseErrorKind::TypeMismatch { .. }
405        | ParseErrorKind::UnexpectedTupleLength { .. }
406        | ParseErrorKind::UnexpectedArrayLength { .. }
407        | ParseErrorKind::NotPrimitive { .. } => 1,
408        _ => 0,
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use crate::eure;
416    use crate::parse::AlwaysParser;
417    use crate::parse::DocumentParserExt as _;
418
419    #[derive(Debug, PartialEq, Clone)]
420    enum TestEnum {
421        Foo,
422        Bar,
423    }
424
425    #[test]
426    fn test_union_single_match() {
427        let doc = eure!({ = "foo" });
428        let root_id = doc.get_root_id();
429        let ctx = doc.parse_context(root_id);
430
431        let result: TestEnum = ctx
432            .parse_union()
433            .unwrap()
434            .variant("foo", |ctx: &ParseContext<'_>| {
435                let s: &str = ctx.parse()?;
436                if s == "foo" {
437                    Ok(TestEnum::Foo)
438                } else {
439                    Err(ParseError {
440                        node_id: ctx.node_id(),
441                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
442                    })
443                }
444            })
445            .variant("bar", |ctx: &ParseContext<'_>| {
446                let s: &str = ctx.parse()?;
447                if s == "bar" {
448                    Ok(TestEnum::Bar)
449                } else {
450                    Err(ParseError {
451                        node_id: ctx.node_id(),
452                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
453                    })
454                }
455            })
456            .parse()
457            .unwrap();
458
459        assert_eq!(result, TestEnum::Foo);
460    }
461
462    #[test]
463    fn test_union_priority_short_circuit() {
464        let doc = eure!({ = "value" });
465        let root_id = doc.get_root_id();
466        let ctx = doc.parse_context(root_id);
467
468        // Both variants would match, but first one wins due to priority
469        let result: String = ctx
470            .parse_union()
471            .unwrap()
472            .variant("first", String::parse)
473            .variant("second", String::parse)
474            .parse()
475            .unwrap();
476
477        assert_eq!(result, "value");
478    }
479
480    #[test]
481    fn test_union_no_match() {
482        let doc = eure!({ = "baz" });
483        let root_id = doc.get_root_id();
484        let ctx = doc.parse_context(root_id);
485
486        let result: Result<TestEnum, ParseError> = ctx
487            .parse_union()
488            .unwrap()
489            .variant("foo", |ctx: &ParseContext<'_>| {
490                let s: &str = ctx.parse()?;
491                if s == "foo" {
492                    Ok(TestEnum::Foo)
493                } else {
494                    Err(ParseError {
495                        node_id: ctx.node_id(),
496                        kind: ParseErrorKind::UnknownVariant(s.to_string()),
497                    })
498                }
499            })
500            .parse();
501
502        assert!(result.is_err());
503    }
504
505    // --- $variant extension tests ---
506
507    #[test]
508    fn test_variant_extension_match_success() {
509        // $variant = "baz" specified, matches other("baz")
510        // All parsers always succeed
511        let doc = eure!({ %variant = "baz", = "anything" });
512        let root_id = doc.get_root_id();
513        let ctx = doc.parse_context(root_id);
514
515        let result: TestEnum = ctx
516            .parse_union()
517            .unwrap()
518            .variant(
519                "foo",
520                AlwaysParser::<TestEnum, ParseError>::new(TestEnum::Foo),
521            )
522            .variant_unambiguous("baz", AlwaysParser::new(TestEnum::Bar))
523            .parse()
524            .unwrap();
525
526        assert_eq!(result, TestEnum::Bar);
527    }
528
529    #[test]
530    fn test_variant_extension_unknown() {
531        // $variant = "unknown" specified, but "unknown" is not registered
532        // All parsers always succeed
533        let doc = eure!({ %variant = "unknown", = "anything" });
534        let root_id = doc.get_root_id();
535        let ctx = doc.parse_context(root_id);
536
537        let err: ParseError = ctx
538            .parse_union()
539            .unwrap()
540            .variant("foo", AlwaysParser::new(TestEnum::Foo))
541            .variant_unambiguous("baz", AlwaysParser::new(TestEnum::Bar))
542            .parse()
543            .unwrap_err();
544
545        assert_eq!(err.node_id, root_id);
546        assert_eq!(
547            err.kind,
548            ParseErrorKind::UnknownVariant("unknown".to_string())
549        );
550    }
551
552    #[test]
553    fn test_variant_extension_match_parse_failure() {
554        // $variant = "baz" specified, "baz" parser fails
555        let doc = eure!({ %variant = "baz", = "anything" });
556        let root_id = doc.get_root_id();
557        let ctx = doc.parse_context(root_id);
558
559        let err = ctx
560            .parse_union()
561            .unwrap()
562            .variant("foo", AlwaysParser::new(TestEnum::Foo))
563            .variant_unambiguous("baz", |ctx: &ParseContext<'_>| {
564                Err(ParseError {
565                    node_id: ctx.node_id(),
566                    kind: ParseErrorKind::MissingField("test".to_string()),
567                })
568            })
569            .parse()
570            .unwrap_err();
571
572        // Parser's error is returned directly
573        assert_eq!(err.node_id, root_id);
574        assert_eq!(err.kind, ParseErrorKind::MissingField("test".to_string()));
575    }
576
577    #[test]
578    fn test_union_rolls_back_accessed_fields_between_untagged_variants() {
579        let doc = eure!({ age = 42 });
580        let root_id = doc.get_root_id();
581        let ctx = doc.parse_context(root_id);
582
583        let err = ctx
584            .parse_union()
585            .unwrap()
586            .variant("full", |ctx: &ParseContext<'_>| {
587                let rec = ctx.parse_record()?;
588                let _: Option<i64> = rec.parse_field_optional("age")?;
589                let name: Option<String> = rec.parse_field_optional("name")?;
590                rec.deny_unknown_fields()?;
591                if name.is_none() {
592                    return Err(ParseError {
593                        node_id: ctx.node_id(),
594                        kind: ParseErrorKind::MissingField("name".to_string()),
595                    });
596                }
597                Ok(TestEnum::Foo)
598            })
599            .variant("minimal", |ctx: &ParseContext<'_>| {
600                let rec = ctx.parse_record()?;
601                let unknown_fields: Vec<_> = rec
602                    .unknown_fields()
603                    .filter_map(|result| match result {
604                        Ok((field_name, _)) => Some(field_name.to_string()),
605                        Err(_) => None,
606                    })
607                    .collect();
608                if unknown_fields.iter().any(|field| field == "age") {
609                    return Err(ParseError {
610                        node_id: ctx.node_id(),
611                        kind: ParseErrorKind::UnknownField("age".to_string()),
612                    });
613                }
614                let name: Option<String> = rec.parse_field_optional("name")?;
615                rec.deny_unknown_fields()?;
616                if name.is_none() {
617                    return Err(ParseError {
618                        node_id: ctx.node_id(),
619                        kind: ParseErrorKind::MissingField("name".to_string()),
620                    });
621                }
622                Ok(TestEnum::Bar)
623            })
624            .parse()
625            .unwrap_err();
626
627        let ParseErrorKind::NoMatchingVariant {
628            best_match: Some(best_match),
629            ..
630        } = err.kind
631        else {
632            panic!("expected no matching variant with best match, got {err:?}");
633        };
634        assert_eq!(best_match.variant_name, "minimal");
635        assert_eq!(
636            best_match.error.kind,
637            ParseErrorKind::UnknownField("age".to_string())
638        );
639    }
640
641    // --- nested variant tests ---
642
643    #[derive(Debug, PartialEq, Clone)]
644    enum Outer {
645        A(Inner),
646        B(i32),
647    }
648
649    #[derive(Debug, PartialEq, Clone)]
650    enum Inner {
651        X,
652        Y,
653    }
654
655    fn parse_inner(ctx: &ParseContext<'_>) -> Result<Inner, ParseError> {
656        ctx.parse_union()
657            .unwrap()
658            .variant("x", AlwaysParser::new(Inner::X))
659            .variant("y", AlwaysParser::new(Inner::Y))
660            .parse()
661    }
662
663    #[test]
664    fn test_variant_nested_single_segment() {
665        // $variant = "a" - matches "a", rest is None -> Inner defaults to X
666        let doc = eure!({ %variant = "a", = "value" });
667        let root_id = doc.get_root_id();
668        let ctx = doc.parse_context(root_id);
669
670        let result: Outer = ctx
671            .parse_union()
672            .unwrap()
673            .variant("a", parse_inner.map(Outer::A))
674            .variant("b", AlwaysParser::new(Outer::B(42)))
675            .parse()
676            .unwrap();
677
678        assert_eq!(result, Outer::A(Inner::X));
679    }
680
681    #[test]
682    fn test_variant_nested_multi_segment() {
683        // $variant = "a.y" - matches "a", rest is Some("y")
684        let doc = eure!({ %variant = "a.y", = "value" });
685        let root_id = doc.get_root_id();
686        let ctx = doc.parse_context(root_id);
687
688        let result: Outer = ctx
689            .parse_union()
690            .unwrap()
691            .variant("a", parse_inner.map(Outer::A))
692            .variant("b", AlwaysParser::new(Outer::B(42)))
693            .parse()
694            .unwrap();
695
696        assert_eq!(result, Outer::A(Inner::Y));
697    }
698
699    #[test]
700    fn test_variant_nested_invalid_inner() {
701        // $variant = "a.z" - matches "a", but "z" is not valid for Inner
702        let doc = eure!({ %variant = "a.z", = "value" });
703        let root_id = doc.get_root_id();
704        let ctx = doc.parse_context(root_id);
705
706        let err = ctx
707            .parse_union()
708            .unwrap()
709            .variant("a", parse_inner.map(Outer::A))
710            .variant("b", AlwaysParser::new(Outer::B(42)))
711            .parse()
712            .unwrap_err();
713
714        assert_eq!(err.kind, ParseErrorKind::UnknownVariant("z".to_string()));
715    }
716
717    #[test]
718    fn test_variant_non_nested_with_nested_path() {
719        // $variant = "b.x" but "b" parser doesn't expect nested path
720        // The child context will have variant_path = Some("x")
721        // If the "b" parser is a non-union type, it should error on unexpected variant path
722        let doc = eure!({ %variant = "b.x", = "value" });
723        let root_id = doc.get_root_id();
724        let ctx = doc.parse_context(root_id);
725
726        // "b" is registered as a variant but if called with "b.x",
727        // the closure gets ctx with variant_path = Some("x")
728        // The simple parser Ok(Outer::B(42)) doesn't check variant path,
729        // but a proper impl would use ctx.parse_primitive() which errors
730        let err = ctx
731            .parse_union()
732            .unwrap()
733            .variant("a", parse_inner.map(Outer::A))
734            .variant("b", |ctx: &ParseContext<'_>| {
735                // Simulate parsing a primitive that checks variant path
736                ctx.parse_primitive()?;
737                Ok(Outer::B(42))
738            })
739            .parse()
740            .unwrap_err();
741
742        // parse_primitive should error because variant path "x" remains
743        assert!(matches!(err.kind, ParseErrorKind::UnexpectedVariantPath(_)));
744    }
745
746    // --- invalid $variant tests ---
747
748    use crate::value::ValueKind;
749
750    #[test]
751    fn test_invalid_variant_type_errors() {
752        // $variant = 123 (integer, not string) - should error at parse_union()
753        // Note: eure! macro can't create invalid $variant types, so we use manual construction
754        use crate::document::node::NodeValue;
755        use crate::value::PrimitiveValue;
756        use num_bigint::BigInt;
757
758        // Create base doc with eure! and then add invalid integer $variant
759        let mut doc = eure!({ = "foo" });
760        let root_id = doc.get_root_id();
761        let variant_node_id = doc
762            .add_extension("variant".parse().unwrap(), root_id)
763            .unwrap()
764            .node_id;
765        doc.node_mut(variant_node_id).content =
766            NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(123)));
767
768        let ctx = doc.parse_context(root_id);
769
770        let Err(err) = ctx.parse_union::<TestEnum, ParseError>() else {
771            panic!("Expected error");
772        };
773        assert_eq!(
774            err,
775            ParseError {
776                node_id: variant_node_id,
777                kind: ParseErrorKind::InvalidVariantType(ValueKind::Integer),
778            }
779        );
780    }
781
782    #[test]
783    fn test_invalid_variant_path_syntax_errors() {
784        // $variant = "foo..bar" (invalid path syntax) - should error at parse_union()
785        let doc = eure!({ %variant = "foo..bar", = "foo" });
786        let root_id = doc.get_root_id();
787        let variant_node_id = *doc.node(root_id).extensions.get(&VARIANT).unwrap();
788        let ctx = doc.parse_context(root_id);
789
790        let Err(err) = ctx.parse_union::<TestEnum, ParseError>() else {
791            panic!("Expected error");
792        };
793        assert_eq!(
794            err,
795            ParseError {
796                node_id: variant_node_id,
797                kind: ParseErrorKind::InvalidVariantPath("foo..bar".to_string()),
798            }
799        );
800    }
801
802    #[test]
803    fn test_variant_path_empty_uses_untagged() {
804        // When variant_path is Some but empty (consumed by parent), use Untagged
805        // This is tested indirectly through nested unions after consuming the path
806        let doc = eure!({ = "value" });
807        let root_id = doc.get_root_id();
808        let ctx = doc.parse_context(root_id);
809
810        // Simulate a context where variant_path was set but is now empty
811        let child_ctx = ctx.with_variant_rest(Some(VariantPath::empty()));
812
813        // With empty variant_path, should use Untagged parsing
814        let result: String = child_ctx
815            .parse_union()
816            .unwrap()
817            .variant("first", String::parse)
818            .variant("second", String::parse)
819            .parse()
820            .unwrap();
821
822        // Priority variant "first" wins in Untagged mode
823        assert_eq!(result, "value");
824    }
825
826    // =============================================================================
827    // Nested union tests (low-level, without derive macro)
828    // =============================================================================
829
830    /// Nested enum for testing: outer level
831    #[derive(Debug, PartialEq, Clone)]
832    enum OuterUnion {
833        Normal(InnerUnion),
834        List(Vec<InnerUnion>),
835    }
836
837    /// Nested enum for testing: inner level
838    #[derive(Debug, PartialEq, Clone)]
839    enum InnerUnion {
840        Text(String),
841        Number(i64),
842    }
843
844    fn parse_inner_union(ctx: &ParseContext<'_>) -> Result<InnerUnion, ParseError> {
845        ctx.parse_union()?
846            .variant("text", |ctx: &ParseContext<'_>| {
847                let s: String = ctx.parse()?;
848                Ok(InnerUnion::Text(s))
849            })
850            .variant("number", |ctx: &ParseContext<'_>| {
851                let n: i64 = ctx.parse()?;
852                Ok(InnerUnion::Number(n))
853            })
854            .parse()
855    }
856
857    fn parse_outer_union(ctx: &ParseContext<'_>) -> Result<OuterUnion, ParseError> {
858        use crate::document::node::NodeArray;
859
860        ctx.parse_union()?
861            .variant("normal", |ctx: &ParseContext<'_>| {
862                let inner = parse_inner_union(ctx)?;
863                Ok(OuterUnion::Normal(inner))
864            })
865            .variant("list", |ctx: &ParseContext<'_>| {
866                // Parse array of InnerUnion using NodeArray
867                let arr: &NodeArray = ctx.parse()?;
868                let items: Result<Vec<InnerUnion>, _> = arr
869                    .iter()
870                    .map(|&node_id| parse_inner_union(&ctx.at(node_id)))
871                    .collect();
872                Ok(OuterUnion::List(items?))
873            })
874            .parse()
875    }
876
877    #[test]
878    fn test_nested_union_basic_text() {
879        // Simple string -> OuterUnion::Normal(InnerUnion::Text)
880        let doc = eure!({ = "hello" });
881        let root_id = doc.get_root_id();
882        let ctx = doc.parse_context(root_id);
883
884        let result = parse_outer_union(&ctx).unwrap();
885        assert_eq!(
886            result,
887            OuterUnion::Normal(InnerUnion::Text("hello".to_string()))
888        );
889    }
890
891    #[test]
892    fn test_nested_union_basic_number() {
893        let doc = eure!({ = 42 });
894        let root_id = doc.get_root_id();
895        let ctx = doc.parse_context(root_id);
896        let result = parse_outer_union(&ctx).unwrap();
897        assert_eq!(result, OuterUnion::Normal(InnerUnion::Number(42)));
898    }
899
900    #[test]
901    fn test_nested_union_variant_path_propagation() {
902        // $variant = "normal.text" should propagate through nested unions
903        let doc = eure!({ %variant = "normal.text", = "test value" });
904        let root_id = doc.get_root_id();
905        let ctx = doc.parse_context(root_id);
906
907        let result = parse_outer_union(&ctx).unwrap();
908        assert_eq!(
909            result,
910            OuterUnion::Normal(InnerUnion::Text("test value".to_string()))
911        );
912    }
913
914    #[test]
915    fn test_nested_union_variant_path_number() {
916        // $variant = "normal.number" - number variant explicitly selected
917        let doc = eure!({ %variant = "normal.number", = 99 });
918        let root_id = doc.get_root_id();
919        let ctx = doc.parse_context(root_id);
920        let result = parse_outer_union(&ctx).unwrap();
921        assert_eq!(result, OuterUnion::Normal(InnerUnion::Number(99)));
922    }
923
924    #[test]
925    fn test_nested_union_inner_fails_outer_recovers() {
926        // When inner union fails, outer should try next variant
927        // Create a document that doesn't match "normal" variant's inner union
928        // but could match "list" variant
929        let doc = eure!({ = ["a", "b"] });
930        let root_id = doc.get_root_id();
931        let ctx = doc.parse_context(root_id);
932
933        let result = parse_outer_union(&ctx).unwrap();
934        assert_eq!(
935            result,
936            OuterUnion::List(alloc::vec![
937                InnerUnion::Text("a".to_string()),
938                InnerUnion::Text("b".to_string()),
939            ])
940        );
941    }
942
943    // =============================================================================
944    // Triple nested union tests
945    // =============================================================================
946
947    #[derive(Debug, PartialEq, Clone)]
948    enum Level1 {
949        A(Level2Union),
950        B(String),
951    }
952
953    #[derive(Debug, PartialEq, Clone)]
954    enum Level2Union {
955        X(Level3),
956        Y(i64),
957    }
958
959    #[derive(Debug, PartialEq, Clone)]
960    enum Level3 {
961        Leaf(String),
962    }
963
964    fn parse_level3(ctx: &ParseContext<'_>) -> Result<Level3, ParseError> {
965        ctx.parse_union()?
966            .variant("leaf", |ctx: &ParseContext<'_>| {
967                let s: String = ctx.parse()?;
968                Ok(Level3::Leaf(s))
969            })
970            .parse()
971    }
972
973    fn parse_level2(ctx: &ParseContext<'_>) -> Result<Level2Union, ParseError> {
974        ctx.parse_union()?
975            .variant("x", |ctx: &ParseContext<'_>| {
976                let inner = parse_level3(ctx)?;
977                Ok(Level2Union::X(inner))
978            })
979            .variant("y", |ctx: &ParseContext<'_>| {
980                let n: i64 = ctx.parse()?;
981                Ok(Level2Union::Y(n))
982            })
983            .parse()
984    }
985
986    fn parse_level1(ctx: &ParseContext<'_>) -> Result<Level1, ParseError> {
987        ctx.parse_union()?
988            .variant("a", |ctx: &ParseContext<'_>| {
989                let inner = parse_level2(ctx)?;
990                Ok(Level1::A(inner))
991            })
992            .variant("b", |ctx: &ParseContext<'_>| {
993                let s: String = ctx.parse()?;
994                Ok(Level1::B(s))
995            })
996            .parse()
997    }
998
999    #[test]
1000    fn test_nested_union_three_levels_untagged() {
1001        // String input should match: Level1::A -> Level2Union::X -> Level3::Leaf
1002        // (first variant at each level wins in untagged mode)
1003        let doc = eure!({ = "deep value" });
1004        let root_id = doc.get_root_id();
1005        let ctx = doc.parse_context(root_id);
1006
1007        let result = parse_level1(&ctx).unwrap();
1008        assert_eq!(
1009            result,
1010            Level1::A(Level2Union::X(Level3::Leaf("deep value".to_string())))
1011        );
1012    }
1013
1014    #[test]
1015    fn test_nested_union_three_levels_variant_path() {
1016        // $variant = "a.x.leaf" - explicitly select through three levels
1017        let doc = eure!({ %variant = "a.x.leaf", = "explicit deep" });
1018        let root_id = doc.get_root_id();
1019        let ctx = doc.parse_context(root_id);
1020
1021        let result = parse_level1(&ctx).unwrap();
1022        assert_eq!(
1023            result,
1024            Level1::A(Level2Union::X(Level3::Leaf("explicit deep".to_string())))
1025        );
1026    }
1027
1028    #[test]
1029    fn test_nested_union_three_levels_variant_path_partial() {
1030        // $variant = "a.y" - select a.y, inner uses untagged
1031        let doc = eure!({ %variant = "a.y", = 123 });
1032        let root_id = doc.get_root_id();
1033        let ctx = doc.parse_context(root_id);
1034        let result = parse_level1(&ctx).unwrap();
1035        assert_eq!(result, Level1::A(Level2Union::Y(123)));
1036    }
1037
1038    #[test]
1039    fn test_nested_union_invalid_inner_variant_path() {
1040        // $variant = "a.x.invalid" - "invalid" doesn't exist in Level3
1041        let doc = eure!({ %variant = "a.x.invalid", = "value" });
1042        let root_id = doc.get_root_id();
1043        let ctx = doc.parse_context(root_id);
1044
1045        let err = parse_level1(&ctx).unwrap_err();
1046        assert_eq!(
1047            err.kind,
1048            ParseErrorKind::UnknownVariant("invalid".to_string())
1049        );
1050    }
1051
1052    // =============================================================================
1053    // Flatten with nested union - accessed field tracking tests
1054    // =============================================================================
1055
1056    #[test]
1057    fn test_flatten_nested_union_accessed_fields_basic() {
1058        use crate::parse::AccessedSet;
1059        use crate::parse::FlattenContext;
1060        use crate::parse::ParserScope;
1061
1062        // Test that accessed fields are properly tracked through nested unions
1063        let doc = eure!({
1064            field_a = "value_a"
1065            field_b = "value_b"
1066        });
1067        let root_id = doc.get_root_id();
1068
1069        // Create flatten context to track field access
1070        let flatten_ctx = FlattenContext::new(AccessedSet::new(), ParserScope::Record);
1071        let ctx = ParseContext::with_flatten_ctx(&doc, root_id, flatten_ctx.clone());
1072
1073        // Parse a union that accesses field_a
1074        let record = ctx.parse_record().unwrap();
1075        let _field_a: String = record.parse_field("field_a").unwrap();
1076
1077        // field_a should be marked as accessed
1078        let (accessed, _) = flatten_ctx.capture_current_state();
1079        assert!(accessed.contains("field_a"));
1080        assert!(!accessed.contains("field_b"));
1081    }
1082}