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::document::NodeId;
22use eure_document::identifier::Identifier;
23use eure_document::parse::{FromEure, ParseContext, ParseError, ParseErrorKind};
24use indexmap::{IndexMap, IndexSet};
25use num_bigint::BigInt;
26
27use crate::interop::UnionInterop;
28use crate::{BindingStyle, Description, FieldCodegen, 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    /// Field-level codegen metadata.
184    #[eure(default)]
185    pub codegen: Option<FieldCodegen>,
186}
187
188/// Policy for handling fields not defined in record properties.
189#[derive(Debug, Clone, Default)]
190pub enum ParsedUnknownFieldsPolicy {
191    /// Deny unknown fields (default, strict)
192    #[default]
193    Deny,
194    /// Allow any unknown fields without validation
195    Allow,
196    /// Unknown fields must match this schema (NodeId reference)
197    Schema(NodeId),
198}
199
200impl FromEure<'_> for ParsedUnknownFieldsPolicy {
201    type Error = ParseError;
202    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
203        let node = ctx.node();
204        let node_id = ctx.node_id();
205
206        // Check if it's a text value that could be a policy literal ("deny" or "allow")
207        if let NodeValue::Primitive(PrimitiveValue::Text(text)) = &node.content {
208            // Only treat plaintext (not inline code) as policy literals
209            if text.language == Language::Plaintext {
210                return match text.as_str() {
211                    "deny" => Ok(ParsedUnknownFieldsPolicy::Deny),
212                    "allow" => Ok(ParsedUnknownFieldsPolicy::Allow),
213                    _ => Err(ParseError {
214                        node_id,
215                        kind: ParseErrorKind::UnknownVariant(text.as_str().to_string()),
216                    }),
217                };
218            }
219        }
220
221        // Otherwise treat as schema NodeId (including inline code like `integer`)
222        Ok(ParsedUnknownFieldsPolicy::Schema(node_id))
223    }
224}
225
226/// Parsed record schema with NodeId references.
227#[derive(Debug, Clone, Default)]
228pub struct ParsedRecordSchema {
229    /// Fixed field schemas (field name -> field schema with metadata)
230    pub properties: IndexMap<String, ParsedRecordFieldSchema>,
231    /// Schemas to be flattened into this record
232    pub flatten: Vec<NodeId>,
233    /// Policy for unknown/additional fields
234    pub unknown_fields: ParsedUnknownFieldsPolicy,
235}
236
237impl FromEure<'_> for ParsedRecordSchema {
238    type Error = ParseError;
239    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
240        // Parse $unknown-fields extension
241        let unknown_fields = ctx
242            .parse_ext_optional::<ParsedUnknownFieldsPolicy>("unknown-fields")?
243            .unwrap_or_default();
244
245        // Parse $flatten extension - list of schemas to flatten into this record
246        let flatten = ctx
247            .parse_ext_optional::<Vec<NodeId>>("flatten")?
248            .unwrap_or_default();
249
250        // Parse all fields in the map as record properties
251        let rec = ctx.parse_record()?;
252        let mut properties = IndexMap::new();
253
254        for result in rec.unknown_fields() {
255            let (field_name, field_ctx) = result.map_err(|(key, ctx)| ParseError {
256                node_id: ctx.node_id(),
257                kind: ParseErrorKind::InvalidKeyType(key.clone()),
258            })?;
259            let field_schema = ParsedRecordFieldSchema::parse(&field_ctx)?;
260            properties.insert(field_name.to_string(), field_schema);
261        }
262
263        Ok(ParsedRecordSchema {
264            properties,
265            flatten,
266            unknown_fields,
267        })
268    }
269}
270
271/// Parsed tuple schema with NodeId references.
272#[derive(Debug, Clone, eure_macros::FromEure)]
273#[eure(crate = eure_document, rename_all = "kebab-case")]
274pub struct ParsedTupleSchema {
275    /// Schema for each element by position (NodeId references)
276    pub elements: Vec<NodeId>,
277    /// Binding style for formatting
278    #[eure(ext, default)]
279    pub binding_style: Option<BindingStyle>,
280}
281
282/// Parsed union schema with NodeId references.
283#[derive(Debug, Clone)]
284pub struct ParsedUnionSchema {
285    /// Variant definitions (variant name -> schema NodeId)
286    pub variants: IndexMap<String, NodeId>,
287    /// Variants that use unambiguous semantics (try all, detect conflicts).
288    /// All other variants use short-circuit semantics (first match wins).
289    pub unambiguous: IndexSet<String>,
290    /// Interop metadata (e.g. wire-level variant representation).
291    pub interop: UnionInterop,
292    /// Variants that deny untagged matching (require explicit $variant)
293    pub deny_untagged: IndexSet<String>,
294}
295
296impl FromEure<'_> for ParsedUnionSchema {
297    type Error = ParseError;
298    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
299        let rec = ctx.parse_record()?;
300        let mut variants = IndexMap::new();
301        let mut unambiguous = IndexSet::new();
302        let mut deny_untagged = IndexSet::new();
303
304        // Check for variants = { ... } field
305        if let Some(variants_ctx) = rec.field_optional("variants") {
306            let variants_rec = variants_ctx.parse_record()?;
307            for result in variants_rec.unknown_fields() {
308                let (name, var_ctx) = result.map_err(|(key, ctx)| ParseError {
309                    node_id: ctx.node_id(),
310                    kind: ParseErrorKind::InvalidKeyType(key.clone()),
311                })?;
312                variants.insert(name.to_string(), var_ctx.node_id());
313
314                // Parse extensions on the variant value
315                if var_ctx
316                    .parse_ext_optional::<bool>("deny-untagged")?
317                    .unwrap_or(false)
318                {
319                    deny_untagged.insert(name.to_string());
320                }
321                if var_ctx
322                    .parse_ext_optional::<bool>("unambiguous")?
323                    .unwrap_or(false)
324                {
325                    unambiguous.insert(name.to_string());
326                }
327            }
328        }
329
330        rec.allow_unknown_fields()?;
331
332        // Legacy extension removed from schema semantics.
333        if ctx.ext_optional("variant-repr").is_some() {
334            return Err(ParseError {
335                node_id: ctx.node_id(),
336                kind: ParseErrorKind::InvalidPattern {
337                    kind: "legacy extension".to_string(),
338                    reason: "`$variant-repr` is removed; use `$interop.variant-repr`".to_string(),
339                },
340            });
341        }
342
343        let interop = ctx
344            .parse_ext_optional::<UnionInterop>("interop")?
345            .unwrap_or_default();
346
347        Ok(ParsedUnionSchema {
348            variants,
349            unambiguous,
350            interop,
351            deny_untagged,
352        })
353    }
354}
355
356/// Parsed extension type schema with NodeId reference.
357#[derive(Debug, Clone, eure_macros::FromEure)]
358#[eure(crate = eure_document, parse_ext)]
359pub struct ParsedExtTypeSchema {
360    /// Schema for the extension value (NodeId reference)
361    #[eure(flatten_ext)]
362    pub schema: NodeId,
363    /// Whether the extension is optional (default: false = required)
364    #[eure(default)]
365    pub optional: bool,
366    /// Binding style for the extension value.
367    #[eure(default)]
368    pub binding_style: Option<BindingStyle>,
369}
370
371/// Parsed schema metadata - extension metadata via $ext-type on $types.type.
372#[derive(Debug, Clone, Default)]
373pub struct ParsedSchemaMetadata {
374    /// Documentation/description
375    pub description: Option<Description>,
376    /// Marks as deprecated
377    pub deprecated: bool,
378    /// Default value (NodeId reference, not Value)
379    pub default: Option<NodeId>,
380    /// Example values as NodeId references
381    pub examples: Option<Vec<NodeId>>,
382}
383
384impl ParsedSchemaMetadata {
385    /// Parse metadata from a node's extensions.
386    pub fn parse_from_extensions(ctx: &ParseContext<'_>) -> Result<Self, ParseError> {
387        let description = ctx.parse_ext_optional::<Description>("description")?;
388        let deprecated = ctx
389            .parse_ext_optional::<bool>("deprecated")?
390            .unwrap_or(false);
391        let default = ctx.ext_optional("default").map(|ctx| ctx.node_id());
392        let examples = ctx.parse_ext_optional::<Vec<NodeId>>("examples")?;
393
394        Ok(ParsedSchemaMetadata {
395            description,
396            deprecated,
397            default,
398            examples,
399        })
400    }
401}
402
403/// Parsed schema node content - the type definition with NodeId references.
404#[derive(Debug, Clone)]
405pub enum ParsedSchemaNodeContent {
406    /// Any type - accepts any valid Eure value
407    Any,
408    /// Text type with constraints
409    Text(TextSchema),
410    /// Integer type with constraints
411    Integer(ParsedIntegerSchema),
412    /// Float type with constraints
413    Float(ParsedFloatSchema),
414    /// Boolean type (no constraints)
415    Boolean,
416    /// Null type
417    Null,
418    /// Literal type - accepts only the exact specified value (NodeId to the literal)
419    Literal(NodeId),
420    /// Array type with item schema
421    Array(ParsedArraySchema),
422    /// Map type with dynamic keys
423    Map(ParsedMapSchema),
424    /// Record type with fixed named fields
425    Record(ParsedRecordSchema),
426    /// Tuple type with fixed-length ordered elements
427    Tuple(ParsedTupleSchema),
428    /// Union type with named variants
429    Union(ParsedUnionSchema),
430    /// Type reference
431    Reference(TypeReference),
432}
433
434/// Parsed schema node - full syntactic representation of a schema node.
435#[derive(Debug, Clone)]
436pub struct ParsedSchemaNode {
437    /// The type definition content
438    pub content: ParsedSchemaNodeContent,
439    /// Cascading metadata
440    pub metadata: ParsedSchemaMetadata,
441    /// Extension type definitions for this node
442    pub ext_types: IndexMap<Identifier, ParsedExtTypeSchema>,
443    /// Optional type-level codegen extension node.
444    pub codegen: Option<NodeId>,
445}
446
447// ============================================================================
448// Helper functions for parsing schema node content
449// ============================================================================
450
451use eure_document::document::node::NodeValue;
452use eure_document::text::Language;
453use eure_document::value::{PrimitiveValue, ValueKind};
454
455/// Get the $variant extension value as a string if present.
456fn get_variant_string(ctx: &ParseContext<'_>) -> Result<Option<String>, ParseError> {
457    let variant_ctx = ctx.ext_optional("variant");
458
459    match variant_ctx {
460        Some(var_ctx) => {
461            let node = var_ctx.node();
462            match &node.content {
463                NodeValue::Primitive(PrimitiveValue::Text(t)) => Ok(Some(t.as_str().to_string())),
464                _ => Err(ParseError {
465                    node_id: var_ctx.node_id(),
466                    kind: ParseErrorKind::TypeMismatch {
467                        expected: ValueKind::Text,
468                        actual: node.content.value_kind(),
469                    },
470                }),
471            }
472        }
473        None => Ok(None),
474    }
475}
476
477/// Parse a type reference string (e.g., "text", "integer", "$types.typename").
478/// Returns ParsedSchemaNodeContent for the referenced type.
479fn parse_type_reference_string(
480    node_id: NodeId,
481    s: &str,
482) -> Result<ParsedSchemaNodeContent, ParseError> {
483    if s.is_empty() {
484        return Err(ParseError {
485            node_id,
486            kind: ParseErrorKind::InvalidPattern {
487                kind: "type reference".to_string(),
488                reason: "expected non-empty type reference, got empty string".to_string(),
489            },
490        });
491    }
492
493    let segments: Vec<&str> = s.split('.').collect();
494    match segments.as_slice() {
495        // Primitive types
496        ["text"] => Ok(ParsedSchemaNodeContent::Text(TextSchema::default())),
497        ["integer"] => Ok(ParsedSchemaNodeContent::Integer(ParsedIntegerSchema {
498            range: None,
499            multiple_of: None,
500        })),
501        ["float"] => Ok(ParsedSchemaNodeContent::Float(ParsedFloatSchema {
502            range: None,
503            multiple_of: None,
504            precision: None,
505        })),
506        ["boolean"] => Ok(ParsedSchemaNodeContent::Boolean),
507        ["null"] => Ok(ParsedSchemaNodeContent::Null),
508        ["any"] => Ok(ParsedSchemaNodeContent::Any),
509
510        // Text with language: text.rust, text.email, etc.
511        ["text", lang] => Ok(ParsedSchemaNodeContent::Text(TextSchema {
512            language: Some((*lang).to_string()),
513            ..Default::default()
514        })),
515
516        // Local type reference: $types.typename
517        ["$types", type_name] => {
518            let name: Identifier = type_name.parse().map_err(|e| ParseError {
519                node_id,
520                kind: ParseErrorKind::InvalidIdentifier(e),
521            })?;
522            Ok(ParsedSchemaNodeContent::Reference(TypeReference {
523                namespace: None,
524                name,
525            }))
526        }
527
528        // External type reference: $types.namespace.typename
529        ["$types", namespace, type_name] => {
530            let name: Identifier = type_name.parse().map_err(|e| ParseError {
531                node_id,
532                kind: ParseErrorKind::InvalidIdentifier(e),
533            })?;
534            Ok(ParsedSchemaNodeContent::Reference(TypeReference {
535                namespace: Some((*namespace).to_string()),
536                name,
537            }))
538        }
539
540        // Invalid pattern
541        _ => Err(ParseError {
542            node_id,
543            kind: ParseErrorKind::InvalidPattern {
544                kind: "type reference".to_string(),
545                reason: format!(
546                    "expected 'text', 'integer', '$types.name', etc., got '{}'",
547                    s
548                ),
549            },
550        }),
551    }
552}
553
554/// Parse a primitive value as a schema node content.
555fn parse_primitive_as_schema(
556    ctx: &ParseContext<'_>,
557    prim: &PrimitiveValue,
558) -> Result<ParsedSchemaNodeContent, ParseError> {
559    let node_id = ctx.node_id();
560    match prim {
561        PrimitiveValue::Text(t) => {
562            match &t.language {
563                // Inline code without language tag or eure-path: `text`, `$types.user`
564                Language::Implicit => parse_type_reference_string(node_id, t.as_str()),
565                Language::Other(lang) if lang == "eure-path" => {
566                    parse_type_reference_string(node_id, t.as_str())
567                }
568                // Plaintext string "..." or other language - treat as literal
569                _ => Ok(ParsedSchemaNodeContent::Literal(node_id)),
570            }
571        }
572        // Other primitives are literals
573        _ => Ok(ParsedSchemaNodeContent::Literal(node_id)),
574    }
575}
576
577/// Parse a map node as a schema node content based on the variant.
578fn parse_map_as_schema(
579    ctx: &ParseContext<'_>,
580    variant: Option<String>,
581) -> Result<ParsedSchemaNodeContent, ParseError> {
582    let node_id = ctx.node_id();
583    match variant.as_deref() {
584        Some("text") => Ok(ParsedSchemaNodeContent::Text(ctx.parse()?)),
585        Some("integer") => Ok(ParsedSchemaNodeContent::Integer(ctx.parse()?)),
586        Some("float") => Ok(ParsedSchemaNodeContent::Float(ctx.parse()?)),
587        Some("boolean") => Ok(ParsedSchemaNodeContent::Boolean),
588        Some("null") => Ok(ParsedSchemaNodeContent::Null),
589        Some("any") => Ok(ParsedSchemaNodeContent::Any),
590        Some("array") => Ok(ParsedSchemaNodeContent::Array(ctx.parse()?)),
591        Some("map") => Ok(ParsedSchemaNodeContent::Map(ctx.parse()?)),
592        Some("tuple") => Ok(ParsedSchemaNodeContent::Tuple(ctx.parse()?)),
593        Some("union") => Ok(ParsedSchemaNodeContent::Union(ctx.parse()?)),
594        Some("literal") => Ok(ParsedSchemaNodeContent::Literal(node_id)),
595        Some("record") | None => Ok(ParsedSchemaNodeContent::Record(ctx.parse()?)),
596        Some(other) => Err(ParseError {
597            node_id,
598            kind: ParseErrorKind::UnknownVariant(other.to_string()),
599        }),
600    }
601}
602
603impl FromEure<'_> for ParsedSchemaNodeContent {
604    type Error = ParseError;
605    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
606        let node_id = ctx.node_id();
607        let node = ctx.node();
608        let variant = get_variant_string(ctx)?;
609
610        match &node.content {
611            NodeValue::Hole(_) => Err(ParseError {
612                node_id,
613                kind: ParseErrorKind::UnexpectedHole,
614            }),
615
616            NodeValue::Primitive(prim) => {
617                // Check if this is explicitly a literal variant
618                if variant.as_deref() == Some("literal") {
619                    return Ok(ParsedSchemaNodeContent::Literal(node_id));
620                }
621                parse_primitive_as_schema(ctx, prim)
622            }
623
624            NodeValue::Array(arr) => {
625                // Array shorthand: [type] represents an array schema
626                if arr.len() == 1 {
627                    Ok(ParsedSchemaNodeContent::Array(ParsedArraySchema {
628                        item: arr.get(0).unwrap(),
629                        min_length: None,
630                        max_length: None,
631                        unique: false,
632                        contains: None,
633                        binding_style: None,
634                    }))
635                } else {
636                    Err(ParseError {
637                        node_id,
638                        kind: ParseErrorKind::InvalidPattern {
639                            kind: "array schema shorthand".to_string(),
640                            reason: format!(
641                                "expected single-element array [type], got {}-element array",
642                                arr.len()
643                            ),
644                        },
645                    })
646                }
647            }
648
649            NodeValue::Tuple(tup) => {
650                // Tuple shorthand: (type1, type2, ...) represents a tuple schema
651                Ok(ParsedSchemaNodeContent::Tuple(ParsedTupleSchema {
652                    elements: tup.to_vec(),
653                    binding_style: None,
654                }))
655            }
656
657            NodeValue::Map(_) => parse_map_as_schema(ctx, variant),
658            NodeValue::PartialMap(_) => Err(ParseError {
659                node_id,
660                kind: ParseErrorKind::TypeMismatch {
661                    expected: ValueKind::Map,
662                    actual: ValueKind::PartialMap,
663                },
664            }),
665        }
666    }
667}
668
669impl FromEure<'_> for ParsedSchemaNode {
670    type Error = ParseError;
671    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
672        // Create a flattened context so child parsers' deny_unknown_* are no-ops.
673        // All accesses are recorded in the shared accessed set (via Rc).
674        let flatten_ctx = ctx.flatten();
675
676        // Parse schema-level extensions - marks $ext-type, $description, etc. as accessed
677        let ext_types = parse_ext_types(&flatten_ctx)?;
678        let metadata = ParsedSchemaMetadata::parse_from_extensions(&flatten_ctx)?;
679        let codegen = flatten_ctx.ext_optional("codegen").map(|ctx| ctx.node_id());
680
681        // Content parsing uses the flattened context
682        let content = flatten_ctx.parse::<ParsedSchemaNodeContent>()?;
683
684        // Note: We do NOT validate unknown extensions here because:
685        // 1. At the document root, $types extension is handled by the converter
686        // 2. Content types use flatten context, so their deny is already no-op
687        // The caller (e.g., Converter) should handle document-level validation if needed.
688
689        Ok(ParsedSchemaNode {
690            content,
691            metadata,
692            ext_types,
693            codegen,
694        })
695    }
696}
697
698/// Parse the $ext-type extension as a map of extension schemas.
699fn parse_ext_types(
700    ctx: &ParseContext<'_>,
701) -> Result<IndexMap<Identifier, ParsedExtTypeSchema>, ParseError> {
702    let ext_type_ctx = ctx.ext_optional("ext-type");
703
704    let mut result = IndexMap::new();
705
706    if let Some(ext_type_ctx) = ext_type_ctx {
707        let rec = ext_type_ctx.parse_record()?;
708        // Collect all extension names first to avoid borrowing issues
709        let ext_fields: Vec<_> = rec
710            .unknown_fields()
711            .map(|r| {
712                r.map_err(|(key, ctx)| ParseError {
713                    node_id: ctx.node_id(),
714                    kind: ParseErrorKind::InvalidKeyType(key.clone()),
715                })
716            })
717            .collect::<Result<Vec<_>, _>>()?;
718
719        for (name, type_ctx) in ext_fields {
720            let ident: Identifier = name.parse().map_err(|e| ParseError {
721                node_id: ext_type_ctx.node_id(),
722                kind: ParseErrorKind::InvalidIdentifier(e),
723            })?;
724            let schema = type_ctx.parse::<ParsedExtTypeSchema>()?;
725            result.insert(ident, schema);
726        }
727
728        // Allow unknown fields since we've processed all via unknown_fields() iterator
729        // (unknown_fields() doesn't mark fields as accessed, so we can't use deny_unknown_fields)
730        rec.allow_unknown_fields()?;
731    }
732
733    Ok(result)
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739    use crate::interop::VariantRepr;
740    use eure_document::document::EureDocument;
741    use eure_document::document::node::NodeValue;
742    use eure_document::text::Text;
743    use eure_document::value::PrimitiveValue;
744
745    fn create_text_node(doc: &mut EureDocument, text: &str) -> NodeId {
746        let root_id = doc.get_root_id();
747        doc.node_mut(root_id).content =
748            NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(text.to_string())));
749        root_id
750    }
751
752    #[test]
753    fn test_binding_style_parse() {
754        let mut doc = EureDocument::new();
755        let node_id = create_text_node(&mut doc, "section");
756
757        let result: BindingStyle = doc.parse(node_id).unwrap();
758        assert_eq!(result, BindingStyle::Section);
759    }
760
761    #[test]
762    fn test_binding_style_parse_unknown() {
763        let mut doc = EureDocument::new();
764        let node_id = create_text_node(&mut doc, "unknown");
765
766        let result: Result<BindingStyle, _> = doc.parse(node_id);
767        let err = result.unwrap_err();
768        assert_eq!(
769            err.kind,
770            ParseErrorKind::UnknownVariant("unknown".to_string())
771        );
772    }
773
774    #[test]
775    fn test_description_parse_default() {
776        let mut doc = EureDocument::new();
777        let node_id = create_text_node(&mut doc, "Hello world");
778
779        let result: Description = doc.parse(node_id).unwrap();
780        assert!(matches!(result, Description::String(s) if s == "Hello world"));
781    }
782
783    #[test]
784    fn test_variant_repr_parse_string() {
785        let mut doc = EureDocument::new();
786        let node_id = create_text_node(&mut doc, "untagged");
787
788        let result: VariantRepr = doc.parse(node_id).unwrap();
789        assert_eq!(result, VariantRepr::Untagged);
790    }
791}