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