Skip to main content

eure_schema/
lib.rs

1//! Eure Schema types and structures
2//!
3//! This library provides schema type definitions for Eure documents,
4//! following the specification in `assets/eure-schema.schema.eure`.
5//!
6//! # Type Variants
7//!
8//! All types are variants of `SchemaNodeContent`:
9//!
10//! **Primitives:**
11//! - `Text` - Text type with optional language and length/pattern constraints
12//! - `Integer` - Integer type with optional range and multiple-of constraints
13//! - `Float` - Float type with optional range and multiple-of constraints
14//! - `Boolean` - Boolean type (no constraints)
15//! - `Null` - Null type
16//! - `Any` - Any type (accepts any value)
17//!
18//! **Literal:**
19//! - `Literal` - Exact value match (e.g., `status = "active"`)
20//!
21//! **Compounds:**
22//! - `Record` - Fixed named fields
23//! - `Array` - Ordered list with item type
24//! - `Map` - Dynamic key-value pairs
25//! - `Tuple` - Fixed-length ordered elements
26//! - `Union` - Tagged union with named variants
27//!
28//! **Reference:**
29//! - `Reference` - Type reference (local or cross-schema)
30
31pub mod build;
32pub mod codegen;
33pub mod convert;
34pub mod identifiers;
35pub mod interop;
36pub mod navigate;
37pub mod parse;
38pub mod resolver;
39pub mod synth;
40pub mod type_path_trace;
41pub mod validate;
42pub mod write;
43
44pub use build::{BuildSchema, SchemaBuilder, SchemaNodeSpec};
45pub use codegen::{
46    CodegenDefaults, FieldCodegen, RecordCodegen, RootCodegen, TypeCodegen, UnionCodegen,
47};
48
49use eure_document::Text;
50use eure_document::constructor::DocumentConstructor;
51use eure_document::document::EureDocument;
52use eure_document::identifier::Identifier;
53use eure_document::plan::{ArrayForm, Form};
54use eure_document::write::{IntoEure, WriteError};
55use eure_macros::{FromEure, IntoEure};
56use indexmap::{IndexMap, IndexSet};
57use num_bigint::BigInt;
58use regex::Regex;
59
60use crate::interop::UnionInterop;
61use crate::resolver::ResolvedSchemaUri;
62
63// ============================================================================
64// Schema Document
65// ============================================================================
66
67/// Schema document with arena-based node storage
68#[derive(Debug, Clone, PartialEq)]
69pub struct SchemaDocument {
70    /// All schema nodes stored in a flat vector
71    pub nodes: Vec<SchemaNode>,
72    /// Root node reference
73    pub root: SchemaNodeId,
74    /// Named type definitions declared by this document's own `$types`.
75    pub types: IndexMap<Identifier, SchemaNodeId>,
76    /// Names exposed to importers via `$types.<alias>.<name>`. Always a subset
77    /// of `types.keys()`. When the source omits `$export`, this is identical to
78    /// `types.keys()`.
79    pub exports: IndexSet<Identifier>,
80    /// Imported schemas keyed by the alias declared in `$import`.
81    pub imports: IndexMap<Identifier, SchemaImport>,
82    /// Root-level codegen settings from `$codegen`.
83    pub root_codegen: RootCodegen,
84    /// Root-level default codegen settings from `$codegen-defaults`.
85    pub codegen_defaults: CodegenDefaults,
86}
87
88/// Types imported from one schema under a single alias.
89#[derive(Debug, Clone, PartialEq)]
90pub struct SchemaImport {
91    pub uri: ResolvedSchemaUri,
92    pub all_types: IndexMap<Identifier, SchemaNodeId>,
93    pub exports: IndexSet<Identifier>,
94}
95
96/// Extension type definition with optionality
97#[derive(Debug, Clone, PartialEq)]
98pub struct ExtTypeSchema {
99    /// Schema for the extension value
100    pub schema: SchemaNodeId,
101    /// Whether the extension is optional (default: false = required)
102    pub optional: bool,
103    /// Preferred binding style for the extension value.
104    pub binding_style: Option<BindingStyle>,
105}
106
107/// Reference to a schema node by index
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub struct SchemaNodeId(pub usize);
110
111/// A single schema node
112#[derive(Debug, Clone, PartialEq)]
113pub struct SchemaNode {
114    /// The type definition, structure, and constraints
115    pub content: SchemaNodeContent,
116    /// Cascading metadata (description, deprecated, default, examples)
117    pub metadata: SchemaMetadata,
118    /// Extension type definitions for this node ($ext-type.X)
119    pub ext_types: IndexMap<Identifier, ExtTypeSchema>,
120    /// Type-level codegen settings (`$codegen`) when this node is a record/union type.
121    pub type_codegen: TypeCodegen,
122}
123
124// ============================================================================
125// Schema Node Content
126// ============================================================================
127
128/// Type definitions with their specific constraints
129///
130/// See spec: `eure-schema.schema.eure` lines 298-525
131#[derive(Debug, Clone, PartialEq)]
132pub enum SchemaNodeContent {
133    // --- Primitives ---
134    /// Any type - accepts any valid Eure value
135    /// Spec: line 391
136    Any,
137
138    /// Text type
139    ///
140    /// # Language Matching
141    ///
142    /// When validating text values:
143    /// - `Language::Plaintext` (from `"..."`) must match `.text` schema only
144    /// - `Language::Implicit` (from `` `...` ``) can be coerced to any language by schema
145    /// - `Language::Other(lang)` (from `` lang`...` ``) must match `.text.{lang}` schema
146    ///
147    /// Spec: lines 333-349
148    Text(TextSchema),
149
150    /// Integer type with optional constraints
151    /// Spec: lines 360-364
152    Integer(IntegerSchema),
153
154    /// Float type with optional constraints
155    /// Spec: lines 371-375
156    Float(FloatSchema),
157
158    /// Boolean type (no constraints)
159    /// Spec: line 383
160    Boolean,
161
162    /// Null type
163    /// Spec: line 387
164    Null,
165
166    // --- Literal ---
167    /// Literal type - accepts only the exact specified value
168    /// Spec: line 396
169    Literal(EureDocument),
170
171    // --- Compounds ---
172    /// Array type with item schema and optional constraints
173    /// Spec: lines 426-439
174    Array(ArraySchema),
175
176    /// Map type with dynamic keys
177    /// Spec: lines 453-459
178    Map(MapSchema),
179
180    /// Record type with fixed named fields
181    /// Spec: lines 401-410
182    Record(RecordSchema),
183
184    /// Tuple type with fixed-length ordered elements
185    /// Spec: lines 465-468
186    Tuple(TupleSchema),
187
188    /// Union type with named variants
189    /// Spec: lines 415-423
190    Union(UnionSchema),
191
192    // --- Reference ---
193    /// Type reference (local or cross-schema)
194    /// Spec: lines 506-510
195    Reference(TypeReference),
196}
197
198/// The kind of a schema node (discriminant without data).
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200pub enum SchemaKind {
201    Any,
202    Text,
203    Integer,
204    Float,
205    Boolean,
206    Null,
207    Literal,
208    Array,
209    Map,
210    Record,
211    Tuple,
212    Union,
213    Reference,
214}
215
216impl std::fmt::Display for SchemaKind {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        let name = match self {
219            Self::Any => "any",
220            Self::Text => "text",
221            Self::Integer => "integer",
222            Self::Float => "float",
223            Self::Boolean => "boolean",
224            Self::Null => "null",
225            Self::Literal => "literal",
226            Self::Array => "array",
227            Self::Map => "map",
228            Self::Record => "record",
229            Self::Tuple => "tuple",
230            Self::Union => "union",
231            Self::Reference => "reference",
232        };
233        write!(f, "{}", name)
234    }
235}
236
237impl SchemaNodeContent {
238    /// Returns the kind of this schema node.
239    pub fn kind(&self) -> SchemaKind {
240        match self {
241            Self::Any => SchemaKind::Any,
242            Self::Text(_) => SchemaKind::Text,
243            Self::Integer(_) => SchemaKind::Integer,
244            Self::Float(_) => SchemaKind::Float,
245            Self::Boolean => SchemaKind::Boolean,
246            Self::Null => SchemaKind::Null,
247            Self::Literal(_) => SchemaKind::Literal,
248            Self::Array(_) => SchemaKind::Array,
249            Self::Map(_) => SchemaKind::Map,
250            Self::Record(_) => SchemaKind::Record,
251            Self::Tuple(_) => SchemaKind::Tuple,
252            Self::Union(_) => SchemaKind::Union,
253            Self::Reference(_) => SchemaKind::Reference,
254        }
255    }
256}
257
258// ============================================================================
259// Primitive Type Schemas
260// ============================================================================
261
262/// Boundary condition for numeric constraints
263///
264/// Uses ADT to prevent invalid states (e.g., both inclusive and exclusive)
265#[derive(Debug, Clone, PartialEq, Default)]
266pub enum Bound<T> {
267    /// No constraint (-∞ or +∞)
268    #[default]
269    Unbounded,
270    /// Inclusive bound (≤ or ≥)
271    Inclusive(T),
272    /// Exclusive bound (< or >)
273    Exclusive(T),
274}
275
276/// Text type constraints
277///
278/// The `language` field determines what kind of text is expected:
279/// - `None` - accepts any text (no language constraint)
280/// - `Some("plaintext")` - expects plaintext (from `"..."` syntax or `Language::Plaintext`)
281/// - `Some("rust")` - expects Rust code (from `` rust`...` `` syntax or `Language::Other("rust")`)
282///
283/// # Schema Syntax
284///
285/// - `.text` - any text (language=None)
286/// - `.text.X` - text with language X (e.g., `.text.rust`, `.text.email`)
287///
288/// # Validation Rules
289///
290/// When validating a `Text` value against a `TextSchema`:
291/// - `Language::Plaintext` matches schema with `language=None` or `language=Some("plaintext")`
292/// - `Language::Implicit` matches any schema (the schema's language is applied)
293/// - `Language::Other(lang)` matches schema with `language=None` or `language=Some(lang)`
294///
295/// ```eure
296/// @variants.text
297/// language = .text (optional)  # e.g., "rust", "email", "markdown"
298/// min-length = .integer (optional)
299/// max-length = .integer (optional)
300/// pattern = .text (optional)
301/// ```
302#[derive(Debug, Clone, Default, FromEure, IntoEure)]
303#[eure(crate = eure_document, rename_all = "kebab-case", allow_unknown_fields, allow_unknown_extensions)]
304pub struct TextSchema {
305    /// Language identifier (e.g., "rust", "javascript", "email", "plaintext")
306    ///
307    /// - `None` - accepts any text regardless of language
308    /// - `Some(lang)` - expects text with the specific language tag
309    ///
310    /// Note: When a value has `Language::Implicit` (from `` `...` `` syntax),
311    /// it can be coerced to match the schema's expected language.
312    #[eure(default)]
313    pub language: Option<String>,
314    /// Minimum length constraint (in UTF-8 code points)
315    #[eure(default)]
316    pub min_length: Option<u32>,
317    /// Maximum length constraint (in UTF-8 code points)
318    #[eure(default)]
319    pub max_length: Option<u32>,
320    /// Regex pattern constraint (applied to the text content).
321    /// Pre-compiled at schema parse time for efficiency.
322    #[eure(default)]
323    pub pattern: Option<Regex>,
324    /// Unknown fields (for future extensions like "flatten")
325    #[eure(flatten)]
326    pub unknown_fields: IndexMap<String, EureDocument>,
327}
328
329impl TextSchema {
330    pub fn is_shorthand_compatible(&self) -> bool {
331        matches!(
332            self,
333            Self {
334                language: _,
335                min_length: None,
336                max_length: None,
337                pattern: None,
338                unknown_fields: _
339            }
340        ) && self.unknown_fields.is_empty()
341    }
342    pub fn shorthand(&self) -> Option<Text> {
343        self.is_shorthand_compatible().then(|| {
344            if let Some(language) = &self.language {
345                Text::inline_implicit(format!("text.{}", language))
346            } else {
347                Text::inline_implicit("text")
348            }
349        })
350    }
351    pub fn write(&self, c: &mut DocumentConstructor) -> Result<(), WriteError> {
352        if let Some(shorthand) = self.shorthand() {
353            c.write(shorthand)
354        } else {
355            c.record(|rec| {
356                rec.constructor().set_variant("text")?;
357                <Self as IntoEure>::write_flatten(self.clone(), rec)?;
358                Ok(())
359            })
360        }
361    }
362}
363
364impl PartialEq for TextSchema {
365    fn eq(&self, other: &Self) -> bool {
366        self.language == other.language
367            && self.min_length == other.min_length
368            && self.max_length == other.max_length
369            && self.unknown_fields == other.unknown_fields
370            && match (&self.pattern, &other.pattern) {
371                (None, None) => true,
372                (Some(a), Some(b)) => a.as_str() == b.as_str(),
373                _ => false,
374            }
375    }
376}
377
378/// Integer type constraints
379///
380/// Spec: lines 360-364
381/// ```eure
382/// @variants.integer
383/// range = .$types.range-string (optional)
384/// multiple-of = .integer (optional)
385/// ```
386///
387/// Note: Range string is parsed in the converter to Bound<BigInt>
388#[derive(Debug, Clone, Default, PartialEq)]
389pub struct IntegerSchema {
390    /// Minimum value constraint (parsed from range string)
391    pub min: Bound<BigInt>,
392    /// Maximum value constraint (parsed from range string)
393    pub max: Bound<BigInt>,
394    /// Multiple-of constraint
395    pub multiple_of: Option<BigInt>,
396}
397
398/// Float precision specifier
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
400pub enum FloatPrecision {
401    /// 32-bit floating point (f32)
402    F32,
403    /// 64-bit floating point (f64) - default
404    #[default]
405    F64,
406}
407
408/// Float type constraints
409///
410/// Spec: lines 371-375
411/// ```eure
412/// @variants.float
413/// range = .$types.range-string (optional)
414/// multiple-of = .float (optional)
415/// precision = "f32" | "f64" (optional, default: "f64")
416/// ```
417///
418/// Note: Range string is parsed in the converter to Bound<f64>
419#[derive(Debug, Clone, Default, PartialEq)]
420pub struct FloatSchema {
421    /// Minimum value constraint (parsed from range string)
422    pub min: Bound<f64>,
423    /// Maximum value constraint (parsed from range string)
424    pub max: Bound<f64>,
425    /// Multiple-of constraint
426    pub multiple_of: Option<f64>,
427    /// Float precision (f32 or f64)
428    pub precision: FloatPrecision,
429}
430
431// ============================================================================
432// Compound Type Schemas
433// ============================================================================
434
435/// Array type constraints
436///
437/// Spec: lines 426-439
438/// ```eure
439/// @variants.array
440/// item = .$types.type
441/// min-length = .integer (optional)
442/// max-length = .integer (optional)
443/// unique = .boolean (optional)
444/// contains = .$types.type (optional)
445/// $ext-type.binding-style = .$types.binding-style (optional)
446/// ```
447#[derive(Debug, Clone, PartialEq)]
448pub struct ArraySchema {
449    /// Schema for array elements (required)
450    pub item: SchemaNodeId,
451    /// Minimum number of elements
452    pub min_length: Option<u32>,
453    /// Maximum number of elements
454    pub max_length: Option<u32>,
455    /// All elements must be unique
456    pub unique: bool,
457    /// Array must contain at least one element matching this schema
458    pub contains: Option<SchemaNodeId>,
459    /// Binding style for formatting
460    pub binding_style: Option<BindingStyle>,
461}
462
463/// Map type constraints
464///
465/// Spec: lines 453-459
466/// ```eure
467/// @variants.map
468/// key = .$types.type
469/// value = .$types.type
470/// min-size = .integer (optional)
471/// max-size = .integer (optional)
472/// ```
473#[derive(Debug, Clone, PartialEq)]
474pub struct MapSchema {
475    /// Schema for keys
476    pub key: SchemaNodeId,
477    /// Schema for values
478    pub value: SchemaNodeId,
479    /// Minimum number of key-value pairs
480    pub min_size: Option<u32>,
481    /// Maximum number of key-value pairs
482    pub max_size: Option<u32>,
483}
484
485/// Record field with per-field metadata
486///
487/// Spec: lines 401-410 (value extensions)
488/// ```eure
489/// value.$ext-type.optional = .boolean (optional)
490/// value.$ext-type.binding-style = .$types.binding-style (optional)
491/// ```
492#[derive(Debug, Clone, PartialEq)]
493pub struct RecordFieldSchema {
494    /// Schema for this field's value
495    pub schema: SchemaNodeId,
496    /// Field is optional (defaults to false = required)
497    pub optional: bool,
498    /// Binding style for this field
499    pub binding_style: Option<BindingStyle>,
500    /// Field-level codegen settings from `$codegen`.
501    pub field_codegen: FieldCodegen,
502}
503
504/// Record type with fixed named fields
505///
506/// Spec: lines 401-410
507/// ```eure
508/// @variants.record
509/// $variant: map
510/// key = .text
511/// value = .$types.type
512/// $ext-type.unknown-fields = .$types.unknown-fields-policy (optional)
513/// ```
514#[derive(Debug, Clone, Default, PartialEq)]
515pub struct RecordSchema {
516    /// Fixed field schemas (field name -> field schema with metadata)
517    pub properties: IndexMap<String, RecordFieldSchema>,
518    /// Schemas to flatten into this record.
519    /// Each must point to a Record or Union schema.
520    /// Fields from flattened schemas are merged into this record's field space.
521    pub flatten: Vec<SchemaNodeId>,
522    /// Policy for unknown/additional fields (default: deny)
523    pub unknown_fields: UnknownFieldsPolicy,
524}
525
526/// Policy for handling fields not defined in record properties
527///
528/// Spec: lines 240-251
529/// ```eure
530/// @ $types.unknown-fields-policy
531/// @variants.deny = "deny"
532/// @variants.allow = "allow"
533/// @variants.schema = .$types.type
534/// ```
535#[derive(Debug, Clone, Default, PartialEq)]
536pub enum UnknownFieldsPolicy {
537    /// Deny unknown fields (default, strict)
538    #[default]
539    Deny,
540    /// Allow any unknown fields without validation
541    Allow,
542    /// Unknown fields must match this schema
543    Schema(SchemaNodeId),
544}
545
546/// Tuple type with fixed-length ordered elements
547///
548/// Spec: lines 465-468
549/// ```eure
550/// @variants.tuple
551/// elements = [.$types.type]
552/// $ext-type.binding-style = .$types.binding-style (optional)
553/// ```
554#[derive(Debug, Clone, PartialEq)]
555pub struct TupleSchema {
556    /// Schema for each element by position
557    pub elements: Vec<SchemaNodeId>,
558    /// Binding style for formatting
559    pub binding_style: Option<BindingStyle>,
560}
561
562/// Union type with named variants
563///
564/// Spec: lines 415-423
565/// ```eure
566/// @variants.union
567/// variants = { $variant: map, key => .text, value => .$types.type }
568/// $ext-type.interop = .$types.union-interop (optional)
569/// ```
570#[derive(Debug, Clone, PartialEq)]
571pub struct UnionSchema {
572    /// Variant definitions (variant name -> schema)
573    pub variants: IndexMap<String, SchemaNodeId>,
574    /// Variants that use unambiguous semantics (try all, detect conflicts).
575    /// All other variants use short-circuit semantics (first match wins).
576    pub unambiguous: IndexSet<String>,
577    /// Interop metadata for non-native representations.
578    pub interop: UnionInterop,
579    /// Variants that deny untagged matching (require explicit $variant)
580    pub deny_untagged: IndexSet<String>,
581}
582
583// ============================================================================
584// Binding Style
585// ============================================================================
586
587/// How to represent document paths in formatted output.
588///
589/// Uses the seven-variant [`Form`] taxonomy from [`eure_document::plan`].
590///
591/// ```eure
592/// @ $types.binding-style
593/// $variant: union
594/// variants { inline, binding-block, binding-value-block, section, section-block, section-value-block, flatten }
595/// ```
596pub type BindingStyle = Form;
597
598/// How to represent array-valued fields.
599///
600/// Mirrors [`eure_document::plan::ArrayForm`]: orthogonal to [`BindingStyle`];
601/// describes whether array elements are emitted inline, per-element with
602/// push (`[]`) markers, or per-element with explicit indices (`[i]`).
603pub type ArrayBindingStyle = ArrayForm;
604
605// ============================================================================
606// Type Reference
607// ============================================================================
608
609/// Type reference (local, cross-schema, or resolved to a schema node).
610///
611/// - Local reference: `$types.my-type`
612/// - Cross-schema reference: `$types.namespace.type-name`
613#[derive(Debug, Clone, PartialEq, Eq)]
614pub enum TypeReference {
615    Named {
616        /// Namespace for cross-schema references (None for local refs).
617        namespace: Option<Identifier>,
618        /// Type name.
619        name: Identifier,
620    },
621    Resolved(SchemaNodeId),
622}
623
624/// A displayable type reference name.
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
626pub struct TypeReferenceName<'a> {
627    pub namespace: Option<&'a Identifier>,
628    pub name: &'a Identifier,
629}
630
631impl std::fmt::Display for TypeReferenceName<'_> {
632    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
633        if let Some(namespace) = self.namespace {
634            write!(f, "{}.{}", namespace, self.name)
635        } else {
636            write!(f, "{}", self.name)
637        }
638    }
639}
640
641// ============================================================================
642// Metadata
643// ============================================================================
644
645/// Description can be plain string or markdown
646///
647/// Spec: lines 312-316
648/// ```eure
649/// description => { $variant: union, variants.string => .text, variants.markdown => .text.markdown }
650/// ```
651#[derive(Debug, Clone, PartialEq, FromEure)]
652#[eure(crate = eure_document, rename_all = "lowercase")]
653pub enum Description {
654    /// Plain text description
655    String(String),
656    /// Markdown formatted description
657    Markdown(String),
658}
659
660/// Schema metadata (available at any nesting level via $ext-type on $types.type)
661///
662/// ```eure
663/// description => union { string, .text.markdown } (optional)
664/// deprecated => .boolean (optional)
665/// default => .any (optional)
666/// examples => [`any`] (optional)
667/// ```
668///
669/// Note: `optional` and `binding_style` are per-field extensions stored in `RecordFieldSchema`
670#[derive(Debug, Clone, Default, PartialEq)]
671pub struct SchemaMetadata {
672    /// Documentation/description
673    pub description: Option<Description>,
674    /// Marks as deprecated
675    pub deprecated: bool,
676    /// Default value for optional fields
677    pub default: Option<EureDocument>,
678    /// Example values as Eure documents
679    pub examples: Option<Vec<EureDocument>>,
680}
681
682// ============================================================================
683// Implementation
684// ============================================================================
685
686impl SchemaDocument {
687    /// Create a new empty schema document
688    pub fn new() -> Self {
689        Self {
690            nodes: vec![SchemaNode {
691                content: SchemaNodeContent::Any,
692                metadata: SchemaMetadata::default(),
693                ext_types: IndexMap::new(),
694                type_codegen: TypeCodegen::None,
695            }],
696            root: SchemaNodeId(0),
697            types: IndexMap::new(),
698            exports: IndexSet::new(),
699            imports: IndexMap::new(),
700            root_codegen: RootCodegen::default(),
701            codegen_defaults: CodegenDefaults::default(),
702        }
703    }
704
705    /// Get a reference to a node
706    pub fn node(&self, id: SchemaNodeId) -> &SchemaNode {
707        &self.nodes[id.0]
708    }
709
710    /// Get a mutable reference to a node
711    pub fn node_mut(&mut self, id: SchemaNodeId) -> &mut SchemaNode {
712        &mut self.nodes[id.0]
713    }
714
715    /// Create a new node and return its ID
716    pub fn create_node(&mut self, content: SchemaNodeContent) -> SchemaNodeId {
717        let id = SchemaNodeId(self.nodes.len());
718        self.nodes.push(SchemaNode {
719            content,
720            metadata: SchemaMetadata::default(),
721            ext_types: IndexMap::new(),
722            type_codegen: TypeCodegen::None,
723        });
724        id
725    }
726
727    /// Register a named type
728    pub fn register_type(&mut self, name: Identifier, node_id: SchemaNodeId) {
729        self.types.insert(name, node_id);
730    }
731
732    /// Look up a named type
733    pub fn get_type(&self, name: &Identifier) -> Option<SchemaNodeId> {
734        self.types.get(name).copied()
735    }
736
737    /// Resolve a type reference to an arena node ID.
738    pub fn resolve_reference(&self, reference: &TypeReference) -> Option<SchemaNodeId> {
739        match reference {
740            TypeReference::Resolved(id) => Some(*id),
741            TypeReference::Named {
742                namespace: None,
743                name,
744            } => self.types.get(name).copied(),
745            TypeReference::Named {
746                namespace: Some(namespace),
747                name,
748            } => self
749                .imports
750                .get(namespace)
751                .and_then(|import| import.all_types.get(name).copied()),
752        }
753    }
754
755    /// Return the user-facing name for a type reference when it is nameable.
756    pub fn reference_name<'a>(
757        &'a self,
758        reference: &'a TypeReference,
759    ) -> Option<TypeReferenceName<'a>> {
760        match reference {
761            TypeReference::Named { namespace, name } => Some(TypeReferenceName {
762                namespace: namespace.as_ref(),
763                name,
764            }),
765            TypeReference::Resolved(target) => {
766                if let Some((name, _)) = self.types.iter().find(|(_, id)| **id == *target) {
767                    return Some(TypeReferenceName {
768                        namespace: None,
769                        name,
770                    });
771                }
772                self.imports.iter().find_map(|(alias, import)| {
773                    import
774                        .all_types
775                        .iter()
776                        .find(|(_, id)| **id == *target)
777                        .map(|(name, _)| TypeReferenceName {
778                            namespace: Some(alias),
779                            name,
780                        })
781                })
782            }
783        }
784    }
785
786    /// Best-effort display name for diagnostics.
787    pub fn display_reference(&self, reference: &TypeReference) -> String {
788        self.reference_name(reference)
789            .map(|name| name.to_string())
790            .unwrap_or_else(|| match reference {
791                TypeReference::Resolved(id) => format!("node#{}", id.0),
792                TypeReference::Named { namespace, name } => {
793                    if let Some(namespace) = namespace {
794                        format!("{}.{}", namespace, name)
795                    } else {
796                        name.to_string()
797                    }
798                }
799            })
800    }
801}
802
803impl Default for SchemaDocument {
804    fn default() -> Self {
805        Self::new()
806    }
807}
808
809/// Build a [`LayoutPlan`] for `doc` using the schema-derived [`LayoutStrategies`].
810///
811/// This is the canonical entry point for applying schema-controlled layout to a
812/// data document: it resolves each document node's type path against `schema`,
813/// then turns each resolved trace into an explicit [`Form`] or [`ArrayForm`]
814/// assignment. Any conflict between schema-declared forms and the actual node
815/// kind surfaces as a typed [`PlanError`] instead of silently falling back to a
816/// default layout.
817pub fn layout_plan_from_schema(
818    doc: eure_document::document::EureDocument,
819    schema: &SchemaDocument,
820    strategies: &type_path_trace::LayoutStrategies,
821) -> Result<eure_document::plan::LayoutPlan, eure_document::plan::PlanError> {
822    let traces = validate::resolve_node_type_traces(&doc, schema, &strategies.schema_node_paths);
823    type_path_trace::materialize_layout_plan(doc, &traces, strategies)
824}
825
826// ============================================================================
827// Schema Reference
828// ============================================================================
829
830/// Reference to a schema file from `$schema` extension.
831///
832/// This type is used to extract the schema path from a document's root node.
833/// The `$schema` extension specifies the path to the schema file that should
834/// be used to validate the document.
835///
836/// # Example
837///
838/// ```eure
839/// $schema = "./person.schema.eure"
840/// name = "John"
841/// age = 30
842/// ```
843#[derive(Debug, Clone)]
844pub struct SchemaRef {
845    /// Path to the schema file
846    pub path: String,
847    /// NodeId where the $schema was defined (for error reporting)
848    pub node_id: eure_document::document::NodeId,
849}