Skip to main content

eure_document/parse/
record.rs

1//! RecordParser for parsing record types from Eure documents.
2
3use crate::parse::DocumentParser;
4use crate::prelude_internal::*;
5
6use super::{FromEure, ParseContext, ParseError, ParseErrorKind, ParserScope};
7
8/// Helper for parsing record (map with string keys) from Eure documents.
9///
10/// Tracks accessed fields for unknown field checking.
11///
12/// # Flatten Context
13///
14/// When `flatten_ctx` is `Some`, this parser is part of a flattened chain:
15/// - Field accesses are recorded in the shared `FlattenContext`
16/// - `deny_unknown_fields()` is a no-op (root parser validates)
17///
18/// When `flatten_ctx` is `None`, this is a root parser:
19/// - Field accesses are recorded in local `accessed` set
20/// - `deny_unknown_fields()` actually validates
21///
22/// # Example
23///
24/// ```ignore
25/// impl<'doc> FromEure<'doc> for User {
26///     fn parse(ctx: &ParseContext<'doc>) -> Result<Self, ParseError> {
27///         let mut rec = ctx.parse_record()?;
28///         let name = rec.field::<String>("name")?;
29///         let age = rec.field_optional::<u32>("age")?;
30///         rec.deny_unknown_fields()?;
31///         Ok(User { name, age })
32///     }
33/// }
34/// ```
35#[must_use]
36pub struct RecordParser<'doc> {
37    map: &'doc NodeMap,
38    /// The parse context (holds doc, node_id, accessed, flatten_ctx).
39    ctx: ParseContext<'doc>,
40}
41
42impl<'doc> RecordParser<'doc> {
43    /// Create a new RecordParser for the given context.
44    pub(crate) fn new(ctx: &ParseContext<'doc>) -> Result<Self, ParseError> {
45        // Error if called in Extension scope - this is a user mistake
46        // (using #[eure(flatten_ext)] with a record-parsing type)
47        if let Some(fc) = ctx.flatten_ctx()
48            && fc.scope() == ParserScope::Extension
49        {
50            return Err(ParseError {
51                node_id: ctx.node_id(),
52                kind: ParseErrorKind::RecordInExtensionScope,
53            });
54        }
55
56        let node = ctx.node();
57        match &node.content {
58            NodeValue::Map(map) => Ok(Self {
59                map,
60                ctx: ctx.clone(),
61            }),
62            NodeValue::Hole(_) => Err(ParseError {
63                node_id: ctx.node_id(),
64                kind: ParseErrorKind::UnexpectedHole,
65            }),
66            _ => Err(ctx.unexpected_kind(crate::value::ValueKind::Map)),
67        }
68    }
69
70    /// Create a new RecordParser from document and node ID directly.
71    pub(crate) fn from_doc_and_node(
72        doc: &'doc EureDocument,
73        node_id: NodeId,
74    ) -> Result<Self, ParseError> {
75        let ctx = ParseContext::new(doc, node_id);
76        Self::new(&ctx)
77    }
78
79    /// Mark a field as accessed.
80    fn mark_accessed(&self, name: &str) {
81        self.ctx.accessed().add_field(name);
82    }
83
84    /// Get the node ID being parsed.
85    pub fn node_id(&self) -> NodeId {
86        self.ctx.node_id()
87    }
88
89    /// Get a required field.
90    ///
91    /// Returns `ParseErrorKind::MissingField` if the field is not present or is excluded.
92    pub fn parse_field<T>(&self, name: &str) -> Result<T, T::Error>
93    where
94        T: FromEure<'doc>,
95        T::Error: From<ParseError>,
96    {
97        self.parse_field_with(name, T::parse)
98    }
99
100    pub fn parse_field_with<T>(&self, name: &str, mut parser: T) -> Result<T::Output, T::Error>
101    where
102        T: DocumentParser<'doc>,
103        T::Error: From<ParseError>,
104    {
105        self.mark_accessed(name);
106        let field_node_id = self
107            .map
108            .get(&ObjectKey::String(name.to_string()))
109            .ok_or_else(|| ParseError {
110                node_id: self.ctx.node_id(),
111                kind: ParseErrorKind::MissingField(name.to_string()),
112            })?;
113        let ctx = ParseContext::new(self.ctx.doc(), *field_node_id);
114        parser.parse(&ctx)
115    }
116
117    pub fn parse_field_optional<T>(&self, name: &str) -> Result<Option<T>, T::Error>
118    where
119        T: FromEure<'doc>,
120        T::Error: From<ParseError>,
121    {
122        self.parse_field_optional_with(name, T::parse)
123    }
124
125    /// Get an optional field.
126    ///
127    /// Returns `Ok(None)` if the field is not present.
128    pub fn parse_field_optional_with<T>(
129        &self,
130        name: &str,
131        mut parser: T,
132    ) -> Result<Option<T::Output>, T::Error>
133    where
134        T: DocumentParser<'doc>,
135        T::Error: From<ParseError>,
136    {
137        self.mark_accessed(name);
138        match self.map.get(&ObjectKey::String(name.to_string())) {
139            Some(field_node_id) => {
140                let ctx = ParseContext::new(self.ctx.doc(), *field_node_id);
141                Ok(Some(parser.parse(&ctx)?))
142            }
143            None => Ok(None),
144        }
145    }
146
147    /// Get the parse context for a field without parsing it.
148    ///
149    /// Use this when you need access to the field's NodeId or want to defer parsing.
150    /// Returns `ParseErrorKind::MissingField` if the field is not present.
151    pub fn field(&self, name: &str) -> Result<ParseContext<'doc>, ParseError> {
152        self.mark_accessed(name);
153        let field_node_id = self
154            .map
155            .get(&ObjectKey::String(name.to_string()))
156            .ok_or_else(|| ParseError {
157                node_id: self.ctx.node_id(),
158                kind: ParseErrorKind::MissingField(name.to_string()),
159            })?;
160        Ok(ParseContext::new(self.ctx.doc(), *field_node_id))
161    }
162
163    /// Get the parse context for an optional field without parsing it.
164    ///
165    /// Use this when you need access to the field's NodeId or want to defer parsing.
166    /// Returns `None` if the field is not present.
167    pub fn field_optional(&self, name: &str) -> Option<ParseContext<'doc>> {
168        self.mark_accessed(name);
169        self.map
170            .get(&ObjectKey::String(name.to_string()))
171            .map(|node_id| ParseContext::new(self.ctx.doc(), *node_id))
172    }
173
174    /// Get a field as a nested record parser.
175    ///
176    /// Returns `ParseErrorKind::MissingField` if the field is not present.
177    pub fn field_record(&self, name: &str) -> Result<RecordParser<'doc>, ParseError> {
178        self.mark_accessed(name);
179        let field_node_id = self
180            .map
181            .get(&ObjectKey::String(name.to_string()))
182            .ok_or_else(|| ParseError {
183                node_id: self.ctx.node_id(),
184                kind: ParseErrorKind::MissingField(name.to_string()),
185            })?;
186        let ctx = ParseContext::new(self.ctx.doc(), *field_node_id);
187        RecordParser::new(&ctx)
188    }
189
190    /// Get an optional field as a nested record parser.
191    ///
192    /// Returns `Ok(None)` if the field is not present.
193    pub fn field_record_optional(
194        &self,
195        name: &str,
196    ) -> Result<Option<RecordParser<'doc>>, ParseError> {
197        self.mark_accessed(name);
198        match self.map.get(&ObjectKey::String(name.to_string())) {
199            Some(field_node_id) => {
200                let ctx = ParseContext::new(self.ctx.doc(), *field_node_id);
201                Ok(Some(RecordParser::new(&ctx)?))
202            }
203            None => Ok(None),
204        }
205    }
206
207    /// Finish parsing with Deny policy (error if unknown fields exist).
208    ///
209    /// This also errors if the map contains non-string keys, as records
210    /// should only have string-keyed fields.
211    ///
212    /// **Flatten behavior**: If this parser has a flatten_ctx (i.e., is a child
213    /// in a flatten chain), this is a no-op. Only root parsers validate.
214    pub fn deny_unknown_fields(self) -> Result<(), ParseError> {
215        // If child (has flatten_ctx with Record scope), no-op - parent will validate
216        if let Some(fc) = self.ctx.flatten_ctx()
217            && fc.scope() == ParserScope::Record
218        {
219            return Ok(());
220        }
221
222        // Root parser - validate using accessed set
223        let accessed = self.ctx.accessed();
224        for (key, _) in self.map.iter() {
225            match key {
226                ObjectKey::String(name) => {
227                    if !accessed.has_field(name.as_str()) {
228                        return Err(ParseError {
229                            node_id: self.ctx.node_id(),
230                            kind: ParseErrorKind::UnknownField(name.clone()),
231                        });
232                    }
233                }
234                // Non-string keys are invalid in records
235                other => {
236                    return Err(ParseError {
237                        node_id: self.ctx.node_id(),
238                        kind: ParseErrorKind::InvalidKeyType(other.clone()),
239                    });
240                }
241            }
242        }
243        Ok(())
244    }
245
246    /// Finish parsing with Allow policy (allow unknown string fields).
247    ///
248    /// This still errors if the map contains non-string keys, as records
249    /// should only have string-keyed fields.
250    pub fn allow_unknown_fields(self) -> Result<(), ParseError> {
251        // Check for non-string keys (invalid in records)
252        for (key, _) in self.map.iter() {
253            if !matches!(key, ObjectKey::String(_)) {
254                return Err(ParseError {
255                    node_id: self.ctx.node_id(),
256                    kind: ParseErrorKind::InvalidKeyType(key.clone()),
257                });
258            }
259        }
260        Ok(())
261    }
262
263    /// Get an iterator over unknown fields (for Schema policy or custom handling).
264    ///
265    /// Returns `Result` items:
266    /// - `Ok((field_name, context))` for unaccessed string-keyed fields
267    /// - `Err((invalid_key, context))` for non-string keys, allowing caller to handle directly
268    ///
269    /// Note: In flattened contexts, this still returns fields - use `deny_unknown_fields()`
270    /// if you want the automatic no-op behavior for child parsers.
271    pub fn unknown_fields(
272        &self,
273    ) -> impl Iterator<
274        Item = Result<(&'doc str, ParseContext<'doc>), (&'doc ObjectKey, ParseContext<'doc>)>,
275    > + '_ {
276        let doc = self.ctx.doc();
277        // Clone the accessed set for filtering - we need the current state
278        let accessed = self.ctx.accessed().clone();
279        self.map
280            .iter()
281            .filter_map(move |(key, &node_id)| match key {
282                ObjectKey::String(name) => {
283                    if !accessed.has_field(name.as_str()) {
284                        Some(Ok((name.as_str(), ParseContext::new(doc, node_id))))
285                    } else {
286                        None // Accessed, skip
287                    }
288                }
289                other => Some(Err((other, ParseContext::new(doc, node_id)))),
290            })
291    }
292
293    /// Get an iterator over all unknown entries including non-string keys.
294    ///
295    /// Returns (ObjectKey, context) pairs for:
296    /// - String keys that haven't been accessed
297    /// - All non-string keys (e.g., integer keys)
298    ///
299    /// This is useful for flatten map validation where both string and integer
300    /// keys need to be validated against the map's key schema.
301    pub fn unknown_entries(
302        &self,
303    ) -> impl Iterator<Item = (&'doc ObjectKey, ParseContext<'doc>)> + '_ {
304        let doc = self.ctx.doc();
305        // Clone the accessed set for filtering - we need the current state
306        let accessed = self.ctx.accessed().clone();
307        self.map.iter().filter_map(move |(key, &node_id)| {
308            match key {
309                ObjectKey::String(name) => {
310                    // For string keys, only return if not accessed
311                    if !accessed.has_field(name.as_str()) {
312                        Some((key, ParseContext::new(doc, node_id)))
313                    } else {
314                        None
315                    }
316                }
317                // Non-string keys are always returned (they can't be "accessed" via field methods)
318                _ => Some((key, ParseContext::new(doc, node_id))),
319            }
320        })
321    }
322
323    /// Create a flatten context for child parsers in Record scope.
324    ///
325    /// This creates a FlattenContext initialized with the current accessed fields,
326    /// and returns a ParseContext that children can use. Children created from this
327    /// context will:
328    /// - Add their accessed fields to the shared FlattenContext
329    /// - Have deny_unknown_fields() be a no-op
330    ///
331    /// The root parser should call deny_unknown_fields() after all children are done.
332    pub fn flatten(&self) -> ParseContext<'doc> {
333        self.ctx.flatten()
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::value::PrimitiveValue;
341
342    fn create_test_doc() -> EureDocument {
343        let mut doc = EureDocument::new();
344        let root_id = doc.get_root_id();
345
346        // Add fields: name = "Alice", age = 30
347        let name_id = doc
348            .add_map_child(ObjectKey::String("name".to_string()), root_id)
349            .unwrap()
350            .node_id;
351        doc.node_mut(name_id).content = NodeValue::Primitive(PrimitiveValue::Text(
352            crate::text::Text::plaintext("Alice".to_string()),
353        ));
354
355        let age_id = doc
356            .add_map_child(ObjectKey::String("age".to_string()), root_id)
357            .unwrap()
358            .node_id;
359        doc.node_mut(age_id).content = NodeValue::Primitive(PrimitiveValue::Integer(30.into()));
360
361        doc
362    }
363
364    #[test]
365    fn test_record_field() {
366        let doc = create_test_doc();
367        let rec = doc.parse_record(doc.get_root_id()).unwrap();
368
369        let name: String = rec.parse_field("name").unwrap();
370        assert_eq!(name, "Alice");
371    }
372
373    #[test]
374    fn test_record_field_missing() {
375        let doc = create_test_doc();
376        let rec = doc.parse_record(doc.get_root_id()).unwrap();
377
378        let result: Result<String, _> = rec.parse_field("nonexistent");
379        assert!(matches!(
380            result.unwrap_err().kind,
381            ParseErrorKind::MissingField(_)
382        ));
383    }
384
385    #[test]
386    fn test_record_field_optional() {
387        let doc = create_test_doc();
388        let rec = doc.parse_record(doc.get_root_id()).unwrap();
389
390        let name: Option<String> = rec.parse_field_optional("name").unwrap();
391        assert_eq!(name, Some("Alice".to_string()));
392
393        let missing: Option<String> = rec.parse_field_optional("nonexistent").unwrap();
394        assert_eq!(missing, None);
395    }
396
397    #[test]
398    fn test_record_deny_unknown_fields() {
399        let doc = create_test_doc();
400        let rec = doc.parse_record(doc.get_root_id()).unwrap();
401
402        let _name: String = rec.parse_field("name").unwrap();
403        // Didn't access "age", so deny should fail
404        let result = rec.deny_unknown_fields();
405        assert!(matches!(
406            result.unwrap_err().kind,
407            ParseErrorKind::UnknownField(_)
408        ));
409    }
410
411    #[test]
412    fn test_record_deny_unknown_fields_all_accessed() {
413        let doc = create_test_doc();
414        let rec = doc.parse_record(doc.get_root_id()).unwrap();
415
416        let _name: String = rec.parse_field("name").unwrap();
417        let _age: num_bigint::BigInt = rec.parse_field("age").unwrap();
418        // Accessed all fields, should succeed
419        rec.deny_unknown_fields().unwrap();
420    }
421
422    #[test]
423    fn test_record_allow_unknown_fields() {
424        let doc = create_test_doc();
425        let rec = doc.parse_record(doc.get_root_id()).unwrap();
426
427        let _name: String = rec.parse_field("name").unwrap();
428        // Didn't access "age", but allow should succeed
429        rec.allow_unknown_fields().unwrap();
430    }
431
432    #[test]
433    fn test_record_unknown_fields_iterator() {
434        let doc = create_test_doc();
435        let rec = doc.parse_record(doc.get_root_id()).unwrap();
436
437        let _name: String = rec.parse_field("name").unwrap();
438        // "age" should be in unknown fields
439        let unknown: Vec<_> = rec.unknown_fields().collect::<Result<Vec<_>, _>>().unwrap();
440        assert_eq!(unknown.len(), 1);
441        assert_eq!(unknown[0].0, "age");
442    }
443
444    #[test]
445    fn test_record_with_non_string_keys_deny_should_error() {
446        // deny_unknown_fields() errors on non-string keys
447        use crate::eure;
448
449        let doc = eure!({ 0 = "value" });
450        let rec = doc.parse_record(doc.get_root_id()).unwrap();
451
452        let result = rec.deny_unknown_fields();
453        assert!(
454            matches!(result.unwrap_err().kind, ParseErrorKind::InvalidKeyType(_)),
455            "deny_unknown_fields() should error on non-string keys"
456        );
457    }
458
459    #[test]
460    fn test_record_with_non_string_keys_unknown_fields_iterator() {
461        // unknown_fields() returns Err for non-string keys
462        use crate::eure;
463
464        let doc = eure!({ 0 = "value" });
465        let rec = doc.parse_record(doc.get_root_id()).unwrap();
466
467        // unknown_fields() should return an error for the non-string key
468        let result: Result<Vec<_>, _> = rec.unknown_fields().collect();
469        let (invalid_key, _ctx) = result.unwrap_err();
470        assert!(
471            matches!(invalid_key, ObjectKey::Number(_)),
472            "unknown_fields() should return the invalid key directly"
473        );
474    }
475
476    #[test]
477    fn test_unknown_fields_err_contains_correct_context() {
478        // Verify that the Err case contains a context pointing to the value node
479        use crate::eure;
480
481        let doc = eure!({ 42 = "test" });
482        let rec = doc.parse_record(doc.get_root_id()).unwrap();
483
484        let result: Result<Vec<_>, _> = rec.unknown_fields().collect();
485        let (key, ctx) = result.unwrap_err();
486
487        // Verify the key is the numeric key
488        assert_eq!(key, &ObjectKey::Number(42.into()));
489        // Verify the context can be used to parse the value
490        let value: String = ctx.parse().unwrap();
491        assert_eq!(value, "test");
492    }
493
494    #[test]
495    fn test_unknown_fields_mixed_string_and_non_string_keys() {
496        // Test that string keys return Ok, non-string keys return Err
497        use crate::eure;
498
499        let doc = eure!({
500            name = "Alice"
501            123 = "numeric"
502        });
503        let rec = doc.parse_record(doc.get_root_id()).unwrap();
504
505        // Collect results to inspect both Ok and Err
506        let mut ok_fields = Vec::new();
507        let mut err_keys = Vec::new();
508        for result in rec.unknown_fields() {
509            match result {
510                Ok((name, _ctx)) => ok_fields.push(name.to_string()),
511                Err((key, _ctx)) => err_keys.push(key.clone()),
512            }
513        }
514
515        // Should have one Ok (string key) and one Err (numeric key)
516        assert_eq!(ok_fields, vec!["name"]);
517        assert_eq!(err_keys, vec![ObjectKey::Number(123.into())]);
518    }
519
520    #[test]
521    fn test_unknown_fields_accessed_fields_filtered_non_string_always_returned() {
522        // Accessed string keys are filtered out, but non-string keys always return Err
523        use crate::eure;
524
525        let doc = eure!({
526            name = "Alice"
527            age = 30
528            999 = "numeric"
529        });
530        let rec = doc.parse_record(doc.get_root_id()).unwrap();
531
532        // Access the "name" field
533        let _name: String = rec.parse_field("name").unwrap();
534
535        // Check unknown_fields - "name" filtered, "age" is Ok, numeric is Err
536        let mut ok_fields = Vec::new();
537        let mut err_keys = Vec::new();
538        for result in rec.unknown_fields() {
539            match result {
540                Ok((name, _ctx)) => ok_fields.push(name.to_string()),
541                Err((key, _ctx)) => err_keys.push(key.clone()),
542            }
543        }
544
545        assert_eq!(ok_fields, vec!["age"]);
546        assert_eq!(err_keys, vec![ObjectKey::Number(999.into())]);
547    }
548
549    #[test]
550    fn test_unknown_fields_multiple_non_string_keys() {
551        // Test handling of multiple non-string keys
552        use crate::eure;
553
554        let doc = eure!({
555            1 = "one"
556            2 = "two"
557        });
558        let rec = doc.parse_record(doc.get_root_id()).unwrap();
559
560        // Collect all errors
561        let mut err_keys: Vec<ObjectKey> = Vec::new();
562        for result in rec.unknown_fields() {
563            if let Err((key, _ctx)) = result {
564                err_keys.push(key.clone());
565            }
566        }
567
568        // Should have both numeric keys as errors
569        assert_eq!(err_keys.len(), 2);
570        assert!(err_keys.contains(&ObjectKey::Number(1.into())));
571        assert!(err_keys.contains(&ObjectKey::Number(2.into())));
572    }
573
574    #[test]
575    fn test_parse_ext() {
576        let mut doc = EureDocument::new();
577        let root_id = doc.get_root_id();
578
579        // Add extension: $ext-type.optional = true
580        let ext_id = doc
581            .add_extension("optional".parse().unwrap(), root_id)
582            .unwrap()
583            .node_id;
584        doc.node_mut(ext_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
585
586        let ctx = doc.parse_extension_context(root_id);
587        let optional: bool = ctx.parse_ext("optional").unwrap();
588        assert!(optional);
589    }
590
591    #[test]
592    fn test_parse_ext_optional_missing() {
593        let doc = EureDocument::new();
594        let root_id = doc.get_root_id();
595
596        let ctx = doc.parse_extension_context(root_id);
597        let optional: Option<bool> = ctx.parse_ext_optional("optional").unwrap();
598        assert_eq!(optional, None);
599    }
600
601    /// Helper struct for testing three-level nested flatten pattern.
602    /// Parses: { a, b, c, d, e } with three-level flatten.
603    #[derive(Debug, PartialEq)]
604    struct ThreeLevelFlatten {
605        a: i32,
606        b: i32,
607        c: i32,
608        d: i32,
609        e: i32,
610    }
611
612    impl<'doc> FromEure<'doc> for ThreeLevelFlatten {
613        type Error = ParseError;
614
615        fn parse(ctx: &ParseContext<'doc>) -> Result<Self, Self::Error> {
616            // Level 1
617            let rec1 = ctx.parse_record()?;
618            let a = rec1.parse_field("a")?;
619            let ctx2 = rec1.flatten();
620
621            // Level 2
622            let rec2 = ctx2.parse_record()?;
623            let b = rec2.parse_field("b")?;
624            let c = rec2.parse_field("c")?;
625            let ctx3 = rec2.flatten();
626
627            // Level 3
628            let rec3 = ctx3.parse_record()?;
629            let d = rec3.parse_field("d")?;
630            let e = rec3.parse_field("e")?;
631            rec3.deny_unknown_fields()?;
632
633            // Level 2 deny (no-op since child)
634            rec2.deny_unknown_fields()?;
635
636            // Level 1 deny (root - validates all)
637            rec1.deny_unknown_fields()?;
638
639            Ok(Self { a, b, c, d, e })
640        }
641    }
642
643    #[test]
644    fn test_nested_flatten_preserves_consumed_fields() {
645        // Document: { a = 1, b = 2, c = 3, d = 4, e = 5 }
646        //
647        // Parsing structure:
648        // Level 1: parse_record(), field(a), flatten() →
649        //   Level 2: field(b), field(c), flatten() →
650        //     Level 3: field(d), field(e), deny_unknown_fields()
651        //   Level 2: deny_unknown_fields()
652        // Level 1: deny_unknown_fields()
653        //
654        // Expected: All deny_unknown_fields() should succeed
655        use crate::eure;
656
657        let doc = eure!({ a = 1, b = 2, c = 3, d = 4, e = 5 });
658        let result: ThreeLevelFlatten = doc.parse(doc.get_root_id()).unwrap();
659
660        assert_eq!(
661            result,
662            ThreeLevelFlatten {
663                a: 1,
664                b: 2,
665                c: 3,
666                d: 4,
667                e: 5
668            }
669        );
670    }
671
672    #[test]
673    fn test_nested_flatten_catches_unaccessed_field() {
674        // Document: { a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 }
675        //
676        // Parsing structure (NOT accessing f):
677        // Level 1: field(a), flatten() →
678        //   Level 2: field(b), field(c), flatten() →
679        //     Level 3: field(d), field(e), deny_unknown_fields()
680        //   Level 2: deny_unknown_fields()
681        // Level 1: deny_unknown_fields() <- Should FAIL because f is not accessed
682        //
683        // Expected: Level 1's deny_unknown_fields() should fail with UnknownField("f")
684        use crate::eure;
685
686        let doc = eure!({ a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 });
687        let result: Result<ThreeLevelFlatten, _> = doc.parse(doc.get_root_id());
688
689        assert_eq!(
690            result.unwrap_err().kind,
691            ParseErrorKind::UnknownField("f".to_string())
692        );
693    }
694
695    #[test]
696    fn test_flatten_union_reverts_accessed_fields_on_failure() {
697        use crate::eure;
698
699        let doc = eure!({
700            a = 1
701            b = 2
702            c = 3
703            d = 4
704        });
705
706        // Define enum with two variants
707        #[derive(Debug, PartialEq)]
708        enum TestOption {
709            A { a: i32, c: i32, e: i32 },
710            B { a: i32, b: i32 },
711        }
712
713        impl<'doc> FromEure<'doc> for TestOption {
714            type Error = ParseError;
715
716            fn parse(ctx: &ParseContext<'doc>) -> Result<Self, Self::Error> {
717                ctx.parse_union()?
718                    .variant("A", |ctx: &ParseContext<'_>| {
719                        let rec = ctx.parse_record()?;
720                        let a = rec.parse_field("a")?;
721                        let c = rec.parse_field("c")?;
722                        let e = rec.parse_field("e")?; // Will fail - field doesn't exist
723                        rec.deny_unknown_fields()?;
724                        Ok(TestOption::A { a, c, e })
725                    })
726                    .variant("B", |ctx: &ParseContext<'_>| {
727                        let rec = ctx.parse_record()?;
728                        let a = rec.parse_field("a")?;
729                        let b = rec.parse_field("b")?;
730                        rec.deny_unknown_fields()?;
731                        Ok(TestOption::B { a, b })
732                    })
733                    .parse()
734            }
735        }
736
737        // Parse with flatten
738        let root_id = doc.get_root_id();
739        let root_ctx = ParseContext::new(&doc, root_id);
740        let record = root_ctx.parse_record().unwrap();
741
742        // Parse union - should succeed with VariantB
743        let option = record.flatten().parse::<TestOption>().unwrap();
744        assert_eq!(option, TestOption::B { a: 1, b: 2 });
745
746        // Access field d
747        let d: i32 = record.parse_field("d").unwrap();
748        assert_eq!(d, 4);
749
750        // BUG: This should FAIL because field 'c' was never accessed by VariantB
751        // (the successful variant), but it SUCCEEDS because VariantA tried 'c'
752        // before failing
753        let result = record.deny_unknown_fields();
754
755        assert_eq!(
756            result.unwrap_err(),
757            ParseError {
758                node_id: root_id,
759                kind: ParseErrorKind::UnknownField("c".to_string()),
760            }
761        );
762    }
763
764    // =========================================================================
765    // Tests for alternating flatten/flatten_ext scope changes
766    // =========================================================================
767
768    /// Tests alternating flatten -> flatten_ext -> flatten -> flatten_ext pattern
769    #[derive(Debug, PartialEq)]
770    struct AlternatingFlattenTest {
771        normal1: i32,
772        ext_normal2: i32,
773        ext_normal3: i32,
774        ext_content: String,
775    }
776
777    impl<'doc> FromEure<'doc> for AlternatingFlattenTest {
778        type Error = ParseError;
779
780        fn parse(ctx: &ParseContext<'doc>) -> Result<Self, Self::Error> {
781            // Level 1: Record scope (flatten)
782            let rec1 = ctx.parse_record()?;
783            let normal1 = rec1.parse_field("normal")?;
784            let ctx1 = rec1.flatten();
785
786            // Level 2: Extension scope (flatten_ext)
787            let ctx2 = ctx1.flatten_ext();
788            assert_eq!(ctx2.parser_scope(), Some(ParserScope::Extension));
789            let ext1_ctx = ctx2.ext("item")?;
790
791            // Level 3: Record scope (flatten) - inside extension value
792            let rec2 = ext1_ctx.parse_record()?;
793            let ext_normal2 = rec2.parse_field("normal")?;
794            let ctx3 = rec2.flatten();
795            assert_eq!(ctx3.parser_scope(), Some(ParserScope::Record));
796
797            // Level 4: Extension scope (flatten_ext)
798            let ctx4 = ctx3.flatten_ext();
799            assert_eq!(ctx4.parser_scope(), Some(ParserScope::Extension));
800            let ext2_ctx = ctx4.ext("item")?;
801
802            // Level 5: Record scope (flatten)
803            let rec3 = ext2_ctx.parse_record()?;
804            let ext_normal3 = rec3.parse_field("normal")?;
805            let ctx5 = rec3.flatten();
806
807            // Level 6: Extension scope (flatten_ext)
808            let ctx6 = ctx5.flatten_ext();
809            let ext3_ctx = ctx6.ext("item")?;
810
811            // Innermost: Record
812            let rec4 = ext3_ctx.parse_record()?;
813            let ext_content = rec4.parse_field("content")?;
814
815            // Deny at all levels
816            rec4.deny_unknown_fields()?;
817            ctx6.deny_unknown_extensions()?;
818            rec3.deny_unknown_fields()?;
819            ctx4.deny_unknown_extensions()?;
820            rec2.deny_unknown_fields()?;
821            ctx2.deny_unknown_extensions()?;
822            rec1.deny_unknown_fields()?;
823            ctx1.deny_unknown_extensions()?;
824
825            Ok(Self {
826                normal1,
827                ext_normal2,
828                ext_normal3,
829                ext_content,
830            })
831        }
832    }
833
834    #[test]
835    fn test_alternating_flatten_flatten_ext() {
836        use crate::eure;
837
838        let doc = eure!({
839            normal = 1
840            %item {
841                normal = 2
842                %item {
843                    normal = 3
844                    %item {
845                        content = "Hello"
846                    }
847                }
848            }
849        });
850
851        let result: AlternatingFlattenTest = doc.parse(doc.get_root_id()).unwrap();
852        assert_eq!(
853            result,
854            AlternatingFlattenTest {
855                normal1: 1,
856                ext_normal2: 2,
857                ext_normal3: 3,
858                ext_content: "Hello".to_string(),
859            }
860        );
861    }
862
863    #[test]
864    fn test_alternating_flatten_scope_changes() {
865        use crate::eure;
866
867        let doc = eure!({});
868        let root_id = doc.get_root_id();
869        let ctx = ParseContext::new(&doc, root_id);
870
871        // flatten -> Record
872        let ctx1 = ctx.flatten();
873        assert_eq!(ctx1.parser_scope(), Some(ParserScope::Record));
874
875        // flatten_ext -> Extension
876        let ctx2 = ctx1.flatten_ext();
877        assert_eq!(ctx2.parser_scope(), Some(ParserScope::Extension));
878
879        // flatten -> Record (THIS IS THE BUG - currently stays Extension)
880        let ctx3 = ctx2.flatten();
881        assert_eq!(ctx3.parser_scope(), Some(ParserScope::Record));
882
883        // flatten_ext -> Extension
884        let ctx4 = ctx3.flatten_ext();
885        assert_eq!(ctx4.parser_scope(), Some(ParserScope::Extension));
886    }
887}