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 convert;
33pub mod identifiers;
34pub mod parse;
35pub mod synth;
36pub mod validate;
37
38pub use build::{BuildSchema, SchemaBuilder};
39
40use eure_document::data_model::VariantRepr;
41use eure_document::document::EureDocument;
42use eure_document::identifier::Identifier;
43use eure_macros::ParseDocument;
44use indexmap::{IndexMap, IndexSet};
45use num_bigint::BigInt;
46use regex::Regex;
47
48// ============================================================================
49// Schema Document
50// ============================================================================
51
52/// Schema document with arena-based node storage
53#[derive(Debug, Clone, PartialEq)]
54pub struct SchemaDocument {
55    /// All schema nodes stored in a flat vector
56    pub nodes: Vec<SchemaNode>,
57    /// Root node reference
58    pub root: SchemaNodeId,
59    /// Named type definitions ($types)
60    pub types: IndexMap<Identifier, SchemaNodeId>,
61}
62
63/// Extension type definition with optionality
64#[derive(Debug, Clone, PartialEq)]
65pub struct ExtTypeSchema {
66    /// Schema for the extension value
67    pub schema: SchemaNodeId,
68    /// Whether the extension is optional (default: false = required)
69    pub optional: bool,
70}
71
72/// Reference to a schema node by index
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub struct SchemaNodeId(pub usize);
75
76/// A single schema node
77#[derive(Debug, Clone, PartialEq)]
78pub struct SchemaNode {
79    /// The type definition, structure, and constraints
80    pub content: SchemaNodeContent,
81    /// Cascading metadata (description, deprecated, default, examples)
82    pub metadata: SchemaMetadata,
83    /// Extension type definitions for this node ($ext-type.X)
84    pub ext_types: IndexMap<Identifier, ExtTypeSchema>,
85}
86
87// ============================================================================
88// Schema Node Content
89// ============================================================================
90
91/// Type definitions with their specific constraints
92///
93/// See spec: `eure-schema.schema.eure` lines 298-525
94#[derive(Debug, Clone, PartialEq)]
95pub enum SchemaNodeContent {
96    // --- Primitives ---
97    /// Any type - accepts any valid Eure value
98    /// Spec: line 391
99    Any,
100
101    /// Text type
102    ///
103    /// # Language Matching
104    ///
105    /// When validating text values:
106    /// - `Language::Plaintext` (from `"..."`) must match `.text` schema only
107    /// - `Language::Implicit` (from `` `...` ``) can be coerced to any language by schema
108    /// - `Language::Other(lang)` (from `` lang`...` ``) must match `.text.{lang}` schema
109    ///
110    /// Spec: lines 333-349
111    Text(TextSchema),
112
113    /// Integer type with optional constraints
114    /// Spec: lines 360-364
115    Integer(IntegerSchema),
116
117    /// Float type with optional constraints
118    /// Spec: lines 371-375
119    Float(FloatSchema),
120
121    /// Boolean type (no constraints)
122    /// Spec: line 383
123    Boolean,
124
125    /// Null type
126    /// Spec: line 387
127    Null,
128
129    // --- Literal ---
130    /// Literal type - accepts only the exact specified value
131    /// Spec: line 396
132    Literal(EureDocument),
133
134    // --- Compounds ---
135    /// Array type with item schema and optional constraints
136    /// Spec: lines 426-439
137    Array(ArraySchema),
138
139    /// Map type with dynamic keys
140    /// Spec: lines 453-459
141    Map(MapSchema),
142
143    /// Record type with fixed named fields
144    /// Spec: lines 401-410
145    Record(RecordSchema),
146
147    /// Tuple type with fixed-length ordered elements
148    /// Spec: lines 465-468
149    Tuple(TupleSchema),
150
151    /// Union type with named variants
152    /// Spec: lines 415-423
153    Union(UnionSchema),
154
155    // --- Reference ---
156    /// Type reference (local or cross-schema)
157    /// Spec: lines 506-510
158    Reference(TypeReference),
159}
160
161// ============================================================================
162// Primitive Type Schemas
163// ============================================================================
164
165/// Boundary condition for numeric constraints
166///
167/// Uses ADT to prevent invalid states (e.g., both inclusive and exclusive)
168#[derive(Debug, Clone, PartialEq, Default)]
169pub enum Bound<T> {
170    /// No constraint (-∞ or +∞)
171    #[default]
172    Unbounded,
173    /// Inclusive bound (≤ or ≥)
174    Inclusive(T),
175    /// Exclusive bound (< or >)
176    Exclusive(T),
177}
178
179/// Text type constraints
180///
181/// The `language` field determines what kind of text is expected:
182/// - `None` - accepts any text (no language constraint)
183/// - `Some("plaintext")` - expects plaintext (from `"..."` syntax or `Language::Plaintext`)
184/// - `Some("rust")` - expects Rust code (from `` rust`...` `` syntax or `Language::Other("rust")`)
185///
186/// # Schema Syntax
187///
188/// - `.text` - any text (language=None)
189/// - `.text.X` - text with language X (e.g., `.text.rust`, `.text.email`)
190///
191/// # Validation Rules
192///
193/// When validating a `Text` value against a `TextSchema`:
194/// - `Language::Plaintext` matches schema with `language=None` or `language=Some("plaintext")`
195/// - `Language::Implicit` matches any schema (the schema's language is applied)
196/// - `Language::Other(lang)` matches schema with `language=None` or `language=Some(lang)`
197///
198/// ```eure
199/// @variants.text
200/// language = .text (optional)  # e.g., "rust", "email", "markdown"
201/// min-length = .integer (optional)
202/// max-length = .integer (optional)
203/// pattern = .text (optional)
204/// ```
205#[derive(Debug, Clone, Default, ParseDocument)]
206#[eure(crate = eure_document, rename_all = "kebab-case", allow_unknown_fields, allow_unknown_extensions)]
207pub struct TextSchema {
208    /// Language identifier (e.g., "rust", "javascript", "email", "plaintext")
209    ///
210    /// - `None` - accepts any text regardless of language
211    /// - `Some(lang)` - expects text with the specific language tag
212    ///
213    /// Note: When a value has `Language::Implicit` (from `` `...` `` syntax),
214    /// it can be coerced to match the schema's expected language.
215    #[eure(default)]
216    pub language: Option<String>,
217    /// Minimum length constraint (in UTF-8 code points)
218    #[eure(default)]
219    pub min_length: Option<u32>,
220    /// Maximum length constraint (in UTF-8 code points)
221    #[eure(default)]
222    pub max_length: Option<u32>,
223    /// Regex pattern constraint (applied to the text content).
224    /// Pre-compiled at schema parse time for efficiency.
225    #[eure(default)]
226    pub pattern: Option<Regex>,
227    /// Unknown fields (for future extensions like "flatten")
228    #[eure(flatten)]
229    pub unknown_fields: IndexMap<String, eure_document::document::NodeId>,
230}
231
232impl PartialEq for TextSchema {
233    fn eq(&self, other: &Self) -> bool {
234        self.language == other.language
235            && self.min_length == other.min_length
236            && self.max_length == other.max_length
237            && self.unknown_fields == other.unknown_fields
238            && match (&self.pattern, &other.pattern) {
239                (None, None) => true,
240                (Some(a), Some(b)) => a.as_str() == b.as_str(),
241                _ => false,
242            }
243    }
244}
245
246/// Integer type constraints
247///
248/// Spec: lines 360-364
249/// ```eure
250/// @variants.integer
251/// range = .$types.range-string (optional)
252/// multiple-of = .integer (optional)
253/// ```
254///
255/// Note: Range string is parsed in the converter to Bound<BigInt>
256#[derive(Debug, Clone, Default, PartialEq)]
257pub struct IntegerSchema {
258    /// Minimum value constraint (parsed from range string)
259    pub min: Bound<BigInt>,
260    /// Maximum value constraint (parsed from range string)
261    pub max: Bound<BigInt>,
262    /// Multiple-of constraint
263    pub multiple_of: Option<BigInt>,
264}
265
266/// Float precision specifier
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
268pub enum FloatPrecision {
269    /// 32-bit floating point (f32)
270    F32,
271    /// 64-bit floating point (f64) - default
272    #[default]
273    F64,
274}
275
276/// Float type constraints
277///
278/// Spec: lines 371-375
279/// ```eure
280/// @variants.float
281/// range = .$types.range-string (optional)
282/// multiple-of = .float (optional)
283/// precision = "f32" | "f64" (optional, default: "f64")
284/// ```
285///
286/// Note: Range string is parsed in the converter to Bound<f64>
287#[derive(Debug, Clone, Default, PartialEq)]
288pub struct FloatSchema {
289    /// Minimum value constraint (parsed from range string)
290    pub min: Bound<f64>,
291    /// Maximum value constraint (parsed from range string)
292    pub max: Bound<f64>,
293    /// Multiple-of constraint
294    pub multiple_of: Option<f64>,
295    /// Float precision (f32 or f64)
296    pub precision: FloatPrecision,
297}
298
299// ============================================================================
300// Compound Type Schemas
301// ============================================================================
302
303/// Array type constraints
304///
305/// Spec: lines 426-439
306/// ```eure
307/// @variants.array
308/// item = .$types.type
309/// min-length = .integer (optional)
310/// max-length = .integer (optional)
311/// unique = .boolean (optional)
312/// contains = .$types.type (optional)
313/// $ext-type.binding-style = .$types.binding-style (optional)
314/// ```
315#[derive(Debug, Clone, PartialEq)]
316pub struct ArraySchema {
317    /// Schema for array elements (required)
318    pub item: SchemaNodeId,
319    /// Minimum number of elements
320    pub min_length: Option<u32>,
321    /// Maximum number of elements
322    pub max_length: Option<u32>,
323    /// All elements must be unique
324    pub unique: bool,
325    /// Array must contain at least one element matching this schema
326    pub contains: Option<SchemaNodeId>,
327    /// Binding style for formatting
328    pub binding_style: Option<BindingStyle>,
329}
330
331/// Map type constraints
332///
333/// Spec: lines 453-459
334/// ```eure
335/// @variants.map
336/// key = .$types.type
337/// value = .$types.type
338/// min-size = .integer (optional)
339/// max-size = .integer (optional)
340/// ```
341#[derive(Debug, Clone, PartialEq)]
342pub struct MapSchema {
343    /// Schema for keys
344    pub key: SchemaNodeId,
345    /// Schema for values
346    pub value: SchemaNodeId,
347    /// Minimum number of key-value pairs
348    pub min_size: Option<u32>,
349    /// Maximum number of key-value pairs
350    pub max_size: Option<u32>,
351}
352
353/// Record field with per-field metadata
354///
355/// Spec: lines 401-410 (value extensions)
356/// ```eure
357/// value.$ext-type.optional = .boolean (optional)
358/// value.$ext-type.binding-style = .$types.binding-style (optional)
359/// ```
360#[derive(Debug, Clone, PartialEq)]
361pub struct RecordFieldSchema {
362    /// Schema for this field's value
363    pub schema: SchemaNodeId,
364    /// Field is optional (defaults to false = required)
365    pub optional: bool,
366    /// Binding style for this field
367    pub binding_style: Option<BindingStyle>,
368}
369
370/// Record type with fixed named fields
371///
372/// Spec: lines 401-410
373/// ```eure
374/// @variants.record
375/// $variant: map
376/// key = .text
377/// value = .$types.type
378/// $ext-type.unknown-fields = .$types.unknown-fields-policy (optional)
379/// ```
380#[derive(Debug, Clone, Default, PartialEq)]
381pub struct RecordSchema {
382    /// Fixed field schemas (field name -> field schema with metadata)
383    pub properties: IndexMap<String, RecordFieldSchema>,
384    /// Policy for unknown/additional fields (default: deny)
385    pub unknown_fields: UnknownFieldsPolicy,
386}
387
388/// Policy for handling fields not defined in record properties
389///
390/// Spec: lines 240-251
391/// ```eure
392/// @ $types.unknown-fields-policy
393/// @variants.deny = "deny"
394/// @variants.allow = "allow"
395/// @variants.schema = .$types.type
396/// ```
397#[derive(Debug, Clone, Default, PartialEq)]
398pub enum UnknownFieldsPolicy {
399    /// Deny unknown fields (default, strict)
400    #[default]
401    Deny,
402    /// Allow any unknown fields without validation
403    Allow,
404    /// Unknown fields must match this schema
405    Schema(SchemaNodeId),
406}
407
408/// Tuple type with fixed-length ordered elements
409///
410/// Spec: lines 465-468
411/// ```eure
412/// @variants.tuple
413/// elements = [.$types.type]
414/// $ext-type.binding-style = .$types.binding-style (optional)
415/// ```
416#[derive(Debug, Clone, PartialEq)]
417pub struct TupleSchema {
418    /// Schema for each element by position
419    pub elements: Vec<SchemaNodeId>,
420    /// Binding style for formatting
421    pub binding_style: Option<BindingStyle>,
422}
423
424/// Union type with named variants
425///
426/// Spec: lines 415-423
427/// ```eure
428/// @variants.union
429/// variants = { $variant: map, key => .text, value => .$types.type }
430/// $ext-type.variant-repr = .$types.variant-repr (optional)
431/// ```
432#[derive(Debug, Clone, PartialEq)]
433pub struct UnionSchema {
434    /// Variant definitions (variant name -> schema)
435    pub variants: IndexMap<String, SchemaNodeId>,
436    /// Variants that use unambiguous semantics (try all, detect conflicts).
437    /// All other variants use short-circuit semantics (first match wins).
438    pub unambiguous: IndexSet<String>,
439    /// Variant representation strategy (default: External)
440    pub repr: VariantRepr,
441    /// Variants that deny untagged matching (require explicit $variant)
442    pub deny_untagged: IndexSet<String>,
443}
444
445// ============================================================================
446// Binding Style
447// ============================================================================
448
449/// How to represent document paths in formatted output
450///
451/// Spec: lines 263-296
452/// ```eure
453/// @ $types.binding-style
454/// $variant: union
455/// variants { auto, passthrough, section, nested, binding, section-binding, section-root-binding }
456/// ```
457#[derive(Debug, Clone, PartialEq, Eq, Default, ParseDocument)]
458#[eure(crate = ::eure_document, rename_all = "kebab-case")]
459pub enum BindingStyle {
460    /// Automatically determine the best representation
461    #[default]
462    Auto,
463    /// Pass through; defer to subsequent keys
464    Passthrough,
465    /// Create a new section (@ a.b.c)
466    Section,
467    /// Create a nested section (@ a.b.c { ... })
468    Nested,
469    /// Bind value (a.b.c = value)
470    Binding,
471    /// Section with block (a.b.c { ... })
472    SectionBinding,
473    /// Section with root binding (@ a.b.c = value)
474    SectionRootBinding,
475}
476
477// ============================================================================
478// Type Reference
479// ============================================================================
480
481/// Type reference (local or cross-schema)
482///
483/// - Local reference: `$types.my-type`
484/// - Cross-schema reference: `$types.namespace.type-name`
485#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct TypeReference {
487    /// Namespace for cross-schema references (None for local refs)
488    pub namespace: Option<String>,
489    /// Type name
490    pub name: Identifier,
491}
492
493// ============================================================================
494// Metadata
495// ============================================================================
496
497/// Description can be plain string or markdown
498///
499/// Spec: lines 312-316
500/// ```eure
501/// description => { $variant: union, variants.string => .text, variants.markdown => .text.markdown }
502/// ```
503#[derive(Debug, Clone, PartialEq, ParseDocument)]
504#[eure(crate = eure_document, rename_all = "lowercase")]
505pub enum Description {
506    /// Plain text description
507    String(String),
508    /// Markdown formatted description
509    Markdown(String),
510}
511
512/// Schema metadata (available at any nesting level via $ext-type on $types.type)
513///
514/// ```eure
515/// description => union { string, .text.markdown } (optional)
516/// deprecated => .boolean (optional)
517/// default => .any (optional)
518/// examples => [`any`] (optional)
519/// ```
520///
521/// Note: `optional` and `binding_style` are per-field extensions stored in `RecordFieldSchema`
522#[derive(Debug, Clone, Default, PartialEq)]
523pub struct SchemaMetadata {
524    /// Documentation/description
525    pub description: Option<Description>,
526    /// Marks as deprecated
527    pub deprecated: bool,
528    /// Default value for optional fields
529    pub default: Option<EureDocument>,
530    /// Example values as Eure documents
531    pub examples: Option<Vec<EureDocument>>,
532}
533
534// ============================================================================
535// Implementation
536// ============================================================================
537
538impl SchemaDocument {
539    /// Create a new empty schema document
540    pub fn new() -> Self {
541        Self {
542            nodes: vec![SchemaNode {
543                content: SchemaNodeContent::Any,
544                metadata: SchemaMetadata::default(),
545                ext_types: IndexMap::new(),
546            }],
547            root: SchemaNodeId(0),
548            types: IndexMap::new(),
549        }
550    }
551
552    /// Get a reference to a node
553    pub fn node(&self, id: SchemaNodeId) -> &SchemaNode {
554        &self.nodes[id.0]
555    }
556
557    /// Get a mutable reference to a node
558    pub fn node_mut(&mut self, id: SchemaNodeId) -> &mut SchemaNode {
559        &mut self.nodes[id.0]
560    }
561
562    /// Create a new node and return its ID
563    pub fn create_node(&mut self, content: SchemaNodeContent) -> SchemaNodeId {
564        let id = SchemaNodeId(self.nodes.len());
565        self.nodes.push(SchemaNode {
566            content,
567            metadata: SchemaMetadata::default(),
568            ext_types: IndexMap::new(),
569        });
570        id
571    }
572
573    /// Register a named type
574    pub fn register_type(&mut self, name: Identifier, node_id: SchemaNodeId) {
575        self.types.insert(name, node_id);
576    }
577
578    /// Look up a named type
579    pub fn get_type(&self, name: &Identifier) -> Option<SchemaNodeId> {
580        self.types.get(name).copied()
581    }
582}
583
584impl Default for SchemaDocument {
585    fn default() -> Self {
586        Self::new()
587    }
588}
589
590// ============================================================================
591// Schema Reference
592// ============================================================================
593
594/// Reference to a schema file from `$schema` extension.
595///
596/// This type is used to extract the schema path from a document's root node.
597/// The `$schema` extension specifies the path to the schema file that should
598/// be used to validate the document.
599///
600/// # Example
601///
602/// ```eure
603/// $schema = "./person.schema.eure"
604/// name = "John"
605/// age = 30
606/// ```
607#[derive(Debug, Clone)]
608pub struct SchemaRef {
609    /// Path to the schema file
610    pub path: String,
611    /// NodeId where the $schema was defined (for error reporting)
612    pub node_id: eure_document::document::NodeId,
613}