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}