Skip to main content

eure_schema/
convert.rs

1//! Conversion from EureDocument to SchemaDocument
2//!
3//! This module provides functionality to convert Eure documents containing schema definitions
4//! into SchemaDocument structures.
5//!
6//! # Schema Syntax
7//!
8//! Schema types are defined using the following syntax:
9//!
10//! **Primitives (shorthands via inline code):**
11//! - `` `text` ``, `` `integer` ``, `` `float` ``, `` `boolean` ``, `` `null` ``, `` `any` ``
12//! - `` `text.rust` ``, `` `text.email` ``, `` `text.plaintext` ``
13//!
14//! **Primitives with constraints:**
15//! ```eure
16//! @ field {
17//!   $variant = "text"
18//!   min-length = 3
19//!   max-length = 20
20//!   pattern = `^[a-z]+$`
21//! }
22//! ```
23//!
24//! **Array:** `` [`text`] `` or `` { $variant = "array", item = `text`, ... } ``
25//!
26//! **Tuple:** `` (`text`, `integer`) `` or `{ $variant = "tuple", elements = [...] }`
27//!
28//! **Record:** `` { name = `text`, age = `integer` } ``
29//!
30//! **Union with named variants:**
31//! ```eure
32//! @ field {
33//!   $variant = "union"
34//!   variants.success = { data = `any` }
35//!   variants.error = { message = `text` }
36//!   variants.error.$ext-type.unambiguous = true  // optional, for catch-all variants
37//!   $variant-repr = "untagged"  // optional
38//! }
39//! ```
40//!
41//! **Literal:** Any constant value (e.g., `{ = "active", $variant = "literal" }`, `42`, `true`)
42//!
43//! **Type reference:** `` `$types.my-type` `` or `` `$types.namespace.type` ``
44
45use crate::parse::{
46    ParsedArraySchema, ParsedExtTypeSchema, ParsedFloatSchema, ParsedIntegerSchema,
47    ParsedMapSchema, ParsedRecordSchema, ParsedSchemaMetadata, ParsedSchemaNode,
48    ParsedSchemaNodeContent, ParsedTupleSchema, ParsedUnionSchema, ParsedUnknownFieldsPolicy,
49};
50use crate::{
51    ArraySchema, Bound, ExtTypeSchema, FloatPrecision, FloatSchema, IntegerSchema, MapSchema,
52    RecordFieldSchema, RecordSchema, SchemaDocument, SchemaMetadata, SchemaNodeContent,
53    SchemaNodeId, TupleSchema, UnionSchema, UnknownFieldsPolicy,
54};
55use eure_document::document::node::{Node, NodeValue};
56use eure_document::document::{EureDocument, NodeId};
57use eure_document::identifier::Identifier;
58use eure_document::parse::ParseError;
59use eure_document::value::ObjectKey;
60use indexmap::IndexMap;
61use num_bigint::BigInt;
62use thiserror::Error;
63
64/// Errors that can occur during document to schema conversion
65#[derive(Debug, Error, Clone, PartialEq)]
66pub enum ConversionError {
67    #[error("Invalid type name: {0}")]
68    InvalidTypeName(ObjectKey),
69
70    #[error("Unsupported schema construct at path: {0}")]
71    UnsupportedConstruct(String),
72
73    #[error("Invalid extension value: {extension} at path {path}")]
74    InvalidExtensionValue { extension: String, path: String },
75
76    #[error("Invalid range string: {0}")]
77    InvalidRangeString(String),
78
79    #[error("Invalid precision: {0} (expected \"f32\" or \"f64\")")]
80    InvalidPrecision(String),
81
82    #[error("Undefined type reference: {0}")]
83    UndefinedTypeReference(String),
84
85    #[error("Parse error: {0}")]
86    ParseError(#[from] ParseError),
87}
88
89/// Mapping from schema node IDs to their source document node IDs.
90/// Used for propagating origin information for error formatting.
91pub type SchemaSourceMap = IndexMap<SchemaNodeId, NodeId>;
92
93/// Internal converter state
94struct Converter<'a> {
95    doc: &'a EureDocument,
96    schema: SchemaDocument,
97    /// Track source document NodeId for each schema node
98    source_map: SchemaSourceMap,
99}
100
101impl<'a> Converter<'a> {
102    fn new(doc: &'a EureDocument) -> Self {
103        Self {
104            doc,
105            schema: SchemaDocument::new(),
106            source_map: IndexMap::new(),
107        }
108    }
109
110    /// Convert the root node and produce the final schema with source mapping
111    fn convert(mut self) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
112        let root_id = self.doc.get_root_id();
113        let root_node = self.doc.node(root_id);
114
115        // Convert all type definitions from $types extension
116        self.convert_types(root_node)?;
117
118        // Convert root node
119        self.schema.root = self.convert_node(root_id)?;
120
121        // Validate all type references exist
122        self.validate_type_references()?;
123
124        Ok((self.schema, self.source_map))
125    }
126
127    /// Convert all type definitions from $types extension
128    fn convert_types(&mut self, node: &Node) -> Result<(), ConversionError> {
129        let types_ident: Identifier = "types".parse().unwrap();
130        if let Some(types_node_id) = node.extensions.get(&types_ident) {
131            let types_node = self.doc.node(*types_node_id);
132            if let NodeValue::Map(map) = &types_node.content {
133                for (key, &node_id) in map.iter() {
134                    if let ObjectKey::String(name) = key {
135                        let type_name: Identifier = name
136                            .parse()
137                            .map_err(|_| ConversionError::InvalidTypeName(key.clone()))?;
138                        let schema_id = self.convert_node(node_id)?;
139                        self.schema.types.insert(type_name, schema_id);
140                    } else {
141                        return Err(ConversionError::InvalidTypeName(key.clone()));
142                    }
143                }
144            } else {
145                return Err(ConversionError::InvalidExtensionValue {
146                    extension: "types".to_string(),
147                    path: "$types must be a map".to_string(),
148                });
149            }
150        }
151        Ok(())
152    }
153
154    /// Validate that all type references point to defined types
155    fn validate_type_references(&self) -> Result<(), ConversionError> {
156        for node in &self.schema.nodes {
157            if let SchemaNodeContent::Reference(type_ref) = &node.content
158                && type_ref.namespace.is_none()
159                && !self.schema.types.contains_key(&type_ref.name)
160            {
161                return Err(ConversionError::UndefinedTypeReference(
162                    type_ref.name.to_string(),
163                ));
164            }
165        }
166        Ok(())
167    }
168
169    /// Convert a document node to a schema node using ParseDocument trait
170    fn convert_node(&mut self, node_id: NodeId) -> Result<SchemaNodeId, ConversionError> {
171        // Parse the node using ParseDocument trait
172        let parsed: ParsedSchemaNode = self.doc.parse(node_id)?;
173
174        // Convert the parsed node to final schema
175        let content = self.convert_content(parsed.content)?;
176        let metadata = self.convert_metadata(parsed.metadata)?;
177        let ext_types = self.convert_ext_types(parsed.ext_types)?;
178
179        // Create the final schema node
180        let schema_id = self.schema.create_node(content);
181        self.schema.node_mut(schema_id).metadata = metadata;
182        self.schema.node_mut(schema_id).ext_types = ext_types;
183
184        // Record source mapping for span resolution
185        self.source_map.insert(schema_id, node_id);
186        Ok(schema_id)
187    }
188
189    /// Convert parsed schema node content to final schema node content
190    fn convert_content(
191        &mut self,
192        content: ParsedSchemaNodeContent,
193    ) -> Result<SchemaNodeContent, ConversionError> {
194        match content {
195            ParsedSchemaNodeContent::Any => Ok(SchemaNodeContent::Any),
196            ParsedSchemaNodeContent::Boolean => Ok(SchemaNodeContent::Boolean),
197            ParsedSchemaNodeContent::Null => Ok(SchemaNodeContent::Null),
198            ParsedSchemaNodeContent::Text(schema) => Ok(SchemaNodeContent::Text(schema)),
199            ParsedSchemaNodeContent::Reference(type_ref) => {
200                Ok(SchemaNodeContent::Reference(type_ref))
201            }
202
203            ParsedSchemaNodeContent::Integer(parsed) => Ok(SchemaNodeContent::Integer(
204                self.convert_integer_schema(parsed)?,
205            )),
206            ParsedSchemaNodeContent::Float(parsed) => {
207                Ok(SchemaNodeContent::Float(self.convert_float_schema(parsed)?))
208            }
209            ParsedSchemaNodeContent::Literal(node_id) => {
210                Ok(SchemaNodeContent::Literal(self.node_to_document(node_id)?))
211            }
212            ParsedSchemaNodeContent::Array(parsed) => {
213                Ok(SchemaNodeContent::Array(self.convert_array_schema(parsed)?))
214            }
215            ParsedSchemaNodeContent::Map(parsed) => {
216                Ok(SchemaNodeContent::Map(self.convert_map_schema(parsed)?))
217            }
218            ParsedSchemaNodeContent::Record(parsed) => Ok(SchemaNodeContent::Record(
219                self.convert_record_schema(parsed)?,
220            )),
221            ParsedSchemaNodeContent::Tuple(parsed) => {
222                Ok(SchemaNodeContent::Tuple(self.convert_tuple_schema(parsed)?))
223            }
224            ParsedSchemaNodeContent::Union(parsed) => {
225                Ok(SchemaNodeContent::Union(self.convert_union_schema(parsed)?))
226            }
227        }
228    }
229
230    /// Convert parsed integer schema (with range string) to final integer schema (with Bound)
231    fn convert_integer_schema(
232        &self,
233        parsed: ParsedIntegerSchema,
234    ) -> Result<IntegerSchema, ConversionError> {
235        let (min, max) = if let Some(range_str) = &parsed.range {
236            parse_integer_range(range_str)?
237        } else {
238            (Bound::Unbounded, Bound::Unbounded)
239        };
240
241        Ok(IntegerSchema {
242            min,
243            max,
244            multiple_of: parsed.multiple_of,
245        })
246    }
247
248    /// Convert parsed float schema (with range string) to final float schema (with Bound)
249    fn convert_float_schema(
250        &self,
251        parsed: ParsedFloatSchema,
252    ) -> Result<FloatSchema, ConversionError> {
253        let (min, max) = if let Some(range_str) = &parsed.range {
254            parse_float_range(range_str)?
255        } else {
256            (Bound::Unbounded, Bound::Unbounded)
257        };
258
259        let precision = match parsed.precision.as_deref() {
260            Some("f32") => FloatPrecision::F32,
261            Some("f64") | None => FloatPrecision::F64,
262            Some(other) => {
263                return Err(ConversionError::InvalidPrecision(other.to_string()));
264            }
265        };
266
267        Ok(FloatSchema {
268            min,
269            max,
270            multiple_of: parsed.multiple_of,
271            precision,
272        })
273    }
274
275    /// Convert parsed array schema to final array schema
276    fn convert_array_schema(
277        &mut self,
278        parsed: ParsedArraySchema,
279    ) -> Result<ArraySchema, ConversionError> {
280        let item = self.convert_node(parsed.item)?;
281        let contains = parsed
282            .contains
283            .map(|id| self.convert_node(id))
284            .transpose()?;
285
286        Ok(ArraySchema {
287            item,
288            min_length: parsed.min_length,
289            max_length: parsed.max_length,
290            unique: parsed.unique,
291            contains,
292            binding_style: parsed.binding_style,
293        })
294    }
295
296    /// Convert parsed map schema to final map schema
297    fn convert_map_schema(
298        &mut self,
299        parsed: ParsedMapSchema,
300    ) -> Result<MapSchema, ConversionError> {
301        let key = self.convert_node(parsed.key)?;
302        let value = self.convert_node(parsed.value)?;
303
304        Ok(MapSchema {
305            key,
306            value,
307            min_size: parsed.min_size,
308            max_size: parsed.max_size,
309        })
310    }
311
312    /// Convert parsed tuple schema to final tuple schema
313    fn convert_tuple_schema(
314        &mut self,
315        parsed: ParsedTupleSchema,
316    ) -> Result<TupleSchema, ConversionError> {
317        let elements: Vec<SchemaNodeId> = parsed
318            .elements
319            .iter()
320            .map(|&id| self.convert_node(id))
321            .collect::<Result<_, _>>()?;
322
323        Ok(TupleSchema {
324            elements,
325            binding_style: parsed.binding_style,
326        })
327    }
328
329    /// Convert parsed record schema to final record schema
330    fn convert_record_schema(
331        &mut self,
332        parsed: ParsedRecordSchema,
333    ) -> Result<RecordSchema, ConversionError> {
334        let mut properties = IndexMap::new();
335
336        for (field_name, field_parsed) in parsed.properties {
337            let schema = self.convert_node(field_parsed.schema)?;
338            properties.insert(
339                field_name,
340                RecordFieldSchema {
341                    schema,
342                    optional: field_parsed.optional,
343                    binding_style: field_parsed.binding_style,
344                },
345            );
346        }
347
348        // Convert flatten targets
349        let flatten = parsed
350            .flatten
351            .into_iter()
352            .map(|id| self.convert_node(id))
353            .collect::<Result<Vec<_>, _>>()?;
354
355        let unknown_fields = self.convert_unknown_fields_policy(parsed.unknown_fields)?;
356
357        Ok(RecordSchema {
358            properties,
359            flatten,
360            unknown_fields,
361        })
362    }
363
364    /// Convert parsed union schema to final union schema
365    fn convert_union_schema(
366        &mut self,
367        parsed: ParsedUnionSchema,
368    ) -> Result<UnionSchema, ConversionError> {
369        let mut variants = IndexMap::new();
370
371        for (variant_name, variant_node_id) in parsed.variants {
372            let schema = self.convert_node(variant_node_id)?;
373            variants.insert(variant_name, schema);
374        }
375
376        Ok(UnionSchema {
377            variants,
378            unambiguous: parsed.unambiguous,
379            repr: parsed.repr,
380            deny_untagged: parsed.deny_untagged,
381        })
382    }
383
384    /// Convert parsed unknown fields policy to final policy
385    fn convert_unknown_fields_policy(
386        &mut self,
387        parsed: ParsedUnknownFieldsPolicy,
388    ) -> Result<UnknownFieldsPolicy, ConversionError> {
389        match parsed {
390            ParsedUnknownFieldsPolicy::Deny => Ok(UnknownFieldsPolicy::Deny),
391            ParsedUnknownFieldsPolicy::Allow => Ok(UnknownFieldsPolicy::Allow),
392            ParsedUnknownFieldsPolicy::Schema(node_id) => {
393                let schema = self.convert_node(node_id)?;
394                Ok(UnknownFieldsPolicy::Schema(schema))
395            }
396        }
397    }
398
399    /// Convert parsed metadata to final metadata
400    fn convert_metadata(
401        &mut self,
402        parsed: ParsedSchemaMetadata,
403    ) -> Result<SchemaMetadata, ConversionError> {
404        let default = parsed
405            .default
406            .map(|id| self.node_to_document(id))
407            .transpose()?;
408
409        let examples = parsed
410            .examples
411            .map(|ids| {
412                ids.into_iter()
413                    .map(|id| self.node_to_document(id))
414                    .collect::<Result<Vec<_>, _>>()
415            })
416            .transpose()?;
417
418        Ok(SchemaMetadata {
419            description: parsed.description,
420            deprecated: parsed.deprecated,
421            default,
422            examples,
423        })
424    }
425
426    /// Convert parsed ext types to final ext types
427    fn convert_ext_types(
428        &mut self,
429        parsed: IndexMap<Identifier, ParsedExtTypeSchema>,
430    ) -> Result<IndexMap<Identifier, ExtTypeSchema>, ConversionError> {
431        let mut result = IndexMap::new();
432
433        for (name, parsed_schema) in parsed {
434            let schema = self.convert_node(parsed_schema.schema)?;
435            result.insert(
436                name,
437                ExtTypeSchema {
438                    schema,
439                    optional: parsed_schema.optional,
440                },
441            );
442        }
443
444        Ok(result)
445    }
446
447    /// Extract a subtree as a new EureDocument for literal types
448    fn node_to_document(&self, node_id: NodeId) -> Result<EureDocument, ConversionError> {
449        let mut new_doc = EureDocument::new();
450        let root_id = new_doc.get_root_id();
451        self.copy_node_to(&mut new_doc, root_id, node_id)?;
452        Ok(new_doc)
453    }
454
455    /// Recursively copy a node from source document to destination
456    fn copy_node_to(
457        &self,
458        dest: &mut EureDocument,
459        dest_node_id: NodeId,
460        src_node_id: NodeId,
461    ) -> Result<(), ConversionError> {
462        let src_node = self.doc.node(src_node_id);
463
464        // Collect child info before mutating dest
465        let children_to_copy: Vec<_> = match &src_node.content {
466            NodeValue::Primitive(prim) => {
467                dest.set_content(dest_node_id, NodeValue::Primitive(prim.clone()));
468                vec![]
469            }
470            NodeValue::Array(arr) => {
471                dest.set_content(dest_node_id, NodeValue::empty_array());
472                arr.to_vec()
473            }
474            NodeValue::Tuple(tup) => {
475                dest.set_content(dest_node_id, NodeValue::empty_tuple());
476                tup.to_vec()
477            }
478            NodeValue::Map(map) => {
479                dest.set_content(dest_node_id, NodeValue::empty_map());
480                map.iter()
481                    .map(|(k, &v)| (k.clone(), v))
482                    .collect::<Vec<_>>()
483                    .into_iter()
484                    .map(|(_, v)| v)
485                    .collect()
486            }
487            NodeValue::Hole(_) => {
488                return Err(ConversionError::UnsupportedConstruct(
489                    "Hole node".to_string(),
490                ));
491            }
492        };
493
494        // Skip ALL extensions during literal value copying.
495        // Extensions are schema metadata (like $variant, $deny-untagged, $optional, etc.)
496        // and should not be part of the literal value comparison.
497        // Literal types compare only the data structure, not metadata.
498
499        // Now copy children based on the type
500        let src_node = self.doc.node(src_node_id);
501        match &src_node.content {
502            NodeValue::Array(_) => {
503                for child_id in children_to_copy {
504                    let new_child_id = dest
505                        .add_array_element(None, dest_node_id)
506                        .map_err(|e| ConversionError::UnsupportedConstruct(e.to_string()))?
507                        .node_id;
508                    self.copy_node_to(dest, new_child_id, child_id)?;
509                }
510            }
511            NodeValue::Tuple(_) => {
512                for (index, child_id) in children_to_copy.into_iter().enumerate() {
513                    let new_child_id = dest
514                        .add_tuple_element(index as u8, dest_node_id)
515                        .map_err(|e| ConversionError::UnsupportedConstruct(e.to_string()))?
516                        .node_id;
517                    self.copy_node_to(dest, new_child_id, child_id)?;
518                }
519            }
520            NodeValue::Map(map) => {
521                for (key, &child_id) in map.iter() {
522                    let new_child_id = dest
523                        .add_map_child(key.clone(), dest_node_id)
524                        .map_err(|e| ConversionError::UnsupportedConstruct(e.to_string()))?
525                        .node_id;
526                    self.copy_node_to(dest, new_child_id, child_id)?;
527                }
528            }
529            _ => {}
530        }
531
532        Ok(())
533    }
534}
535
536/// Parse an integer range string (Rust-style or interval notation)
537fn parse_integer_range(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
538    let s = s.trim();
539
540    // Try interval notation first: [a, b], (a, b), [a, b), (a, b]
541    if s.starts_with('[') || s.starts_with('(') {
542        return parse_interval_integer(s);
543    }
544
545    // Rust-style: a..b, a..=b, a.., ..b, ..=b
546    if let Some(eq_pos) = s.find("..=") {
547        let left = &s[..eq_pos];
548        let right = &s[eq_pos + 3..];
549        let min = if left.is_empty() {
550            Bound::Unbounded
551        } else {
552            Bound::Inclusive(parse_bigint(left)?)
553        };
554        let max = if right.is_empty() {
555            Bound::Unbounded
556        } else {
557            Bound::Inclusive(parse_bigint(right)?)
558        };
559        Ok((min, max))
560    } else if let Some(dot_pos) = s.find("..") {
561        let left = &s[..dot_pos];
562        let right = &s[dot_pos + 2..];
563        let min = if left.is_empty() {
564            Bound::Unbounded
565        } else {
566            Bound::Inclusive(parse_bigint(left)?)
567        };
568        let max = if right.is_empty() {
569            Bound::Unbounded
570        } else {
571            Bound::Exclusive(parse_bigint(right)?)
572        };
573        Ok((min, max))
574    } else {
575        Err(ConversionError::InvalidRangeString(s.to_string()))
576    }
577}
578
579/// Parse interval notation for integers: [a, b], (a, b), etc.
580fn parse_interval_integer(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
581    let left_inclusive = s.starts_with('[');
582    let right_inclusive = s.ends_with(']');
583
584    let inner = &s[1..s.len() - 1];
585    let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
586    if parts.len() != 2 {
587        return Err(ConversionError::InvalidRangeString(s.to_string()));
588    }
589
590    let min = if parts[0].is_empty() {
591        Bound::Unbounded
592    } else if left_inclusive {
593        Bound::Inclusive(parse_bigint(parts[0])?)
594    } else {
595        Bound::Exclusive(parse_bigint(parts[0])?)
596    };
597
598    let max = if parts[1].is_empty() {
599        Bound::Unbounded
600    } else if right_inclusive {
601        Bound::Inclusive(parse_bigint(parts[1])?)
602    } else {
603        Bound::Exclusive(parse_bigint(parts[1])?)
604    };
605
606    Ok((min, max))
607}
608
609/// Parse a float range string
610fn parse_float_range(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
611    let s = s.trim();
612
613    // Try interval notation first
614    if s.starts_with('[') || s.starts_with('(') {
615        return parse_interval_float(s);
616    }
617
618    // Rust-style
619    if let Some(eq_pos) = s.find("..=") {
620        let left = &s[..eq_pos];
621        let right = &s[eq_pos + 3..];
622        let min = if left.is_empty() {
623            Bound::Unbounded
624        } else {
625            Bound::Inclusive(parse_f64(left)?)
626        };
627        let max = if right.is_empty() {
628            Bound::Unbounded
629        } else {
630            Bound::Inclusive(parse_f64(right)?)
631        };
632        Ok((min, max))
633    } else if let Some(dot_pos) = s.find("..") {
634        let left = &s[..dot_pos];
635        let right = &s[dot_pos + 2..];
636        let min = if left.is_empty() {
637            Bound::Unbounded
638        } else {
639            Bound::Inclusive(parse_f64(left)?)
640        };
641        let max = if right.is_empty() {
642            Bound::Unbounded
643        } else {
644            Bound::Exclusive(parse_f64(right)?)
645        };
646        Ok((min, max))
647    } else {
648        Err(ConversionError::InvalidRangeString(s.to_string()))
649    }
650}
651
652/// Parse interval notation for floats
653fn parse_interval_float(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
654    let left_inclusive = s.starts_with('[');
655    let right_inclusive = s.ends_with(']');
656
657    let inner = &s[1..s.len() - 1];
658    let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
659    if parts.len() != 2 {
660        return Err(ConversionError::InvalidRangeString(s.to_string()));
661    }
662
663    let min = if parts[0].is_empty() {
664        Bound::Unbounded
665    } else if left_inclusive {
666        Bound::Inclusive(parse_f64(parts[0])?)
667    } else {
668        Bound::Exclusive(parse_f64(parts[0])?)
669    };
670
671    let max = if parts[1].is_empty() {
672        Bound::Unbounded
673    } else if right_inclusive {
674        Bound::Inclusive(parse_f64(parts[1])?)
675    } else {
676        Bound::Exclusive(parse_f64(parts[1])?)
677    };
678
679    Ok((min, max))
680}
681
682fn parse_bigint(s: &str) -> Result<BigInt, ConversionError> {
683    s.parse()
684        .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid integer: {}", s)))
685}
686
687fn parse_f64(s: &str) -> Result<f64, ConversionError> {
688    s.parse()
689        .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid float: {}", s)))
690}
691
692/// Convert an EureDocument containing schema definitions to a SchemaDocument
693///
694/// This function traverses the document and extracts schema information from:
695/// - Type paths (`.text`, `.integer`, `.text.rust`, etc.)
696/// - `$variant` extension for explicit type variants
697/// - `variants.*` fields for union variant definitions
698/// - Constraint fields (`min-length`, `max-length`, `pattern`, `range`, etc.)
699/// - Metadata extensions (`$description`, `$deprecated`, `$default`, `$examples`)
700///
701/// # Arguments
702///
703/// * `doc` - The EureDocument containing schema definitions
704///
705/// # Returns
706///
707/// A tuple of (SchemaDocument, SchemaSourceMap) on success, or a ConversionError on failure.
708/// The SchemaSourceMap maps each schema node ID to its source document node ID, which can be
709/// used for propagating origin information for error formatting.
710///
711/// # Examples
712///
713/// ```ignore
714/// use eure::parse_to_document;
715/// use eure_schema::convert::document_to_schema;
716///
717/// let input = r#"
718/// name = `text`
719/// age = `integer`
720/// "#;
721///
722/// let doc = parse_to_document(input).unwrap();
723/// let (schema, source_map) = document_to_schema(&doc).unwrap();
724/// ```
725pub fn document_to_schema(
726    doc: &EureDocument,
727) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
728    Converter::new(doc).convert()
729}
730
731#[cfg(test)]
732mod tests {
733    use super::*;
734    use crate::identifiers::{EXT_TYPE, OPTIONAL};
735    use eure_document::document::node::NodeMap;
736    use eure_document::eure;
737    use eure_document::text::Text;
738    use eure_document::value::PrimitiveValue;
739
740    /// Create a document with a record containing a single field with $ext-type extension
741    fn create_schema_with_field_ext_type(ext_type_content: NodeValue) -> EureDocument {
742        let mut doc = EureDocument::new();
743        let root_id = doc.get_root_id();
744
745        // Create field value: `text`
746        let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
747            Text::inline_implicit("text"),
748        )));
749
750        // Add $ext-type extension to the field
751        let ext_type_id = doc.create_node(ext_type_content);
752        doc.node_mut(field_value_id)
753            .extensions
754            .insert(EXT_TYPE.clone(), ext_type_id);
755
756        // Create root as record with field: { name = `text` }
757        let mut root_map = NodeMap::default();
758        root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
759        doc.node_mut(root_id).content = NodeValue::Map(root_map);
760
761        doc
762    }
763
764    #[test]
765    fn extract_ext_types_not_map() {
766        // name.$ext-type = 1 should error, not silently ignore
767        // The new parser catches this during parse_record() which expects a map
768        let doc = create_schema_with_field_ext_type(NodeValue::Primitive(PrimitiveValue::Integer(
769            1.into(),
770        )));
771
772        let err = document_to_schema(&doc).unwrap_err();
773        use eure_document::parse::ParseErrorKind;
774        use eure_document::value::ValueKind;
775        assert_eq!(
776            err,
777            ConversionError::ParseError(ParseError {
778                node_id: NodeId(2),
779                kind: ParseErrorKind::TypeMismatch {
780                    expected: ValueKind::Map,
781                    actual: ValueKind::Integer,
782                }
783            })
784        );
785    }
786
787    #[test]
788    fn extract_ext_types_invalid_key() {
789        // name.$ext-type = { 0 => `text` } should error, not silently ignore
790        // The parser catches this during parse_ext_types() -> unknown_fields()
791        let mut doc = EureDocument::new();
792        let root_id = doc.get_root_id();
793
794        // Create field value: `text`
795        let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
796            Text::inline_implicit("text"),
797        )));
798
799        // Create $ext-type as map with integer key
800        // The value's node_id is returned in the error since that's the entry with invalid key
801        let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
802            Text::inline_implicit("text"),
803        )));
804        let mut ext_type_map = NodeMap::default();
805        ext_type_map.insert(ObjectKey::Number(0.into()), ext_type_value_id);
806
807        let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
808        doc.node_mut(field_value_id)
809            .extensions
810            .insert(EXT_TYPE.clone(), ext_type_id);
811
812        // Create root as record
813        let mut root_map = NodeMap::default();
814        root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
815        doc.node_mut(root_id).content = NodeValue::Map(root_map);
816
817        let err = document_to_schema(&doc).unwrap_err();
818        use eure_document::parse::ParseErrorKind;
819        assert_eq!(
820            err,
821            ConversionError::ParseError(ParseError {
822                // The error points to the value's node_id (the entry with invalid key)
823                node_id: ext_type_value_id,
824                kind: ParseErrorKind::InvalidKeyType(ObjectKey::Number(0.into()))
825            })
826        );
827    }
828
829    #[test]
830    fn extract_ext_types_invalid_optional() {
831        // name.$ext-type.desc.$optional = 1 should error, not silently default to false
832        // The new parser catches this during field_optional::<bool>() parsing
833        let mut doc = EureDocument::new();
834        let root_id = doc.get_root_id();
835
836        // Create field value: `text`
837        let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
838            Text::inline_implicit("text"),
839        )));
840
841        // Create ext-type value with invalid $optional = 1
842        let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
843            Text::inline_implicit("text"),
844        )));
845        let optional_node_id =
846            doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
847        doc.node_mut(ext_type_value_id)
848            .extensions
849            .insert(OPTIONAL.clone(), optional_node_id);
850
851        // Create $ext-type map
852        let mut ext_type_map = NodeMap::default();
853        ext_type_map.insert(ObjectKey::String("desc".to_string()), ext_type_value_id);
854
855        let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
856        doc.node_mut(field_value_id)
857            .extensions
858            .insert(EXT_TYPE.clone(), ext_type_id);
859
860        // Create root as record
861        let mut root_map = NodeMap::default();
862        root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
863        doc.node_mut(root_id).content = NodeValue::Map(root_map);
864
865        let err = document_to_schema(&doc).unwrap_err();
866        use eure_document::parse::ParseErrorKind;
867        use eure_document::value::ValueKind;
868        assert_eq!(
869            err,
870            ConversionError::ParseError(ParseError {
871                node_id: NodeId(3),
872                kind: ParseErrorKind::TypeMismatch {
873                    expected: ValueKind::Bool,
874                    actual: ValueKind::Integer,
875                }
876            })
877        );
878    }
879
880    #[test]
881    fn literal_variant_with_inline_code() {
882        // Test: { = `any`, $variant => "literal" } should create Literal(Text("any"))
883        // NOT Any (which would happen if $variant is not detected)
884        // Note: { = value, $ext => ... } is represented in document model as just the value with extensions
885        let mut doc = EureDocument::new();
886        let root_id = doc.get_root_id();
887
888        // Create the $variant extension value: "literal"
889        let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
890            Text::plaintext("literal"),
891        )));
892
893        // Set root content to the inline code value directly: `any`
894        // (not wrapped in a map, since { = value } unwraps to just value)
895        doc.node_mut(root_id).content =
896            NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("any")));
897
898        // Add $variant extension
899        doc.node_mut(root_id)
900            .extensions
901            .insert("variant".parse().unwrap(), variant_value_id);
902
903        let (schema, _source_map) =
904            document_to_schema(&doc).expect("Schema conversion should succeed");
905
906        // The root should be a Literal, not Any
907        let root_content = &schema.node(schema.root).content;
908        match root_content {
909            SchemaNodeContent::Literal(doc) => {
910                // The value should be Text("any")
911                match &doc.root().content {
912                    NodeValue::Primitive(PrimitiveValue::Text(t)) => {
913                        assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
914                    }
915                    _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
916                }
917            }
918            SchemaNodeContent::Any => {
919                panic!("BUG: Got Any instead of Literal - $variant extension not detected!");
920            }
921            other => panic!("Expected Literal, got {:?}", other),
922        }
923    }
924
925    #[test]
926    fn literal_variant_parsed_from_eure() {
927        let doc = eure!({
928            = @code("any")
929            %variant = "literal"
930        });
931
932        let (schema, _source_map) =
933            document_to_schema(&doc).expect("Schema conversion should succeed");
934
935        let root_content = &schema.node(schema.root).content;
936        match root_content {
937            SchemaNodeContent::Literal(doc) => match &doc.root().content {
938                NodeValue::Primitive(PrimitiveValue::Text(t)) => {
939                    assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
940                }
941                _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
942            },
943            SchemaNodeContent::Any => {
944                panic!(
945                    "BUG: Got Any instead of Literal - $variant extension not respected for primitive"
946                );
947            }
948            other => panic!("Expected Literal, got {:?}", other),
949        }
950    }
951
952    #[test]
953    fn union_with_literal_any_variant() {
954        // Test a union like $types.type which has variants including:
955        // @variants.any = { = `any`, $variant => "literal" }
956        // @variants.literal = `any`
957        // The 'any' variant should match only literal "any", not any value.
958        let mut doc = EureDocument::new();
959        let root_id = doc.get_root_id();
960
961        // Create the 'any' variant value: { = `any`, $variant => "literal" }
962        // Note: { = value, $ext => ... } unwraps to just the value with extensions
963        let any_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
964            Text::inline_implicit("any"),
965        )));
966        // Add $variant => "literal" extension
967        let literal_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
968            Text::plaintext("literal"),
969        )));
970        doc.node_mut(any_variant_node)
971            .extensions
972            .insert("variant".parse().unwrap(), literal_ext);
973
974        // Create the 'literal' variant value: `any` (type Any)
975        let literal_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
976            Text::inline_implicit("any"),
977        )));
978
979        // Create the variants map
980        let mut variants_map = NodeMap::default();
981        variants_map.insert(ObjectKey::String("any".to_string()), any_variant_node);
982        variants_map.insert(
983            ObjectKey::String("literal".to_string()),
984            literal_variant_node,
985        );
986        let variants_node = doc.create_node(NodeValue::Map(variants_map));
987
988        // Create root as union
989        let union_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
990            Text::plaintext("union"),
991        )));
992        let untagged_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
993            Text::plaintext("untagged"),
994        )));
995
996        // Create root map with variants
997        let mut root_map = NodeMap::default();
998        root_map.insert(ObjectKey::String("variants".to_string()), variants_node);
999
1000        doc.node_mut(root_id).content = NodeValue::Map(root_map);
1001        doc.node_mut(root_id)
1002            .extensions
1003            .insert("variant".parse().unwrap(), union_ext);
1004        doc.node_mut(root_id)
1005            .extensions
1006            .insert("variant-repr".parse().unwrap(), untagged_ext);
1007
1008        let (schema, _source_map) =
1009            document_to_schema(&doc).expect("Schema conversion should succeed");
1010
1011        // Check the union schema
1012        let root_content = &schema.node(schema.root).content;
1013        match root_content {
1014            SchemaNodeContent::Union(union_schema) => {
1015                // Check 'any' variant is Literal("any"), not Any
1016                let any_variant_id = union_schema
1017                    .variants
1018                    .get("any")
1019                    .expect("'any' variant missing");
1020                let any_content = &schema.node(*any_variant_id).content;
1021                match any_content {
1022                    SchemaNodeContent::Literal(doc) => match &doc.root().content {
1023                        NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1024                            assert_eq!(
1025                                t.as_str(),
1026                                "any",
1027                                "'any' variant should be Literal(\"any\")"
1028                            );
1029                        }
1030                        _ => panic!("'any' variant: expected Text, got {:?}", doc),
1031                    },
1032                    SchemaNodeContent::Any => {
1033                        panic!(
1034                            "BUG: 'any' variant is Any instead of Literal(\"any\") - $variant extension not detected!"
1035                        );
1036                    }
1037                    other => panic!("'any' variant: expected Literal, got {:?}", other),
1038                }
1039
1040                // Check 'literal' variant is Any
1041                let literal_variant_id = union_schema
1042                    .variants
1043                    .get("literal")
1044                    .expect("'literal' variant missing");
1045                let literal_content = &schema.node(*literal_variant_id).content;
1046                match literal_content {
1047                    SchemaNodeContent::Any => {
1048                        // Correct: 'literal' variant should be Any
1049                    }
1050                    other => panic!("'literal' variant: expected Any, got {:?}", other),
1051                }
1052            }
1053            other => panic!("Expected Union, got {:?}", other),
1054        }
1055    }
1056}