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