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