Skip to main content

eure_schema/
parse.rs

1//! FromEure implementations for schema types.
2//!
3//! This module provides two categories of types:
4//!
5//! 1. **FromEure implementations for existing types** - Types that don't contain
6//!    `SchemaNodeId` can implement `FromEure` directly (e.g., `BindingStyle`, `TextSchema`).
7//!
8//! 2. **Parsed types** - Syntactic representations of schema types that use `NodeId`
9//!    instead of `SchemaNodeId` (e.g., `ParsedArraySchema`, `ParsedRecordSchema`).
10//!
11//! # Architecture
12//!
13//! ```text
14//! EureDocument
15//!     ↓ FromEure trait
16//! ParsedSchemaNode, ParsedArraySchema, ...
17//!     ↓ Converter (convert.rs)
18//! SchemaDocument, SchemaNode, ArraySchema, ...
19//! ```
20
21use eure_document::data_model::VariantRepr;
22use eure_document::document::NodeId;
23use eure_document::identifier::Identifier;
24use eure_document::parse::{FromEure, ParseContext, ParseError, ParseErrorKind};
25use indexmap::{IndexMap, IndexSet};
26use num_bigint::BigInt;
27
28use crate::{BindingStyle, Description, TextSchema, TypeReference};
29
30impl FromEure<'_> for TypeReference {
31    type Error = ParseError;
32    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
33        // TypeReference is parsed from a path like `$types.my-type` or `$types.namespace.type`
34        // The path is stored as text in inline code format
35        let path: &str = ctx.parse()?;
36
37        // Parse the path: should start with "$types." followed by name or namespace.name
38        let path = path.strip_prefix("$types.").ok_or_else(|| ParseError {
39            node_id: ctx.node_id(),
40            kind: ParseErrorKind::InvalidPattern {
41                kind: "type reference".to_string(),
42                reason: format!(
43                    "expected '$types.<name>' or '$types.<namespace>.<name>', got '{}'",
44                    path
45                ),
46            },
47        })?;
48
49        // Split by '.' to get parts
50        let parts: Vec<&str> = path.split('.').collect();
51        match parts.as_slice() {
52            [name] => {
53                let name: Identifier = name.parse().map_err(|e| ParseError {
54                    node_id: ctx.node_id(),
55                    kind: ParseErrorKind::InvalidIdentifier(e),
56                })?;
57                Ok(TypeReference {
58                    namespace: None,
59                    name,
60                })
61            }
62            [namespace, name] => {
63                let name: Identifier = name.parse().map_err(|e| ParseError {
64                    node_id: ctx.node_id(),
65                    kind: ParseErrorKind::InvalidIdentifier(e),
66                })?;
67                Ok(TypeReference {
68                    namespace: Some((*namespace).to_string()),
69                    name,
70                })
71            }
72            _ => Err(ParseError {
73                node_id: ctx.node_id(),
74                kind: ParseErrorKind::InvalidPattern {
75                    kind: "type reference".to_string(),
76                    reason: format!(
77                        "expected '$types.<name>' or '$types.<namespace>.<name>', got '$types.{}'",
78                        path
79                    ),
80                },
81            }),
82        }
83    }
84}
85
86impl FromEure<'_> for crate::SchemaRef {
87    type Error = ParseError;
88
89    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
90        let schema_ctx = ctx.ext("schema")?;
91
92        let path: String = schema_ctx.parse()?;
93        Ok(crate::SchemaRef {
94            path,
95            node_id: schema_ctx.node_id(),
96        })
97    }
98}
99
100// ============================================================================
101// Parsed types (contain NodeId instead of SchemaNodeId)
102// ============================================================================
103
104/// Parsed integer schema - syntactic representation with range as string.
105#[derive(Debug, Clone, eure_macros::FromEure)]
106#[eure(crate = eure_document, rename_all = "kebab-case")]
107pub struct ParsedIntegerSchema {
108    /// Range constraint as string (e.g., "[0, 100)", "(-∞, 0]")
109    #[eure(default)]
110    pub range: Option<String>,
111    /// Multiple-of constraint
112    #[eure(default)]
113    pub multiple_of: Option<BigInt>,
114}
115
116/// Parsed float schema - syntactic representation with range as string.
117#[derive(Debug, Clone, eure_macros::FromEure)]
118#[eure(crate = eure_document, rename_all = "kebab-case")]
119pub struct ParsedFloatSchema {
120    /// Range constraint as string
121    #[eure(default)]
122    pub range: Option<String>,
123    /// Multiple-of constraint
124    #[eure(default)]
125    pub multiple_of: Option<f64>,
126    /// Precision constraint ("f32" or "f64")
127    #[eure(default)]
128    pub precision: Option<String>,
129}
130
131/// Parsed array schema with NodeId references.
132#[derive(Debug, Clone, eure_macros::FromEure)]
133#[eure(crate = eure_document, rename_all = "kebab-case")]
134pub struct ParsedArraySchema {
135    /// Schema for array elements
136    pub item: NodeId,
137    /// Minimum number of elements
138    #[eure(default)]
139    pub min_length: Option<u32>,
140    /// Maximum number of elements
141    #[eure(default)]
142    pub max_length: Option<u32>,
143    /// All elements must be unique
144    #[eure(default)]
145    pub unique: bool,
146    /// Array must contain at least one element matching this schema
147    #[eure(default)]
148    pub contains: Option<NodeId>,
149    /// Binding style for formatting
150    #[eure(ext, default)]
151    pub binding_style: Option<BindingStyle>,
152}
153
154/// Parsed map schema with NodeId references.
155#[derive(Debug, Clone, eure_macros::FromEure)]
156#[eure(crate = eure_document, rename_all = "kebab-case")]
157pub struct ParsedMapSchema {
158    /// Schema for keys
159    pub key: NodeId,
160    /// Schema for values
161    pub value: NodeId,
162    /// Minimum number of key-value pairs
163    #[eure(default)]
164    pub min_size: Option<u32>,
165    /// Maximum number of key-value pairs
166    #[eure(default)]
167    pub max_size: Option<u32>,
168}
169
170/// Parsed record field schema with NodeId reference.
171#[derive(Debug, Clone, eure_macros::FromEure)]
172#[eure(crate = eure_document, parse_ext, rename_all = "kebab-case")]
173pub struct ParsedRecordFieldSchema {
174    /// Schema for this field's value (NodeId reference)
175    #[eure(flatten_ext)]
176    pub schema: NodeId,
177    /// Field is optional (defaults to false = required)
178    #[eure(default)]
179    pub optional: bool,
180    /// Binding style for this field
181    #[eure(default)]
182    pub binding_style: Option<BindingStyle>,
183}
184
185/// Policy for handling fields not defined in record properties.
186#[derive(Debug, Clone, Default)]
187pub enum ParsedUnknownFieldsPolicy {
188    /// Deny unknown fields (default, strict)
189    #[default]
190    Deny,
191    /// Allow any unknown fields without validation
192    Allow,
193    /// Unknown fields must match this schema (NodeId reference)
194    Schema(NodeId),
195}
196
197impl FromEure<'_> for ParsedUnknownFieldsPolicy {
198    type Error = ParseError;
199    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
200        let node = ctx.node();
201        let node_id = ctx.node_id();
202
203        // Check if it's a text value that could be a policy literal ("deny" or "allow")
204        if let NodeValue::Primitive(PrimitiveValue::Text(text)) = &node.content {
205            // Only treat plaintext (not inline code) as policy literals
206            if text.language == Language::Plaintext {
207                return match text.as_str() {
208                    "deny" => Ok(ParsedUnknownFieldsPolicy::Deny),
209                    "allow" => Ok(ParsedUnknownFieldsPolicy::Allow),
210                    _ => Err(ParseError {
211                        node_id,
212                        kind: ParseErrorKind::UnknownVariant(text.as_str().to_string()),
213                    }),
214                };
215            }
216        }
217
218        // Otherwise treat as schema NodeId (including inline code like `integer`)
219        Ok(ParsedUnknownFieldsPolicy::Schema(node_id))
220    }
221}
222
223/// Parsed record schema with NodeId references.
224#[derive(Debug, Clone, Default)]
225pub struct ParsedRecordSchema {
226    /// Fixed field schemas (field name -> field schema with metadata)
227    pub properties: IndexMap<String, ParsedRecordFieldSchema>,
228    /// Schemas to be flattened into this record
229    pub flatten: Vec<NodeId>,
230    /// Policy for unknown/additional fields
231    pub unknown_fields: ParsedUnknownFieldsPolicy,
232}
233
234impl FromEure<'_> for ParsedRecordSchema {
235    type Error = ParseError;
236    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
237        // Parse $unknown-fields extension
238        let unknown_fields = ctx
239            .parse_ext_optional::<ParsedUnknownFieldsPolicy>("unknown-fields")?
240            .unwrap_or_default();
241
242        // Parse $flatten extension - list of schemas to flatten into this record
243        let flatten = ctx
244            .parse_ext_optional::<Vec<NodeId>>("flatten")?
245            .unwrap_or_default();
246
247        // Parse all fields in the map as record properties
248        let rec = ctx.parse_record()?;
249        let mut properties = IndexMap::new();
250
251        for result in rec.unknown_fields() {
252            let (field_name, field_ctx) = result.map_err(|(key, ctx)| ParseError {
253                node_id: ctx.node_id(),
254                kind: ParseErrorKind::InvalidKeyType(key.clone()),
255            })?;
256            let field_schema = ParsedRecordFieldSchema::parse(&field_ctx)?;
257            properties.insert(field_name.to_string(), field_schema);
258        }
259
260        Ok(ParsedRecordSchema {
261            properties,
262            flatten,
263            unknown_fields,
264        })
265    }
266}
267
268/// Parsed tuple schema with NodeId references.
269#[derive(Debug, Clone, eure_macros::FromEure)]
270#[eure(crate = eure_document, rename_all = "kebab-case")]
271pub struct ParsedTupleSchema {
272    /// Schema for each element by position (NodeId references)
273    pub elements: Vec<NodeId>,
274    /// Binding style for formatting
275    #[eure(ext, default)]
276    pub binding_style: Option<BindingStyle>,
277}
278
279/// Parsed union schema with NodeId references.
280#[derive(Debug, Clone)]
281pub struct ParsedUnionSchema {
282    /// Variant definitions (variant name -> schema NodeId)
283    pub variants: IndexMap<String, NodeId>,
284    /// Variants that use unambiguous semantics (try all, detect conflicts).
285    /// All other variants use short-circuit semantics (first match wins).
286    pub unambiguous: IndexSet<String>,
287    /// Variant representation strategy
288    pub repr: VariantRepr,
289    /// Variants that deny untagged matching (require explicit $variant)
290    pub deny_untagged: IndexSet<String>,
291}
292
293impl FromEure<'_> for ParsedUnionSchema {
294    type Error = ParseError;
295    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
296        let rec = ctx.parse_record()?;
297        let mut variants = IndexMap::new();
298        let mut unambiguous = IndexSet::new();
299        let mut deny_untagged = IndexSet::new();
300
301        // Check for variants = { ... } field
302        if let Some(variants_ctx) = rec.field_optional("variants") {
303            let variants_rec = variants_ctx.parse_record()?;
304            for result in variants_rec.unknown_fields() {
305                let (name, var_ctx) = result.map_err(|(key, ctx)| ParseError {
306                    node_id: ctx.node_id(),
307                    kind: ParseErrorKind::InvalidKeyType(key.clone()),
308                })?;
309                variants.insert(name.to_string(), var_ctx.node_id());
310
311                // Parse extensions on the variant value
312                if var_ctx
313                    .parse_ext_optional::<bool>("deny-untagged")?
314                    .unwrap_or(false)
315                {
316                    deny_untagged.insert(name.to_string());
317                }
318                if var_ctx
319                    .parse_ext_optional::<bool>("unambiguous")?
320                    .unwrap_or(false)
321                {
322                    unambiguous.insert(name.to_string());
323                }
324            }
325        }
326
327        rec.allow_unknown_fields()?;
328
329        // Parse $variant-repr extension
330        let repr = ctx
331            .parse_ext_optional::<VariantRepr>("variant-repr")?
332            .unwrap_or_default();
333
334        Ok(ParsedUnionSchema {
335            variants,
336            unambiguous,
337            repr,
338            deny_untagged,
339        })
340    }
341}
342
343/// Parsed extension type schema with NodeId reference.
344#[derive(Debug, Clone, eure_macros::FromEure)]
345#[eure(crate = eure_document, parse_ext)]
346pub struct ParsedExtTypeSchema {
347    /// Schema for the extension value (NodeId reference)
348    #[eure(flatten_ext)]
349    pub schema: NodeId,
350    /// Whether the extension is optional (default: false = required)
351    #[eure(default)]
352    pub optional: bool,
353}
354
355/// Parsed schema metadata - extension metadata via $ext-type on $types.type.
356#[derive(Debug, Clone, Default)]
357pub struct ParsedSchemaMetadata {
358    /// Documentation/description
359    pub description: Option<Description>,
360    /// Marks as deprecated
361    pub deprecated: bool,
362    /// Default value (NodeId reference, not Value)
363    pub default: Option<NodeId>,
364    /// Example values as NodeId references
365    pub examples: Option<Vec<NodeId>>,
366}
367
368impl ParsedSchemaMetadata {
369    /// Parse metadata from a node's extensions.
370    pub fn parse_from_extensions(ctx: &ParseContext<'_>) -> Result<Self, ParseError> {
371        let description = ctx.parse_ext_optional::<Description>("description")?;
372        let deprecated = ctx
373            .parse_ext_optional::<bool>("deprecated")?
374            .unwrap_or(false);
375        let default = ctx.ext_optional("default").map(|ctx| ctx.node_id());
376        let examples = ctx.parse_ext_optional::<Vec<NodeId>>("examples")?;
377
378        Ok(ParsedSchemaMetadata {
379            description,
380            deprecated,
381            default,
382            examples,
383        })
384    }
385}
386
387/// Parsed schema node content - the type definition with NodeId references.
388#[derive(Debug, Clone)]
389pub enum ParsedSchemaNodeContent {
390    /// Any type - accepts any valid Eure value
391    Any,
392    /// Text type with constraints
393    Text(TextSchema),
394    /// Integer type with constraints
395    Integer(ParsedIntegerSchema),
396    /// Float type with constraints
397    Float(ParsedFloatSchema),
398    /// Boolean type (no constraints)
399    Boolean,
400    /// Null type
401    Null,
402    /// Literal type - accepts only the exact specified value (NodeId to the literal)
403    Literal(NodeId),
404    /// Array type with item schema
405    Array(ParsedArraySchema),
406    /// Map type with dynamic keys
407    Map(ParsedMapSchema),
408    /// Record type with fixed named fields
409    Record(ParsedRecordSchema),
410    /// Tuple type with fixed-length ordered elements
411    Tuple(ParsedTupleSchema),
412    /// Union type with named variants
413    Union(ParsedUnionSchema),
414    /// Type reference
415    Reference(TypeReference),
416}
417
418/// Parsed schema node - full syntactic representation of a schema node.
419#[derive(Debug, Clone)]
420pub struct ParsedSchemaNode {
421    /// The type definition content
422    pub content: ParsedSchemaNodeContent,
423    /// Cascading metadata
424    pub metadata: ParsedSchemaMetadata,
425    /// Extension type definitions for this node
426    pub ext_types: IndexMap<Identifier, ParsedExtTypeSchema>,
427}
428
429// ============================================================================
430// Helper functions for parsing schema node content
431// ============================================================================
432
433use eure_document::document::node::NodeValue;
434use eure_document::text::Language;
435use eure_document::value::{PrimitiveValue, ValueKind};
436
437/// Get the $variant extension value as a string if present.
438fn get_variant_string(ctx: &ParseContext<'_>) -> Result<Option<String>, ParseError> {
439    let variant_ctx = ctx.ext_optional("variant");
440
441    match variant_ctx {
442        Some(var_ctx) => {
443            let node = var_ctx.node();
444            match &node.content {
445                NodeValue::Primitive(PrimitiveValue::Text(t)) => Ok(Some(t.as_str().to_string())),
446                _ => Err(ParseError {
447                    node_id: var_ctx.node_id(),
448                    kind: ParseErrorKind::TypeMismatch {
449                        expected: ValueKind::Text,
450                        actual: node.content.value_kind().unwrap_or(ValueKind::Null),
451                    },
452                }),
453            }
454        }
455        None => Ok(None),
456    }
457}
458
459/// Parse a type reference string (e.g., "text", "integer", "$types.typename").
460/// Returns ParsedSchemaNodeContent for the referenced type.
461fn parse_type_reference_string(
462    node_id: NodeId,
463    s: &str,
464) -> Result<ParsedSchemaNodeContent, ParseError> {
465    if s.is_empty() {
466        return Err(ParseError {
467            node_id,
468            kind: ParseErrorKind::InvalidPattern {
469                kind: "type reference".to_string(),
470                reason: "expected non-empty type reference, got empty string".to_string(),
471            },
472        });
473    }
474
475    let segments: Vec<&str> = s.split('.').collect();
476    match segments.as_slice() {
477        // Primitive types
478        ["text"] => Ok(ParsedSchemaNodeContent::Text(TextSchema::default())),
479        ["integer"] => Ok(ParsedSchemaNodeContent::Integer(ParsedIntegerSchema {
480            range: None,
481            multiple_of: None,
482        })),
483        ["float"] => Ok(ParsedSchemaNodeContent::Float(ParsedFloatSchema {
484            range: None,
485            multiple_of: None,
486            precision: None,
487        })),
488        ["boolean"] => Ok(ParsedSchemaNodeContent::Boolean),
489        ["null"] => Ok(ParsedSchemaNodeContent::Null),
490        ["any"] => Ok(ParsedSchemaNodeContent::Any),
491
492        // Text with language: text.rust, text.email, etc.
493        ["text", lang] => Ok(ParsedSchemaNodeContent::Text(TextSchema {
494            language: Some((*lang).to_string()),
495            ..Default::default()
496        })),
497
498        // Local type reference: $types.typename
499        ["$types", type_name] => {
500            let name: Identifier = type_name.parse().map_err(|e| ParseError {
501                node_id,
502                kind: ParseErrorKind::InvalidIdentifier(e),
503            })?;
504            Ok(ParsedSchemaNodeContent::Reference(TypeReference {
505                namespace: None,
506                name,
507            }))
508        }
509
510        // External type reference: $types.namespace.typename
511        ["$types", namespace, type_name] => {
512            let name: Identifier = type_name.parse().map_err(|e| ParseError {
513                node_id,
514                kind: ParseErrorKind::InvalidIdentifier(e),
515            })?;
516            Ok(ParsedSchemaNodeContent::Reference(TypeReference {
517                namespace: Some((*namespace).to_string()),
518                name,
519            }))
520        }
521
522        // Invalid pattern
523        _ => Err(ParseError {
524            node_id,
525            kind: ParseErrorKind::InvalidPattern {
526                kind: "type reference".to_string(),
527                reason: format!(
528                    "expected 'text', 'integer', '$types.name', etc., got '{}'",
529                    s
530                ),
531            },
532        }),
533    }
534}
535
536/// Parse a primitive value as a schema node content.
537fn parse_primitive_as_schema(
538    ctx: &ParseContext<'_>,
539    prim: &PrimitiveValue,
540) -> Result<ParsedSchemaNodeContent, ParseError> {
541    let node_id = ctx.node_id();
542    match prim {
543        PrimitiveValue::Text(t) => {
544            match &t.language {
545                // Inline code without language tag or eure-path: `text`, `$types.user`
546                Language::Implicit => parse_type_reference_string(node_id, t.as_str()),
547                Language::Other(lang) if lang == "eure-path" => {
548                    parse_type_reference_string(node_id, t.as_str())
549                }
550                // Plaintext string "..." or other language - treat as literal
551                _ => Ok(ParsedSchemaNodeContent::Literal(node_id)),
552            }
553        }
554        // Other primitives are literals
555        _ => Ok(ParsedSchemaNodeContent::Literal(node_id)),
556    }
557}
558
559/// Parse a map node as a schema node content based on the variant.
560fn parse_map_as_schema(
561    ctx: &ParseContext<'_>,
562    variant: Option<String>,
563) -> Result<ParsedSchemaNodeContent, ParseError> {
564    let node_id = ctx.node_id();
565    match variant.as_deref() {
566        Some("text") => Ok(ParsedSchemaNodeContent::Text(ctx.parse()?)),
567        Some("integer") => Ok(ParsedSchemaNodeContent::Integer(ctx.parse()?)),
568        Some("float") => Ok(ParsedSchemaNodeContent::Float(ctx.parse()?)),
569        Some("boolean") => Ok(ParsedSchemaNodeContent::Boolean),
570        Some("null") => Ok(ParsedSchemaNodeContent::Null),
571        Some("any") => Ok(ParsedSchemaNodeContent::Any),
572        Some("array") => Ok(ParsedSchemaNodeContent::Array(ctx.parse()?)),
573        Some("map") => Ok(ParsedSchemaNodeContent::Map(ctx.parse()?)),
574        Some("tuple") => Ok(ParsedSchemaNodeContent::Tuple(ctx.parse()?)),
575        Some("union") => Ok(ParsedSchemaNodeContent::Union(ctx.parse()?)),
576        Some("literal") => Ok(ParsedSchemaNodeContent::Literal(node_id)),
577        Some("record") | None => Ok(ParsedSchemaNodeContent::Record(ctx.parse()?)),
578        Some(other) => Err(ParseError {
579            node_id,
580            kind: ParseErrorKind::UnknownVariant(other.to_string()),
581        }),
582    }
583}
584
585impl FromEure<'_> for ParsedSchemaNodeContent {
586    type Error = ParseError;
587    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
588        let node_id = ctx.node_id();
589        let node = ctx.node();
590        let variant = get_variant_string(ctx)?;
591
592        match &node.content {
593            NodeValue::Hole(_) => Err(ParseError {
594                node_id,
595                kind: ParseErrorKind::UnexpectedHole,
596            }),
597
598            NodeValue::Primitive(prim) => {
599                // Check if this is explicitly a literal variant
600                if variant.as_deref() == Some("literal") {
601                    return Ok(ParsedSchemaNodeContent::Literal(node_id));
602                }
603                parse_primitive_as_schema(ctx, prim)
604            }
605
606            NodeValue::Array(arr) => {
607                // Array shorthand: [type] represents an array schema
608                if arr.len() == 1 {
609                    Ok(ParsedSchemaNodeContent::Array(ParsedArraySchema {
610                        item: arr.get(0).unwrap(),
611                        min_length: None,
612                        max_length: None,
613                        unique: false,
614                        contains: None,
615                        binding_style: None,
616                    }))
617                } else {
618                    Err(ParseError {
619                        node_id,
620                        kind: ParseErrorKind::InvalidPattern {
621                            kind: "array schema shorthand".to_string(),
622                            reason: format!(
623                                "expected single-element array [type], got {}-element array",
624                                arr.len()
625                            ),
626                        },
627                    })
628                }
629            }
630
631            NodeValue::Tuple(tup) => {
632                // Tuple shorthand: (type1, type2, ...) represents a tuple schema
633                Ok(ParsedSchemaNodeContent::Tuple(ParsedTupleSchema {
634                    elements: tup.to_vec(),
635                    binding_style: None,
636                }))
637            }
638
639            NodeValue::Map(_) => parse_map_as_schema(ctx, variant),
640        }
641    }
642}
643
644impl FromEure<'_> for ParsedSchemaNode {
645    type Error = ParseError;
646    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
647        // Create a flattened context so child parsers' deny_unknown_* are no-ops.
648        // All accesses are recorded in the shared accessed set (via Rc).
649        let flatten_ctx = ctx.flatten();
650
651        // Parse schema-level extensions - marks $ext-type, $description, etc. as accessed
652        let ext_types = parse_ext_types(&flatten_ctx)?;
653        let metadata = ParsedSchemaMetadata::parse_from_extensions(&flatten_ctx)?;
654
655        // Content parsing uses the flattened context
656        let content = flatten_ctx.parse::<ParsedSchemaNodeContent>()?;
657
658        // Note: We do NOT validate unknown extensions here because:
659        // 1. At the document root, $types extension is handled by the converter
660        // 2. Content types use flatten context, so their deny is already no-op
661        // The caller (e.g., Converter) should handle document-level validation if needed.
662
663        Ok(ParsedSchemaNode {
664            content,
665            metadata,
666            ext_types,
667        })
668    }
669}
670
671/// Parse the $ext-type extension as a map of extension schemas.
672fn parse_ext_types(
673    ctx: &ParseContext<'_>,
674) -> Result<IndexMap<Identifier, ParsedExtTypeSchema>, ParseError> {
675    let ext_type_ctx = ctx.ext_optional("ext-type");
676
677    let mut result = IndexMap::new();
678
679    if let Some(ext_type_ctx) = ext_type_ctx {
680        let rec = ext_type_ctx.parse_record()?;
681        // Collect all extension names first to avoid borrowing issues
682        let ext_fields: Vec<_> = rec
683            .unknown_fields()
684            .map(|r| {
685                r.map_err(|(key, ctx)| ParseError {
686                    node_id: ctx.node_id(),
687                    kind: ParseErrorKind::InvalidKeyType(key.clone()),
688                })
689            })
690            .collect::<Result<Vec<_>, _>>()?;
691
692        for (name, type_ctx) in ext_fields {
693            let ident: Identifier = name.parse().map_err(|e| ParseError {
694                node_id: ext_type_ctx.node_id(),
695                kind: ParseErrorKind::InvalidIdentifier(e),
696            })?;
697            let schema = type_ctx.parse::<ParsedExtTypeSchema>()?;
698            result.insert(ident, schema);
699        }
700
701        // Allow unknown fields since we've processed all via unknown_fields() iterator
702        // (unknown_fields() doesn't mark fields as accessed, so we can't use deny_unknown_fields)
703        rec.allow_unknown_fields()?;
704    }
705
706    Ok(result)
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712    use eure_document::document::EureDocument;
713    use eure_document::document::node::NodeValue;
714    use eure_document::text::Text;
715    use eure_document::value::PrimitiveValue;
716
717    fn create_text_node(doc: &mut EureDocument, text: &str) -> NodeId {
718        let root_id = doc.get_root_id();
719        doc.node_mut(root_id).content =
720            NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(text.to_string())));
721        root_id
722    }
723
724    #[test]
725    fn test_binding_style_parse() {
726        let mut doc = EureDocument::new();
727        let node_id = create_text_node(&mut doc, "section");
728
729        let result: BindingStyle = doc.parse(node_id).unwrap();
730        assert_eq!(result, BindingStyle::Section);
731    }
732
733    #[test]
734    fn test_binding_style_parse_unknown() {
735        let mut doc = EureDocument::new();
736        let node_id = create_text_node(&mut doc, "unknown");
737
738        let result: Result<BindingStyle, _> = doc.parse(node_id);
739        let err = result.unwrap_err();
740        assert_eq!(
741            err.kind,
742            ParseErrorKind::UnknownVariant("unknown".to_string())
743        );
744    }
745
746    #[test]
747    fn test_description_parse_default() {
748        let mut doc = EureDocument::new();
749        let node_id = create_text_node(&mut doc, "Hello world");
750
751        let result: Description = doc.parse(node_id).unwrap();
752        assert!(matches!(result, Description::String(s) if s == "Hello world"));
753    }
754
755    #[test]
756    fn test_variant_repr_parse_string() {
757        let mut doc = EureDocument::new();
758        let node_id = create_text_node(&mut doc, "untagged");
759
760        let result: VariantRepr = doc.parse(node_id).unwrap();
761        assert_eq!(result, VariantRepr::Untagged);
762    }
763}