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.0.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        let unknown_fields = self.convert_unknown_fields_policy(parsed.unknown_fields)?;
349
350        Ok(RecordSchema {
351            properties,
352            unknown_fields,
353        })
354    }
355
356    /// Convert parsed union schema to final union schema
357    fn convert_union_schema(
358        &mut self,
359        parsed: ParsedUnionSchema,
360    ) -> Result<UnionSchema, ConversionError> {
361        let mut variants = IndexMap::new();
362
363        for (variant_name, variant_node_id) in parsed.variants {
364            let schema = self.convert_node(variant_node_id)?;
365            variants.insert(variant_name, schema);
366        }
367
368        Ok(UnionSchema {
369            variants,
370            unambiguous: parsed.unambiguous,
371            repr: parsed.repr,
372            deny_untagged: parsed.deny_untagged,
373        })
374    }
375
376    /// Convert parsed unknown fields policy to final policy
377    fn convert_unknown_fields_policy(
378        &mut self,
379        parsed: ParsedUnknownFieldsPolicy,
380    ) -> Result<UnknownFieldsPolicy, ConversionError> {
381        match parsed {
382            ParsedUnknownFieldsPolicy::Deny => Ok(UnknownFieldsPolicy::Deny),
383            ParsedUnknownFieldsPolicy::Allow => Ok(UnknownFieldsPolicy::Allow),
384            ParsedUnknownFieldsPolicy::Schema(node_id) => {
385                let schema = self.convert_node(node_id)?;
386                Ok(UnknownFieldsPolicy::Schema(schema))
387            }
388        }
389    }
390
391    /// Convert parsed metadata to final metadata
392    fn convert_metadata(
393        &mut self,
394        parsed: ParsedSchemaMetadata,
395    ) -> Result<SchemaMetadata, ConversionError> {
396        let default = parsed
397            .default
398            .map(|id| self.node_to_document(id))
399            .transpose()?;
400
401        let examples = parsed
402            .examples
403            .map(|ids| {
404                ids.into_iter()
405                    .map(|id| self.node_to_document(id))
406                    .collect::<Result<Vec<_>, _>>()
407            })
408            .transpose()?;
409
410        Ok(SchemaMetadata {
411            description: parsed.description,
412            deprecated: parsed.deprecated,
413            default,
414            examples,
415        })
416    }
417
418    /// Convert parsed ext types to final ext types
419    fn convert_ext_types(
420        &mut self,
421        parsed: IndexMap<Identifier, ParsedExtTypeSchema>,
422    ) -> Result<IndexMap<Identifier, ExtTypeSchema>, ConversionError> {
423        let mut result = IndexMap::new();
424
425        for (name, parsed_schema) in parsed {
426            let schema = self.convert_node(parsed_schema.schema)?;
427            result.insert(
428                name,
429                ExtTypeSchema {
430                    schema,
431                    optional: parsed_schema.optional,
432                },
433            );
434        }
435
436        Ok(result)
437    }
438
439    /// Extract a subtree as a new EureDocument for literal types
440    fn node_to_document(&self, node_id: NodeId) -> Result<EureDocument, ConversionError> {
441        let mut new_doc = EureDocument::new();
442        let root_id = new_doc.get_root_id();
443        self.copy_node_to(&mut new_doc, root_id, node_id)?;
444        Ok(new_doc)
445    }
446
447    /// Recursively copy a node from source document to destination
448    fn copy_node_to(
449        &self,
450        dest: &mut EureDocument,
451        dest_node_id: NodeId,
452        src_node_id: NodeId,
453    ) -> Result<(), ConversionError> {
454        let src_node = self.doc.node(src_node_id);
455
456        // Collect child info before mutating dest
457        let children_to_copy: Vec<_> = match &src_node.content {
458            NodeValue::Primitive(prim) => {
459                dest.set_content(dest_node_id, NodeValue::Primitive(prim.clone()));
460                vec![]
461            }
462            NodeValue::Array(arr) => {
463                dest.set_content(dest_node_id, NodeValue::empty_array());
464                arr.0.to_vec()
465            }
466            NodeValue::Tuple(tup) => {
467                dest.set_content(dest_node_id, NodeValue::empty_tuple());
468                tup.0.to_vec()
469            }
470            NodeValue::Map(map) => {
471                dest.set_content(dest_node_id, NodeValue::empty_map());
472                map.0
473                    .iter()
474                    .map(|(k, &v)| (k.clone(), v))
475                    .collect::<Vec<_>>()
476                    .into_iter()
477                    .map(|(_, v)| v)
478                    .collect()
479            }
480            NodeValue::Hole(_) => {
481                return Err(ConversionError::UnsupportedConstruct(
482                    "Hole node".to_string(),
483                ));
484            }
485        };
486
487        // Skip ALL extensions during literal value copying.
488        // Extensions are schema metadata (like $variant, $deny-untagged, $optional, etc.)
489        // and should not be part of the literal value comparison.
490        // Literal types compare only the data structure, not metadata.
491
492        // Now copy children based on the type
493        let src_node = self.doc.node(src_node_id);
494        match &src_node.content {
495            NodeValue::Array(_) => {
496                for child_id in children_to_copy {
497                    let new_child_id = dest
498                        .add_array_element(None, dest_node_id)
499                        .map_err(|e| ConversionError::UnsupportedConstruct(e.to_string()))?
500                        .node_id;
501                    self.copy_node_to(dest, new_child_id, child_id)?;
502                }
503            }
504            NodeValue::Tuple(_) => {
505                for (index, child_id) in children_to_copy.into_iter().enumerate() {
506                    let new_child_id = dest
507                        .add_tuple_element(index as u8, dest_node_id)
508                        .map_err(|e| ConversionError::UnsupportedConstruct(e.to_string()))?
509                        .node_id;
510                    self.copy_node_to(dest, new_child_id, child_id)?;
511                }
512            }
513            NodeValue::Map(map) => {
514                for (key, &child_id) in map.0.iter() {
515                    let new_child_id = dest
516                        .add_map_child(key.clone(), dest_node_id)
517                        .map_err(|e| ConversionError::UnsupportedConstruct(e.to_string()))?
518                        .node_id;
519                    self.copy_node_to(dest, new_child_id, child_id)?;
520                }
521            }
522            _ => {}
523        }
524
525        Ok(())
526    }
527}
528
529/// Parse an integer range string (Rust-style or interval notation)
530fn parse_integer_range(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
531    let s = s.trim();
532
533    // Try interval notation first: [a, b], (a, b), [a, b), (a, b]
534    if s.starts_with('[') || s.starts_with('(') {
535        return parse_interval_integer(s);
536    }
537
538    // Rust-style: a..b, a..=b, a.., ..b, ..=b
539    if let Some(eq_pos) = s.find("..=") {
540        let left = &s[..eq_pos];
541        let right = &s[eq_pos + 3..];
542        let min = if left.is_empty() {
543            Bound::Unbounded
544        } else {
545            Bound::Inclusive(parse_bigint(left)?)
546        };
547        let max = if right.is_empty() {
548            Bound::Unbounded
549        } else {
550            Bound::Inclusive(parse_bigint(right)?)
551        };
552        Ok((min, max))
553    } else if let Some(dot_pos) = s.find("..") {
554        let left = &s[..dot_pos];
555        let right = &s[dot_pos + 2..];
556        let min = if left.is_empty() {
557            Bound::Unbounded
558        } else {
559            Bound::Inclusive(parse_bigint(left)?)
560        };
561        let max = if right.is_empty() {
562            Bound::Unbounded
563        } else {
564            Bound::Exclusive(parse_bigint(right)?)
565        };
566        Ok((min, max))
567    } else {
568        Err(ConversionError::InvalidRangeString(s.to_string()))
569    }
570}
571
572/// Parse interval notation for integers: [a, b], (a, b), etc.
573fn parse_interval_integer(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
574    let left_inclusive = s.starts_with('[');
575    let right_inclusive = s.ends_with(']');
576
577    let inner = &s[1..s.len() - 1];
578    let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
579    if parts.len() != 2 {
580        return Err(ConversionError::InvalidRangeString(s.to_string()));
581    }
582
583    let min = if parts[0].is_empty() {
584        Bound::Unbounded
585    } else if left_inclusive {
586        Bound::Inclusive(parse_bigint(parts[0])?)
587    } else {
588        Bound::Exclusive(parse_bigint(parts[0])?)
589    };
590
591    let max = if parts[1].is_empty() {
592        Bound::Unbounded
593    } else if right_inclusive {
594        Bound::Inclusive(parse_bigint(parts[1])?)
595    } else {
596        Bound::Exclusive(parse_bigint(parts[1])?)
597    };
598
599    Ok((min, max))
600}
601
602/// Parse a float range string
603fn parse_float_range(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
604    let s = s.trim();
605
606    // Try interval notation first
607    if s.starts_with('[') || s.starts_with('(') {
608        return parse_interval_float(s);
609    }
610
611    // Rust-style
612    if let Some(eq_pos) = s.find("..=") {
613        let left = &s[..eq_pos];
614        let right = &s[eq_pos + 3..];
615        let min = if left.is_empty() {
616            Bound::Unbounded
617        } else {
618            Bound::Inclusive(parse_f64(left)?)
619        };
620        let max = if right.is_empty() {
621            Bound::Unbounded
622        } else {
623            Bound::Inclusive(parse_f64(right)?)
624        };
625        Ok((min, max))
626    } else if let Some(dot_pos) = s.find("..") {
627        let left = &s[..dot_pos];
628        let right = &s[dot_pos + 2..];
629        let min = if left.is_empty() {
630            Bound::Unbounded
631        } else {
632            Bound::Inclusive(parse_f64(left)?)
633        };
634        let max = if right.is_empty() {
635            Bound::Unbounded
636        } else {
637            Bound::Exclusive(parse_f64(right)?)
638        };
639        Ok((min, max))
640    } else {
641        Err(ConversionError::InvalidRangeString(s.to_string()))
642    }
643}
644
645/// Parse interval notation for floats
646fn parse_interval_float(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
647    let left_inclusive = s.starts_with('[');
648    let right_inclusive = s.ends_with(']');
649
650    let inner = &s[1..s.len() - 1];
651    let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
652    if parts.len() != 2 {
653        return Err(ConversionError::InvalidRangeString(s.to_string()));
654    }
655
656    let min = if parts[0].is_empty() {
657        Bound::Unbounded
658    } else if left_inclusive {
659        Bound::Inclusive(parse_f64(parts[0])?)
660    } else {
661        Bound::Exclusive(parse_f64(parts[0])?)
662    };
663
664    let max = if parts[1].is_empty() {
665        Bound::Unbounded
666    } else if right_inclusive {
667        Bound::Inclusive(parse_f64(parts[1])?)
668    } else {
669        Bound::Exclusive(parse_f64(parts[1])?)
670    };
671
672    Ok((min, max))
673}
674
675fn parse_bigint(s: &str) -> Result<BigInt, ConversionError> {
676    s.parse()
677        .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid integer: {}", s)))
678}
679
680fn parse_f64(s: &str) -> Result<f64, ConversionError> {
681    s.parse()
682        .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid float: {}", s)))
683}
684
685/// Convert an EureDocument containing schema definitions to a SchemaDocument
686///
687/// This function traverses the document and extracts schema information from:
688/// - Type paths (`.text`, `.integer`, `.text.rust`, etc.)
689/// - `$variant` extension for explicit type variants
690/// - `variants.*` fields for union variant definitions
691/// - Constraint fields (`min-length`, `max-length`, `pattern`, `range`, etc.)
692/// - Metadata extensions (`$description`, `$deprecated`, `$default`, `$examples`)
693///
694/// # Arguments
695///
696/// * `doc` - The EureDocument containing schema definitions
697///
698/// # Returns
699///
700/// A tuple of (SchemaDocument, SchemaSourceMap) on success, or a ConversionError on failure.
701/// The SchemaSourceMap maps each schema node ID to its source document node ID, which can be
702/// used for propagating origin information for error formatting.
703///
704/// # Examples
705///
706/// ```ignore
707/// use eure::parse_to_document;
708/// use eure_schema::convert::document_to_schema;
709///
710/// let input = r#"
711/// name = `text`
712/// age = `integer`
713/// "#;
714///
715/// let doc = parse_to_document(input).unwrap();
716/// let (schema, source_map) = document_to_schema(&doc).unwrap();
717/// ```
718pub fn document_to_schema(
719    doc: &EureDocument,
720) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
721    Converter::new(doc).convert()
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727    use crate::identifiers::{EXT_TYPE, OPTIONAL};
728    use eure_document::document::node::NodeMap;
729    use eure_document::eure;
730    use eure_document::text::Text;
731    use eure_document::value::PrimitiveValue;
732
733    /// Create a document with a record containing a single field with $ext-type extension
734    fn create_schema_with_field_ext_type(ext_type_content: NodeValue) -> EureDocument {
735        let mut doc = EureDocument::new();
736        let root_id = doc.get_root_id();
737
738        // Create field value: `text`
739        let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
740            Text::inline_implicit("text"),
741        )));
742
743        // Add $ext-type extension to the field
744        let ext_type_id = doc.create_node(ext_type_content);
745        doc.node_mut(field_value_id)
746            .extensions
747            .insert(EXT_TYPE.clone(), ext_type_id);
748
749        // Create root as record with field: { name = `text` }
750        let mut root_map = NodeMap::default();
751        root_map
752            .0
753            .insert(ObjectKey::String("name".to_string()), field_value_id);
754        doc.node_mut(root_id).content = NodeValue::Map(root_map);
755
756        doc
757    }
758
759    #[test]
760    fn extract_ext_types_not_map() {
761        // name.$ext-type = 1 should error, not silently ignore
762        // The new parser catches this during parse_record() which expects a map
763        let doc = create_schema_with_field_ext_type(NodeValue::Primitive(PrimitiveValue::Integer(
764            1.into(),
765        )));
766
767        let err = document_to_schema(&doc).unwrap_err();
768        use eure_document::parse::ParseErrorKind;
769        use eure_document::value::ValueKind;
770        assert_eq!(
771            err,
772            ConversionError::ParseError(ParseError {
773                node_id: NodeId(2),
774                kind: ParseErrorKind::TypeMismatch {
775                    expected: ValueKind::Map,
776                    actual: ValueKind::Integer,
777                }
778            })
779        );
780    }
781
782    #[test]
783    fn extract_ext_types_invalid_key() {
784        // name.$ext-type = { 0 => `text` } should error, not silently ignore
785        // The new parser catches this during parse_ext_types() -> deny_unknown_fields()
786        let mut doc = EureDocument::new();
787        let root_id = doc.get_root_id();
788
789        // Create field value: `text`
790        let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
791            Text::inline_implicit("text"),
792        )));
793
794        // Create $ext-type as map with integer key
795        let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
796            Text::inline_implicit("text"),
797        )));
798        let mut ext_type_map = NodeMap::default();
799        ext_type_map
800            .0
801            .insert(ObjectKey::Number(0.into()), ext_type_value_id);
802
803        let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
804        doc.node_mut(field_value_id)
805            .extensions
806            .insert(EXT_TYPE.clone(), ext_type_id);
807
808        // Create root as record
809        let mut root_map = NodeMap::default();
810        root_map
811            .0
812            .insert(ObjectKey::String("name".to_string()), field_value_id);
813        doc.node_mut(root_id).content = NodeValue::Map(root_map);
814
815        let err = document_to_schema(&doc).unwrap_err();
816        use eure_document::parse::ParseErrorKind;
817        assert_eq!(
818            err,
819            ConversionError::ParseError(ParseError {
820                node_id: NodeId(3),
821                kind: ParseErrorKind::InvalidKeyType(ObjectKey::Number(0.into()))
822            })
823        );
824    }
825
826    #[test]
827    fn extract_ext_types_invalid_optional() {
828        // name.$ext-type.desc.$optional = 1 should error, not silently default to false
829        // The new parser catches this during field_optional::<bool>() parsing
830        let mut doc = EureDocument::new();
831        let root_id = doc.get_root_id();
832
833        // Create field value: `text`
834        let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
835            Text::inline_implicit("text"),
836        )));
837
838        // Create ext-type value with invalid $optional = 1
839        let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
840            Text::inline_implicit("text"),
841        )));
842        let optional_node_id =
843            doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
844        doc.node_mut(ext_type_value_id)
845            .extensions
846            .insert(OPTIONAL.clone(), optional_node_id);
847
848        // Create $ext-type map
849        let mut ext_type_map = NodeMap::default();
850        ext_type_map
851            .0
852            .insert(ObjectKey::String("desc".to_string()), ext_type_value_id);
853
854        let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
855        doc.node_mut(field_value_id)
856            .extensions
857            .insert(EXT_TYPE.clone(), ext_type_id);
858
859        // Create root as record
860        let mut root_map = NodeMap::default();
861        root_map
862            .0
863            .insert(ObjectKey::String("name".to_string()), field_value_id);
864        doc.node_mut(root_id).content = NodeValue::Map(root_map);
865
866        let err = document_to_schema(&doc).unwrap_err();
867        use eure_document::parse::ParseErrorKind;
868        use eure_document::value::ValueKind;
869        assert_eq!(
870            err,
871            ConversionError::ParseError(ParseError {
872                node_id: NodeId(3),
873                kind: ParseErrorKind::TypeMismatch {
874                    expected: ValueKind::Bool,
875                    actual: ValueKind::Integer,
876                }
877            })
878        );
879    }
880
881    #[test]
882    fn literal_variant_with_inline_code() {
883        // Test: { = `any`, $variant => "literal" } should create Literal(Text("any"))
884        // NOT Any (which would happen if $variant is not detected)
885        // Note: { = value, $ext => ... } is represented in document model as just the value with extensions
886        let mut doc = EureDocument::new();
887        let root_id = doc.get_root_id();
888
889        // Create the $variant extension value: "literal"
890        let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
891            Text::plaintext("literal"),
892        )));
893
894        // Set root content to the inline code value directly: `any`
895        // (not wrapped in a map, since { = value } unwraps to just value)
896        doc.node_mut(root_id).content =
897            NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("any")));
898
899        // Add $variant extension
900        doc.node_mut(root_id)
901            .extensions
902            .insert("variant".parse().unwrap(), variant_value_id);
903
904        let (schema, _source_map) =
905            document_to_schema(&doc).expect("Schema conversion should succeed");
906
907        // The root should be a Literal, not Any
908        let root_content = &schema.node(schema.root).content;
909        match root_content {
910            SchemaNodeContent::Literal(doc) => {
911                // The value should be Text("any")
912                match &doc.root().content {
913                    NodeValue::Primitive(PrimitiveValue::Text(t)) => {
914                        assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
915                    }
916                    _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
917                }
918            }
919            SchemaNodeContent::Any => {
920                panic!("BUG: Got Any instead of Literal - $variant extension not detected!");
921            }
922            other => panic!("Expected Literal, got {:?}", other),
923        }
924    }
925
926    #[test]
927    fn literal_variant_parsed_from_eure() {
928        let doc = eure!({
929            = @code("any")
930            %variant = "literal"
931        });
932
933        let (schema, _source_map) =
934            document_to_schema(&doc).expect("Schema conversion should succeed");
935
936        let root_content = &schema.node(schema.root).content;
937        match root_content {
938            SchemaNodeContent::Literal(doc) => match &doc.root().content {
939                NodeValue::Primitive(PrimitiveValue::Text(t)) => {
940                    assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
941                }
942                _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
943            },
944            SchemaNodeContent::Any => {
945                panic!(
946                    "BUG: Got Any instead of Literal - $variant extension not respected for primitive"
947                );
948            }
949            other => panic!("Expected Literal, got {:?}", other),
950        }
951    }
952
953    #[test]
954    fn union_with_literal_any_variant() {
955        // Test a union like $types.type which has variants including:
956        // @variants.any = { = `any`, $variant => "literal" }
957        // @variants.literal = `any`
958        // The 'any' variant should match only literal "any", not any value.
959        let mut doc = EureDocument::new();
960        let root_id = doc.get_root_id();
961
962        // Create the 'any' variant value: { = `any`, $variant => "literal" }
963        // Note: { = value, $ext => ... } unwraps to just the value with extensions
964        let any_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
965            Text::inline_implicit("any"),
966        )));
967        // Add $variant => "literal" extension
968        let literal_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
969            Text::plaintext("literal"),
970        )));
971        doc.node_mut(any_variant_node)
972            .extensions
973            .insert("variant".parse().unwrap(), literal_ext);
974
975        // Create the 'literal' variant value: `any` (type Any)
976        let literal_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
977            Text::inline_implicit("any"),
978        )));
979
980        // Create the variants map
981        let mut variants_map = NodeMap::default();
982        variants_map
983            .0
984            .insert(ObjectKey::String("any".to_string()), any_variant_node);
985        variants_map.0.insert(
986            ObjectKey::String("literal".to_string()),
987            literal_variant_node,
988        );
989        let variants_node = doc.create_node(NodeValue::Map(variants_map));
990
991        // Create root as union
992        let union_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
993            Text::plaintext("union"),
994        )));
995        let untagged_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
996            Text::plaintext("untagged"),
997        )));
998
999        // Create root map with variants
1000        let mut root_map = NodeMap::default();
1001        root_map
1002            .0
1003            .insert(ObjectKey::String("variants".to_string()), variants_node);
1004
1005        doc.node_mut(root_id).content = NodeValue::Map(root_map);
1006        doc.node_mut(root_id)
1007            .extensions
1008            .insert("variant".parse().unwrap(), union_ext);
1009        doc.node_mut(root_id)
1010            .extensions
1011            .insert("variant-repr".parse().unwrap(), untagged_ext);
1012
1013        let (schema, _source_map) =
1014            document_to_schema(&doc).expect("Schema conversion should succeed");
1015
1016        // Check the union schema
1017        let root_content = &schema.node(schema.root).content;
1018        match root_content {
1019            SchemaNodeContent::Union(union_schema) => {
1020                // Check 'any' variant is Literal("any"), not Any
1021                let any_variant_id = union_schema
1022                    .variants
1023                    .get("any")
1024                    .expect("'any' variant missing");
1025                let any_content = &schema.node(*any_variant_id).content;
1026                match any_content {
1027                    SchemaNodeContent::Literal(doc) => match &doc.root().content {
1028                        NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1029                            assert_eq!(
1030                                t.as_str(),
1031                                "any",
1032                                "'any' variant should be Literal(\"any\")"
1033                            );
1034                        }
1035                        _ => panic!("'any' variant: expected Text, got {:?}", doc),
1036                    },
1037                    SchemaNodeContent::Any => {
1038                        panic!(
1039                            "BUG: 'any' variant is Any instead of Literal(\"any\") - $variant extension not detected!"
1040                        );
1041                    }
1042                    other => panic!("'any' variant: expected Literal, got {:?}", other),
1043                }
1044
1045                // Check 'literal' variant is Any
1046                let literal_variant_id = union_schema
1047                    .variants
1048                    .get("literal")
1049                    .expect("'literal' variant missing");
1050                let literal_content = &schema.node(*literal_variant_id).content;
1051                match literal_content {
1052                    SchemaNodeContent::Any => {
1053                        // Correct: 'literal' variant should be Any
1054                    }
1055                    other => panic!("'literal' variant: expected Any, got {:?}", other),
1056                }
1057            }
1058            other => panic!("Expected Union, got {:?}", other),
1059        }
1060    }
1061}