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::{ParseContext, ParseDocument, 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> ParseDocument<'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: ParseDocument<'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: ParseDocument<'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 (field_name, context) pairs for fields that haven't been accessed.
294    pub fn unknown_fields(&self) -> impl Iterator<Item = (&'doc str, ParseContext<'doc>)> + '_ {
295        let doc = self.ctx.doc();
296        let mode = self.union_tag_mode;
297        // Clone the accessed set for filtering - we need the current state
298        let accessed = self.ctx.accessed().clone();
299        self.map.iter().filter_map(move |(key, &node_id)| {
300            if let ObjectKey::String(name) = key
301                && !accessed.has_field(name.as_str())
302            {
303                return Some((
304                    name.as_str(),
305                    ParseContext::with_union_tag_mode(doc, node_id, mode),
306                ));
307            }
308            None
309        })
310    }
311
312    /// Create a flatten context for child parsers in Record scope.
313    ///
314    /// This creates a FlattenContext initialized with the current accessed fields,
315    /// and returns a ParseContext that children can use. Children created from this
316    /// context will:
317    /// - Add their accessed fields to the shared FlattenContext
318    /// - Have deny_unknown_fields() be a no-op
319    ///
320    /// The root parser should call deny_unknown_fields() after all children are done.
321    pub fn flatten(&self) -> ParseContext<'doc> {
322        self.ctx.flatten()
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use crate::value::PrimitiveValue;
330
331    fn create_test_doc() -> EureDocument {
332        let mut doc = EureDocument::new();
333        let root_id = doc.get_root_id();
334
335        // Add fields: name = "Alice", age = 30
336        let name_id = doc
337            .add_map_child(ObjectKey::String("name".to_string()), root_id)
338            .unwrap()
339            .node_id;
340        doc.node_mut(name_id).content = NodeValue::Primitive(PrimitiveValue::Text(
341            crate::text::Text::plaintext("Alice".to_string()),
342        ));
343
344        let age_id = doc
345            .add_map_child(ObjectKey::String("age".to_string()), root_id)
346            .unwrap()
347            .node_id;
348        doc.node_mut(age_id).content = NodeValue::Primitive(PrimitiveValue::Integer(30.into()));
349
350        doc
351    }
352
353    #[test]
354    fn test_record_field() {
355        let doc = create_test_doc();
356        let rec = doc.parse_record(doc.get_root_id()).unwrap();
357
358        let name: String = rec.parse_field("name").unwrap();
359        assert_eq!(name, "Alice");
360    }
361
362    #[test]
363    fn test_record_field_missing() {
364        let doc = create_test_doc();
365        let rec = doc.parse_record(doc.get_root_id()).unwrap();
366
367        let result: Result<String, _> = rec.parse_field("nonexistent");
368        assert!(matches!(
369            result.unwrap_err().kind,
370            ParseErrorKind::MissingField(_)
371        ));
372    }
373
374    #[test]
375    fn test_record_field_optional() {
376        let doc = create_test_doc();
377        let rec = doc.parse_record(doc.get_root_id()).unwrap();
378
379        let name: Option<String> = rec.parse_field_optional("name").unwrap();
380        assert_eq!(name, Some("Alice".to_string()));
381
382        let missing: Option<String> = rec.parse_field_optional("nonexistent").unwrap();
383        assert_eq!(missing, None);
384    }
385
386    #[test]
387    fn test_record_deny_unknown_fields() {
388        let doc = create_test_doc();
389        let rec = doc.parse_record(doc.get_root_id()).unwrap();
390
391        let _name: String = rec.parse_field("name").unwrap();
392        // Didn't access "age", so deny should fail
393        let result = rec.deny_unknown_fields();
394        assert!(matches!(
395            result.unwrap_err().kind,
396            ParseErrorKind::UnknownField(_)
397        ));
398    }
399
400    #[test]
401    fn test_record_deny_unknown_fields_all_accessed() {
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        let _age: num_bigint::BigInt = rec.parse_field("age").unwrap();
407        // Accessed all fields, should succeed
408        rec.deny_unknown_fields().unwrap();
409    }
410
411    #[test]
412    fn test_record_allow_unknown_fields() {
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        // Didn't access "age", but allow should succeed
418        rec.allow_unknown_fields().unwrap();
419    }
420
421    #[test]
422    fn test_record_unknown_fields_iterator() {
423        let doc = create_test_doc();
424        let rec = doc.parse_record(doc.get_root_id()).unwrap();
425
426        let _name: String = rec.parse_field("name").unwrap();
427        // "age" should be in unknown fields
428        let unknown: Vec<_> = rec.unknown_fields().collect();
429        assert_eq!(unknown.len(), 1);
430        assert_eq!(unknown[0].0, "age");
431    }
432
433    #[test]
434    fn test_record_with_non_string_keys_deny_should_error() {
435        // BUG: deny_unknown_fields() silently skips non-string keys
436        // Expected: Should error when a map has numeric keys
437        // Actual: Silently ignores them
438        let mut doc = EureDocument::new();
439        let root_id = doc.get_root_id();
440
441        // Add a field with numeric key: { 0 => "value" }
442        use num_bigint::BigInt;
443        let value_id = doc
444            .add_map_child(ObjectKey::Number(BigInt::from(0)), root_id)
445            .unwrap()
446            .node_id;
447        doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Text(
448            crate::text::Text::plaintext("value".to_string()),
449        ));
450
451        let rec = doc.parse_record(doc.get_root_id()).unwrap();
452
453        // BUG: This should error because there's an unaccessed non-string key
454        // but currently it succeeds
455        let result = rec.deny_unknown_fields();
456        assert!(
457            result.is_err(),
458            "BUG: deny_unknown_fields() should error on non-string keys, but it succeeds"
459        );
460    }
461
462    #[test]
463    fn test_record_with_non_string_keys_unknown_fields_iterator() {
464        // unknown_fields() intentionally only returns string keys (signature: (&str, NodeId))
465        // Non-string keys are caught by deny_unknown_fields() instead
466        let mut doc = EureDocument::new();
467        let root_id = doc.get_root_id();
468
469        // Add a field with numeric key: { 0 => "value" }
470        use num_bigint::BigInt;
471        let value_id = doc
472            .add_map_child(ObjectKey::Number(BigInt::from(0)), root_id)
473            .unwrap()
474            .node_id;
475        doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Text(
476            crate::text::Text::plaintext("value".to_string()),
477        ));
478
479        let rec = doc.parse_record(doc.get_root_id()).unwrap();
480
481        // unknown_fields() returns empty because it only returns string keys
482        // (the numeric key is not included in the iterator by design)
483        let unknown: Vec<_> = rec.unknown_fields().collect();
484        assert_eq!(
485            unknown.len(),
486            0,
487            "unknown_fields() should only return string keys, numeric keys are excluded"
488        );
489    }
490
491    #[test]
492    fn test_parse_ext() {
493        let mut doc = EureDocument::new();
494        let root_id = doc.get_root_id();
495
496        // Add extension: $ext-type.optional = true
497        let ext_id = doc
498            .add_extension("optional".parse().unwrap(), root_id)
499            .unwrap()
500            .node_id;
501        doc.node_mut(ext_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
502
503        let ctx = doc.parse_extension_context(root_id);
504        let optional: bool = ctx.parse_ext("optional").unwrap();
505        assert!(optional);
506    }
507
508    #[test]
509    fn test_parse_ext_optional_missing() {
510        let doc = EureDocument::new();
511        let root_id = doc.get_root_id();
512
513        let ctx = doc.parse_extension_context(root_id);
514        let optional: Option<bool> = ctx.parse_ext_optional("optional").unwrap();
515        assert_eq!(optional, None);
516    }
517
518    /// Helper struct for testing three-level nested flatten pattern.
519    /// Parses: { a, b, c, d, e } with three-level flatten.
520    #[derive(Debug, PartialEq)]
521    struct ThreeLevelFlatten {
522        a: i32,
523        b: i32,
524        c: i32,
525        d: i32,
526        e: i32,
527    }
528
529    impl<'doc> ParseDocument<'doc> for ThreeLevelFlatten {
530        type Error = ParseError;
531
532        fn parse(ctx: &ParseContext<'doc>) -> Result<Self, Self::Error> {
533            // Level 1
534            let rec1 = ctx.parse_record()?;
535            let a = rec1.parse_field("a")?;
536            let ctx2 = rec1.flatten();
537
538            // Level 2
539            let rec2 = ctx2.parse_record()?;
540            let b = rec2.parse_field("b")?;
541            let c = rec2.parse_field("c")?;
542            let ctx3 = rec2.flatten();
543
544            // Level 3
545            let rec3 = ctx3.parse_record()?;
546            let d = rec3.parse_field("d")?;
547            let e = rec3.parse_field("e")?;
548            rec3.deny_unknown_fields()?;
549
550            // Level 2 deny (no-op since child)
551            rec2.deny_unknown_fields()?;
552
553            // Level 1 deny (root - validates all)
554            rec1.deny_unknown_fields()?;
555
556            Ok(Self { a, b, c, d, e })
557        }
558    }
559
560    #[test]
561    fn test_nested_flatten_preserves_consumed_fields() {
562        // Document: { a = 1, b = 2, c = 3, d = 4, e = 5 }
563        //
564        // Parsing structure:
565        // Level 1: parse_record(), field(a), flatten() →
566        //   Level 2: field(b), field(c), flatten() →
567        //     Level 3: field(d), field(e), deny_unknown_fields()
568        //   Level 2: deny_unknown_fields()
569        // Level 1: deny_unknown_fields()
570        //
571        // Expected: All deny_unknown_fields() should succeed
572        use crate::eure;
573
574        let doc = eure!({ a = 1, b = 2, c = 3, d = 4, e = 5 });
575        let result: ThreeLevelFlatten = doc.parse(doc.get_root_id()).unwrap();
576
577        assert_eq!(
578            result,
579            ThreeLevelFlatten {
580                a: 1,
581                b: 2,
582                c: 3,
583                d: 4,
584                e: 5
585            }
586        );
587    }
588
589    #[test]
590    fn test_nested_flatten_catches_unaccessed_field() {
591        // Document: { a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 }
592        //
593        // Parsing structure (NOT accessing f):
594        // Level 1: field(a), flatten() →
595        //   Level 2: field(b), field(c), flatten() →
596        //     Level 3: field(d), field(e), deny_unknown_fields()
597        //   Level 2: deny_unknown_fields()
598        // Level 1: deny_unknown_fields() <- Should FAIL because f is not accessed
599        //
600        // Expected: Level 1's deny_unknown_fields() should fail with UnknownField("f")
601        use crate::eure;
602
603        let doc = eure!({ a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 });
604        let result: Result<ThreeLevelFlatten, _> = doc.parse(doc.get_root_id());
605
606        assert_eq!(
607            result.unwrap_err().kind,
608            ParseErrorKind::UnknownField("f".to_string())
609        );
610    }
611
612    #[test]
613    fn test_flatten_union_reverts_accessed_fields_on_failure() {
614        use crate::eure;
615
616        let doc = eure!({
617            a = 1
618            b = 2
619            c = 3
620            d = 4
621        });
622
623        // Define enum with two variants
624        #[derive(Debug, PartialEq)]
625        enum TestOption {
626            A { a: i32, c: i32, e: i32 },
627            B { a: i32, b: i32 },
628        }
629
630        impl<'doc> ParseDocument<'doc> for TestOption {
631            type Error = ParseError;
632
633            fn parse(ctx: &ParseContext<'doc>) -> Result<Self, Self::Error> {
634                ctx.parse_union(VariantRepr::default())?
635                    .variant("A", |ctx: &ParseContext<'_>| {
636                        let rec = ctx.parse_record()?;
637                        let a = rec.parse_field("a")?;
638                        let c = rec.parse_field("c")?;
639                        let e = rec.parse_field("e")?; // Will fail - field doesn't exist
640                        rec.deny_unknown_fields()?;
641                        Ok(TestOption::A { a, c, e })
642                    })
643                    .variant("B", |ctx: &ParseContext<'_>| {
644                        let rec = ctx.parse_record()?;
645                        let a = rec.parse_field("a")?;
646                        let b = rec.parse_field("b")?;
647                        rec.deny_unknown_fields()?;
648                        Ok(TestOption::B { a, b })
649                    })
650                    .parse()
651            }
652        }
653
654        // Parse with flatten
655        let root_id = doc.get_root_id();
656        let root_ctx = ParseContext::new(&doc, root_id);
657        let record = root_ctx.parse_record().unwrap();
658
659        // Parse union - should succeed with VariantB
660        let option = record.flatten().parse::<TestOption>().unwrap();
661        assert_eq!(option, TestOption::B { a: 1, b: 2 });
662
663        // Access field d
664        let d: i32 = record.parse_field("d").unwrap();
665        assert_eq!(d, 4);
666
667        // BUG: This should FAIL because field 'c' was never accessed by VariantB
668        // (the successful variant), but it SUCCEEDS because VariantA tried 'c'
669        // before failing
670        let result = record.deny_unknown_fields();
671
672        assert_eq!(
673            result.unwrap_err(),
674            ParseError {
675                node_id: root_id,
676                kind: ParseErrorKind::UnknownField("c".to_string()),
677            }
678        );
679    }
680}