eure_document/parse/
record.rs

1//! RecordParser and ExtParser for parsing record types from Eure documents.
2
3extern crate alloc;
4
5use alloc::format;
6use std::collections::HashSet;
7use std::sync::OnceLock;
8
9use crate::document::node::NodeMap;
10use crate::prelude_internal::*;
11
12use super::{ParseDocument, ParseError, ParseErrorKind};
13
14/// Helper for parsing record (map with string keys) from Eure documents.
15///
16/// Tracks accessed fields for unknown field checking.
17///
18/// # Example
19///
20/// ```ignore
21/// impl ParseDocument<'_> for User {
22///     fn parse(doc: &EureDocument, node_id: NodeId) -> Result<Self, ParseError> {
23///         let mut rec = doc.parse_record(node_id)?;
24///         let name = rec.field::<String>("name")?;
25///         let age = rec.field_optional::<u32>("age")?;
26///         rec.deny_unknown_fields()?;
27///         Ok(User { name, age })
28///     }
29/// }
30/// ```
31pub struct RecordParser<'doc> {
32    doc: &'doc EureDocument,
33    node_id: NodeId,
34    map: &'doc NodeMap,
35    accessed: HashSet<String>,
36}
37
38impl<'doc> RecordParser<'doc> {
39    /// Create a new RecordParser for the given node.
40    pub(crate) fn new(doc: &'doc EureDocument, node_id: NodeId, map: &'doc NodeMap) -> Self {
41        Self {
42            doc,
43            node_id,
44            map,
45            accessed: HashSet::new(),
46        }
47    }
48
49    /// Get the node ID being parsed.
50    pub fn node_id(&self) -> NodeId {
51        self.node_id
52    }
53
54    /// Get a required field.
55    ///
56    /// Returns `ParseErrorKind::MissingField` if the field is not present.
57    pub fn field<T: ParseDocument<'doc>>(&mut self, name: &str) -> Result<T, ParseError> {
58        self.accessed.insert(name.to_string());
59        let field_node_id = self
60            .map
61            .get(&ObjectKey::String(name.to_string()))
62            .ok_or_else(|| ParseError {
63                node_id: self.node_id,
64                kind: ParseErrorKind::MissingField(name.to_string()),
65            })?;
66        T::parse(self.doc, field_node_id)
67    }
68
69    /// Get an optional field.
70    ///
71    /// Returns `Ok(None)` if the field is not present.
72    pub fn field_optional<T: ParseDocument<'doc>>(
73        &mut self,
74        name: &str,
75    ) -> Result<Option<T>, ParseError> {
76        self.accessed.insert(name.to_string());
77        match self.map.get(&ObjectKey::String(name.to_string())) {
78            Some(field_node_id) => Ok(Some(T::parse(self.doc, field_node_id)?)),
79            None => Ok(None),
80        }
81    }
82
83    /// Get the NodeId for a field (for manual handling).
84    ///
85    /// Returns `ParseErrorKind::MissingField` if the field is not present.
86    pub fn field_node(&mut self, name: &str) -> Result<NodeId, ParseError> {
87        self.accessed.insert(name.to_string());
88        self.map
89            .get(&ObjectKey::String(name.to_string()))
90            .ok_or_else(|| ParseError {
91                node_id: self.node_id,
92                kind: ParseErrorKind::MissingField(name.to_string()),
93            })
94    }
95
96    /// Get the NodeId for an optional field.
97    ///
98    /// Returns `None` if the field is not present.
99    pub fn field_node_optional(&mut self, name: &str) -> Option<NodeId> {
100        self.accessed.insert(name.to_string());
101        self.map.get(&ObjectKey::String(name.to_string()))
102    }
103
104    /// Finish parsing with Deny policy (error if unknown fields exist).
105    ///
106    /// This also errors if the map contains non-string keys, as records
107    /// should only have string-keyed fields.
108    pub fn deny_unknown_fields(self) -> Result<(), ParseError> {
109        for (key, _) in self.map.iter() {
110            match key {
111                ObjectKey::String(name) => {
112                    if !self.accessed.contains(name.as_str()) {
113                        return Err(ParseError {
114                            node_id: self.node_id,
115                            kind: ParseErrorKind::UnknownField(name.clone()),
116                        });
117                    }
118                }
119                // Non-string keys are invalid in records
120                other => {
121                    return Err(ParseError {
122                        node_id: self.node_id,
123                        kind: ParseErrorKind::InvalidKeyType(other.clone()),
124                    });
125                }
126            }
127        }
128        Ok(())
129    }
130
131    /// Finish parsing with Allow policy (allow unknown string fields).
132    ///
133    /// This still errors if the map contains non-string keys, as records
134    /// should only have string-keyed fields.
135    pub fn allow_unknown_fields(self) -> Result<(), ParseError> {
136        // Check for non-string keys (invalid in records)
137        for (key, _) in self.map.iter() {
138            if !matches!(key, ObjectKey::String(_)) {
139                return Err(ParseError {
140                    node_id: self.node_id,
141                    kind: ParseErrorKind::InvalidKeyType(key.clone()),
142                });
143            }
144        }
145        Ok(())
146    }
147
148    /// Get an iterator over unknown fields (for Schema policy or custom handling).
149    ///
150    /// Returns (field_name, node_id) pairs for fields that haven't been accessed.
151    pub fn unknown_fields(&self) -> impl Iterator<Item = (&'doc str, NodeId)> + '_ {
152        self.map.iter().filter_map(|(key, &node_id)| {
153            if let ObjectKey::String(name) = key
154                && !self.accessed.contains(name.as_str())
155            {
156                return Some((name.as_str(), node_id));
157            }
158            None
159        })
160    }
161}
162
163/// Helper for parsing extension types ($ext-type) from Eure documents.
164///
165/// Similar API to RecordParser but for extension type fields.
166///
167/// # Example
168///
169/// ```ignore
170/// let mut ext = doc.parse_extension(node_id);
171/// let optional = ext.field_optional::<bool>("optional")?;
172/// let binding_style = ext.field_optional::<BindingStyle>("binding-style")?;
173/// ext.allow_unknown_fields()?;
174/// ```
175pub struct ExtParser<'doc> {
176    doc: &'doc EureDocument,
177    node_id: NodeId,
178    extensions: &'doc Map<Identifier, NodeId>,
179    accessed: HashSet<Identifier>,
180}
181
182impl<'doc> ExtParser<'doc> {
183    /// Create a new ExtParser for the given node.
184    pub(crate) fn new(
185        doc: &'doc EureDocument,
186        node_id: NodeId,
187        extensions: &'doc Map<Identifier, NodeId>,
188    ) -> Self {
189        Self {
190            doc,
191            node_id,
192            extensions,
193            accessed: HashSet::new(),
194        }
195    }
196
197    /// Get the node ID being parsed.
198    pub fn node_id(&self) -> NodeId {
199        self.node_id
200    }
201
202    /// Get a required extension field.
203    ///
204    /// Returns `ParseErrorKind::MissingExtension` if the extension is not present.
205    pub fn ext<T: ParseDocument<'doc>>(&mut self, name: &str) -> Result<T, ParseError> {
206        let ident: Identifier = name.parse().map_err(|e| ParseError {
207            node_id: self.node_id,
208            kind: ParseErrorKind::InvalidIdentifier(e),
209        })?;
210        self.accessed.insert(ident.clone());
211        let ext_node_id = self.extensions.get(&ident).ok_or_else(|| ParseError {
212            node_id: self.node_id,
213            kind: ParseErrorKind::MissingExtension(name.to_string()),
214        })?;
215        T::parse(self.doc, *ext_node_id)
216    }
217
218    /// Get an optional extension field.
219    ///
220    /// Returns `Ok(None)` if the extension is not present.
221    pub fn ext_optional<T: ParseDocument<'doc>>(
222        &mut self,
223        name: &str,
224    ) -> Result<Option<T>, ParseError> {
225        let ident: Identifier = name.parse().map_err(|e| ParseError {
226            node_id: self.node_id,
227            kind: ParseErrorKind::InvalidIdentifier(e),
228        })?;
229        self.accessed.insert(ident.clone());
230        match self.extensions.get(&ident) {
231            Some(ext_node_id) => Ok(Some(T::parse(self.doc, *ext_node_id)?)),
232            None => Ok(None),
233        }
234    }
235
236    /// Get the NodeId for an extension field (for manual handling).
237    ///
238    /// Returns `ParseErrorKind::MissingExtension` if the extension is not present.
239    pub fn ext_node(&mut self, name: &str) -> Result<NodeId, ParseError> {
240        let ident: Identifier = name.parse().map_err(|e| ParseError {
241            node_id: self.node_id,
242            kind: ParseErrorKind::InvalidIdentifier(e),
243        })?;
244        self.accessed.insert(ident.clone());
245        self.extensions
246            .get(&ident)
247            .copied()
248            .ok_or_else(|| ParseError {
249                node_id: self.node_id,
250                kind: ParseErrorKind::MissingExtension(name.to_string()),
251            })
252    }
253
254    /// Get the NodeId for an optional extension field.
255    ///
256    /// Returns `None` if the extension is not present.
257    pub fn ext_node_optional(&mut self, name: &str) -> Option<NodeId> {
258        let ident: Identifier = name.parse().ok()?;
259        self.accessed.insert(ident.clone());
260        self.extensions.get(&ident).copied()
261    }
262
263    /// Finish parsing with Deny policy (error if unknown extensions exist).
264    pub fn deny_unknown_extensions(self) -> Result<(), ParseError> {
265        for (ident, _) in self.extensions.iter() {
266            if !self.accessed.contains(ident) {
267                return Err(ParseError {
268                    node_id: self.node_id,
269                    kind: ParseErrorKind::UnknownField(format!("$ext-type.{}", ident)),
270                });
271            }
272        }
273        Ok(())
274    }
275
276    /// Finish parsing with Allow policy (ignore unknown extensions).
277    pub fn allow_unknown_extensions(self) {
278        // Nothing to do - just consume self
279    }
280
281    /// Get an iterator over unknown extensions (for custom handling).
282    ///
283    /// Returns (identifier, node_id) pairs for extensions that haven't been accessed.
284    pub fn unknown_extensions(&self) -> impl Iterator<Item = (&'doc Identifier, NodeId)> + '_ {
285        self.extensions.iter().filter_map(|(ident, node_id)| {
286            if !self.accessed.contains(ident) {
287                Some((ident, *node_id))
288            } else {
289                None
290            }
291        })
292    }
293}
294
295impl EureDocument {
296    /// Get a RecordParser for parsing a record (map with string keys).
297    ///
298    /// Returns `ParseError` if the node is not a map.
299    /// Treats `Uninitialized` nodes as empty maps (useful for nodes with only extensions).
300    pub fn parse_record(&self, node_id: NodeId) -> Result<RecordParser<'_>, ParseError> {
301        // Static empty map for Uninitialized nodes
302        static EMPTY_MAP: OnceLock<NodeMap> = OnceLock::new();
303
304        let node = self.node(node_id);
305        match &node.content {
306            NodeValue::Map(map) => Ok(RecordParser::new(self, node_id, map)),
307            // Treat Uninitialized as empty map (for nodes with only extensions)
308            NodeValue::Hole => {
309                let empty = EMPTY_MAP.get_or_init(NodeMap::default);
310                Ok(RecordParser::new(self, node_id, empty))
311            }
312            value => Err(ParseError {
313                node_id,
314                kind: value
315                    .value_kind()
316                    .map(|actual| ParseErrorKind::TypeMismatch {
317                        expected: crate::value::ValueKind::Map,
318                        actual,
319                    })
320                    .unwrap_or(ParseErrorKind::UnexpectedUninitialized),
321            }),
322        }
323    }
324
325    /// Get an ExtParser for parsing extension types on a node.
326    pub fn parse_extension(&self, node_id: NodeId) -> ExtParser<'_> {
327        let node = self.node(node_id);
328        ExtParser::new(self, node_id, &node.extensions)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::value::PrimitiveValue;
336
337    fn create_test_doc() -> EureDocument {
338        let mut doc = EureDocument::new();
339        let root_id = doc.get_root_id();
340
341        // Add fields: name = "Alice", age = 30
342        let name_id = doc
343            .add_map_child(ObjectKey::String("name".to_string()), root_id)
344            .unwrap()
345            .node_id;
346        doc.node_mut(name_id).content = NodeValue::Primitive(PrimitiveValue::Text(
347            crate::text::Text::plaintext("Alice".to_string()),
348        ));
349
350        let age_id = doc
351            .add_map_child(ObjectKey::String("age".to_string()), root_id)
352            .unwrap()
353            .node_id;
354        doc.node_mut(age_id).content = NodeValue::Primitive(PrimitiveValue::Integer(30.into()));
355
356        doc
357    }
358
359    #[test]
360    fn test_record_field() {
361        let doc = create_test_doc();
362        let mut rec = doc.parse_record(doc.get_root_id()).unwrap();
363
364        let name: String = rec.field("name").unwrap();
365        assert_eq!(name, "Alice");
366    }
367
368    #[test]
369    fn test_record_field_missing() {
370        let doc = create_test_doc();
371        let mut rec = doc.parse_record(doc.get_root_id()).unwrap();
372
373        let result: Result<String, _> = rec.field("nonexistent");
374        assert!(matches!(
375            result.unwrap_err().kind,
376            ParseErrorKind::MissingField(_)
377        ));
378    }
379
380    #[test]
381    fn test_record_field_optional() {
382        let doc = create_test_doc();
383        let mut rec = doc.parse_record(doc.get_root_id()).unwrap();
384
385        let name: Option<String> = rec.field_optional("name").unwrap();
386        assert_eq!(name, Some("Alice".to_string()));
387
388        let missing: Option<String> = rec.field_optional("nonexistent").unwrap();
389        assert_eq!(missing, None);
390    }
391
392    #[test]
393    fn test_record_deny_unknown_fields() {
394        let doc = create_test_doc();
395        let mut rec = doc.parse_record(doc.get_root_id()).unwrap();
396
397        let _name: String = rec.field("name").unwrap();
398        // Didn't access "age", so deny should fail
399        let result = rec.deny_unknown_fields();
400        assert!(matches!(
401            result.unwrap_err().kind,
402            ParseErrorKind::UnknownField(_)
403        ));
404    }
405
406    #[test]
407    fn test_record_deny_unknown_fields_all_accessed() {
408        let doc = create_test_doc();
409        let mut rec = doc.parse_record(doc.get_root_id()).unwrap();
410
411        let _name: String = rec.field("name").unwrap();
412        let _age: num_bigint::BigInt = rec.field("age").unwrap();
413        // Accessed all fields, should succeed
414        rec.deny_unknown_fields().unwrap();
415    }
416
417    #[test]
418    fn test_record_allow_unknown_fields() {
419        let doc = create_test_doc();
420        let mut rec = doc.parse_record(doc.get_root_id()).unwrap();
421
422        let _name: String = rec.field("name").unwrap();
423        // Didn't access "age", but allow should succeed
424        rec.allow_unknown_fields().unwrap();
425    }
426
427    #[test]
428    fn test_record_unknown_fields_iterator() {
429        let doc = create_test_doc();
430        let mut rec = doc.parse_record(doc.get_root_id()).unwrap();
431
432        let _name: String = rec.field("name").unwrap();
433        // "age" should be in unknown fields
434        let unknown: Vec<_> = rec.unknown_fields().collect();
435        assert_eq!(unknown.len(), 1);
436        assert_eq!(unknown[0].0, "age");
437    }
438
439    #[test]
440    fn test_record_with_non_string_keys_deny_should_error() {
441        // BUG: deny_unknown_fields() silently skips non-string keys
442        // Expected: Should error when a map has numeric keys
443        // Actual: Silently ignores them
444        let mut doc = EureDocument::new();
445        let root_id = doc.get_root_id();
446
447        // Add a field with numeric key: { 0 => "value" }
448        use num_bigint::BigInt;
449        let value_id = doc
450            .add_map_child(ObjectKey::Number(BigInt::from(0)), root_id)
451            .unwrap()
452            .node_id;
453        doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Text(
454            crate::text::Text::plaintext("value".to_string()),
455        ));
456
457        let rec = doc.parse_record(doc.get_root_id()).unwrap();
458
459        // BUG: This should error because there's an unaccessed non-string key
460        // but currently it succeeds
461        let result = rec.deny_unknown_fields();
462        assert!(
463            result.is_err(),
464            "BUG: deny_unknown_fields() should error on non-string keys, but it succeeds"
465        );
466    }
467
468    #[test]
469    fn test_record_with_non_string_keys_unknown_fields_iterator() {
470        // unknown_fields() intentionally only returns string keys (signature: (&str, NodeId))
471        // Non-string keys are caught by deny_unknown_fields() instead
472        let mut doc = EureDocument::new();
473        let root_id = doc.get_root_id();
474
475        // Add a field with numeric key: { 0 => "value" }
476        use num_bigint::BigInt;
477        let value_id = doc
478            .add_map_child(ObjectKey::Number(BigInt::from(0)), root_id)
479            .unwrap()
480            .node_id;
481        doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Text(
482            crate::text::Text::plaintext("value".to_string()),
483        ));
484
485        let rec = doc.parse_record(doc.get_root_id()).unwrap();
486
487        // unknown_fields() returns empty because it only returns string keys
488        // (the numeric key is not included in the iterator by design)
489        let unknown: Vec<_> = rec.unknown_fields().collect();
490        assert_eq!(
491            unknown.len(),
492            0,
493            "unknown_fields() should only return string keys, numeric keys are excluded"
494        );
495    }
496
497    #[test]
498    fn test_ext_parser() {
499        let mut doc = EureDocument::new();
500        let root_id = doc.get_root_id();
501
502        // Add extension: $ext-type.optional = true
503        let ext_id = doc
504            .add_extension("optional".parse().unwrap(), root_id)
505            .unwrap()
506            .node_id;
507        doc.node_mut(ext_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
508
509        let mut ext = doc.parse_extension(root_id);
510        let optional: bool = ext.ext("optional").unwrap();
511        assert!(optional);
512    }
513
514    #[test]
515    fn test_ext_parser_optional_missing() {
516        let doc = EureDocument::new();
517        let root_id = doc.get_root_id();
518
519        let mut ext = doc.parse_extension(root_id);
520        let optional: Option<bool> = ext.ext_optional("optional").unwrap();
521        assert_eq!(optional, None);
522    }
523}