1use crate::parse::{
46 ParsedArraySchema, ParsedExports, ParsedExtTypeSchema, ParsedFloatSchema, ParsedImports,
47 ParsedIntegerSchema, ParsedMapSchema, ParsedRecordSchema, ParsedSchemaMetadata,
48 ParsedSchemaNode, ParsedSchemaNodeContent, ParsedTupleSchema, ParsedUnionSchema,
49 ParsedUnknownFieldsPolicy, parse_root_exports, parse_root_imports,
50};
51use crate::resolver::{LoadedSchemaSet, ResolvedSchemaUri, ResolverError};
52use crate::type_path_trace::LayoutStrategies;
53use crate::{
54 ArraySchema, Bound, CodegenDefaults, ExtTypeSchema, FloatPrecision, FloatSchema, IntegerSchema,
55 MapSchema, RecordCodegen, RecordFieldSchema, RecordSchema, RootCodegen, SchemaDocument,
56 SchemaImport, SchemaMetadata, SchemaNodeContent, SchemaNodeId, TupleSchema, TypeCodegen,
57 TypeReference, UnionCodegen, UnionSchema, UnknownFieldsPolicy,
58};
59use eure_document::document::node::{Node, NodeValue};
60use eure_document::document::{EureDocument, InsertErrorKind, NodeId};
61use eure_document::identifier::Identifier;
62use eure_document::parse::ParseError;
63use eure_document::path::{ArrayIndexKind, EurePath, PathSegment};
64use eure_document::value::{ObjectKey, ValueKind};
65use indexmap::{IndexMap, IndexSet};
66use num_bigint::BigInt;
67use thiserror::Error;
68
69#[derive(Debug, Error, Clone, PartialEq)]
71pub enum ConversionError {
72 #[error("Invalid type name: {0}")]
73 InvalidTypeName(ObjectKey),
74
75 #[error("unsupported literal value at node {node_id:?}: {kind}")]
76 UnsupportedLiteralValue { node_id: NodeId, kind: ValueKind },
77
78 #[error("document insert error while copying literal value: {0}")]
79 DocumentInsert(#[from] InsertErrorKind),
80
81 #[error("Invalid extension value: {extension} at path {path}")]
82 InvalidExtensionValue { extension: String, path: String },
83
84 #[error("Invalid range string: {0}")]
85 InvalidRangeString(String),
86
87 #[error("Invalid precision: {0} (expected \"f32\" or \"f64\")")]
88 InvalidPrecision(String),
89
90 #[error("Undefined type reference: {0}")]
91 UndefinedTypeReference(String),
92
93 #[error("non-productive reference cycle detected: {0}")]
94 NonProductiveReferenceCycle(String),
95
96 #[error(
97 "invalid `$codegen` extension at node {node_id:?}: supported only for record/union, got {schema_kind}"
98 )]
99 InvalidTypeCodegenTarget {
100 node_id: NodeId,
101 schema_kind: String,
102 },
103
104 #[error("failed to resolve schema import `{alias}` -> \"{raw_path}\": {source}")]
105 ImportResolverFailed {
106 alias: String,
107 raw_path: String,
108 #[source]
109 source: ResolverError,
110 },
111
112 #[error("schema import cycle: {}", format_cycle(.cycle, .attempted))]
113 ImportCycle {
114 cycle: Vec<ResolvedSchemaUri>,
115 attempted: ResolvedSchemaUri,
116 },
117
118 #[error(
119 "type reference uses unknown import namespace `{namespace}` (in `$types.{namespace}.{name}`)"
120 )]
121 UnknownImportNamespace { namespace: String, name: String },
122
123 #[error("type `{name}` is not exported by `{namespace}` (declared in $import)")]
124 TypeNotExported { namespace: String, name: String },
125
126 #[error("$export lists `{name}`, but no such locally-declared type exists")]
127 ExportedNameNotDeclared { name: String },
128
129 #[error("schema import `{alias}` -> \"{raw_path}\" was not loaded for {base}")]
130 ImportNotLoaded {
131 base: ResolvedSchemaUri,
132 alias: String,
133 raw_path: String,
134 },
135
136 #[error("Parse error: {0}")]
137 ParseError(#[from] ParseError),
138}
139
140fn format_cycle(cycle: &[ResolvedSchemaUri], attempted: &ResolvedSchemaUri) -> String {
141 let mut parts: Vec<String> = cycle.iter().map(|u| u.to_string()).collect();
142 parts.push(attempted.to_string());
143 parts.join(" -> ")
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct SchemaSource {
149 pub uri: ResolvedSchemaUri,
150 pub node_id: NodeId,
151}
152
153pub type SchemaSourceMap = IndexMap<SchemaNodeId, SchemaSource>;
156
157struct Converter<'a> {
159 doc: &'a EureDocument,
160 loaded: &'a LoadedSchemaSet,
161 schema: SchemaDocument,
162 source_map: SchemaSourceMap,
164 base_uri: ResolvedSchemaUri,
167}
168
169impl<'a> Converter<'a> {
170 fn new(
171 doc: &'a EureDocument,
172 loaded: &'a LoadedSchemaSet,
173 base_uri: ResolvedSchemaUri,
174 ) -> Self {
175 Self {
176 doc,
177 loaded,
178 schema: SchemaDocument::new(),
179 source_map: IndexMap::new(),
180 base_uri,
181 }
182 }
183
184 fn run(
190 mut self,
191 visited: &mut IndexSet<ResolvedSchemaUri>,
192 ) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
193 let root_id = self.doc.get_root_id();
194
195 self.process_imports(visited)?;
198
199 let root_node = self.doc.node(root_id);
201 self.convert_types(root_node)?;
202 self.convert_root_codegen(root_node)?;
203
204 self.schema.root = self.convert_node_allow_non_type_codegen(root_id)?;
206
207 self.compute_exports()?;
209
210 self.validate_and_rewrite_type_references()?;
212 self.validate_non_productive_reference_cycles()?;
213
214 Ok((self.schema, self.source_map))
215 }
216
217 fn process_imports(
219 &mut self,
220 visited: &mut IndexSet<ResolvedSchemaUri>,
221 ) -> Result<(), ConversionError> {
222 let root_id = self.doc.get_root_id();
223 let root_ctx = self.doc.parse_context(root_id);
224 let parsed: ParsedImports = parse_root_imports(&root_ctx)?;
225
226 for (alias, entry) in parsed.entries {
227 let imported_uri = self
228 .loaded
229 .import_target(&self.base_uri, &alias)
230 .cloned()
231 .ok_or_else(|| ConversionError::ImportNotLoaded {
232 base: self.base_uri.clone(),
233 alias: alias.to_string(),
234 raw_path: entry.raw_path.clone(),
235 })?;
236 let child_doc = self.loaded.document(&imported_uri).ok_or_else(|| {
237 ConversionError::ImportNotLoaded {
238 base: self.base_uri.clone(),
239 alias: alias.to_string(),
240 raw_path: entry.raw_path.clone(),
241 }
242 })?;
243 let (child_schema, child_source_map) =
244 convert_with_visited(self.loaded, imported_uri.clone(), child_doc, visited)?;
245
246 self.inline_imported_schema(&alias, imported_uri, child_schema, child_source_map)?;
247 }
248 Ok(())
249 }
250
251 fn inline_imported_schema(
254 &mut self,
255 alias: &Identifier,
256 child_uri: ResolvedSchemaUri,
257 child: SchemaDocument,
258 child_source_map: SchemaSourceMap,
259 ) -> Result<(), ConversionError> {
260 let mut remap: Vec<SchemaNodeId> = Vec::with_capacity(child.nodes.len());
262 for _ in 0..child.nodes.len() {
263 let new_id = self.schema.create_node(SchemaNodeContent::Any);
264 remap.push(new_id);
265 }
266
267 for (i, child_node) in child.nodes.iter().enumerate() {
269 let parent_id = remap[i];
270 let new_content = remap_node_content(&child_node.content, &remap);
271 let new_ext_types = remap_ext_types(&child_node.ext_types, &remap);
272
273 let parent_node = self.schema.node_mut(parent_id);
274 parent_node.content = new_content;
275 parent_node.metadata = child_node.metadata.clone();
276 parent_node.ext_types = new_ext_types;
277 parent_node.type_codegen = child_node.type_codegen.clone();
278 }
279
280 let mut all_types = IndexMap::new();
282 for (name, child_id) in &child.types {
283 all_types.insert(name.clone(), remap[child_id.0]);
284 }
285
286 for (child_id, source) in child_source_map {
287 self.source_map.insert(remap[child_id.0], source);
288 }
289
290 self.schema.imports.insert(
291 alias.clone(),
292 SchemaImport {
293 uri: child_uri,
294 all_types,
295 exports: child.exports.clone(),
296 },
297 );
298 Ok(())
299 }
300
301 fn compute_exports(&mut self) -> Result<(), ConversionError> {
304 let root_ctx = self.doc.parse_context(self.doc.get_root_id());
305 let parsed = parse_root_exports(&root_ctx)?;
306
307 match parsed {
308 None => {
309 self.schema.exports = self.schema.types.keys().cloned().collect();
311 }
312 Some(ParsedExports::Explicit { names }) => {
313 let mut exports = IndexSet::new();
314 for name in names {
315 if !self.schema.types.contains_key(&name) {
316 return Err(ConversionError::ExportedNameNotDeclared {
317 name: name.to_string(),
318 });
319 }
320 exports.insert(name);
321 }
322 self.schema.exports = exports;
323 }
324 }
325 Ok(())
326 }
327
328 fn validate_and_rewrite_type_references(&mut self) -> Result<(), ConversionError> {
332 for index in 0..self.schema.nodes.len() {
333 let needs_rewrite = matches!(
335 &self.schema.nodes[index].content,
336 SchemaNodeContent::Reference(_)
337 );
338 if !needs_rewrite {
339 continue;
340 }
341
342 let placeholder = SchemaNodeContent::Any;
345 let original = std::mem::replace(&mut self.schema.nodes[index].content, placeholder);
346 let SchemaNodeContent::Reference(type_ref) = original else {
347 unreachable!("checked above");
348 };
349 let resolved = self.resolve_type_reference(type_ref)?;
350 self.schema.nodes[index].content = SchemaNodeContent::Reference(resolved);
351 }
352 Ok(())
353 }
354
355 fn resolve_type_reference(
356 &self,
357 type_ref: TypeReference,
358 ) -> Result<TypeReference, ConversionError> {
359 match type_ref {
360 TypeReference::Resolved(_) => Ok(type_ref),
361 TypeReference::Named {
362 namespace: None,
363 name,
364 } => {
365 let target = self
366 .schema
367 .types
368 .get(&name)
369 .copied()
370 .ok_or_else(|| ConversionError::UndefinedTypeReference(name.to_string()))?;
371 Ok(TypeReference::Resolved(target))
372 }
373 TypeReference::Named {
374 namespace: Some(ns),
375 name,
376 } => {
377 let import = self.schema.imports.get(&ns).ok_or_else(|| {
378 ConversionError::UnknownImportNamespace {
379 namespace: ns.to_string(),
380 name: name.to_string(),
381 }
382 })?;
383 if !import.exports.contains(&name) {
384 return Err(ConversionError::TypeNotExported {
385 namespace: ns.to_string(),
386 name: name.to_string(),
387 });
388 }
389 let target = import.all_types.get(&name).copied().ok_or_else(|| {
390 ConversionError::UndefinedTypeReference(format!("{}.{}", ns, name))
391 })?;
392 Ok(TypeReference::Resolved(target))
393 }
394 }
395 }
396
397 fn convert_root_codegen(&mut self, node: &Node) -> Result<(), ConversionError> {
398 let codegen_ident: Identifier = "codegen".parse().unwrap();
399 let codegen_defaults_ident: Identifier = "codegen-defaults".parse().unwrap();
400
401 if let Some(node_id) = node.extensions.get(&codegen_ident) {
402 let rec = self.doc.parse_record(*node_id)?;
403 self.schema.root_codegen = RootCodegen {
404 type_name: rec.parse_field_optional::<String>("type")?,
405 };
406 }
407
408 if let Some(node_id) = node.extensions.get(&codegen_defaults_ident) {
409 self.schema.codegen_defaults = self.doc.parse::<CodegenDefaults>(*node_id)?;
410 }
411
412 Ok(())
413 }
414
415 fn convert_types(&mut self, node: &Node) -> Result<(), ConversionError> {
417 let types_ident: Identifier = "types".parse().unwrap();
418 if let Some(types_node_id) = node.extensions.get(&types_ident) {
419 let types_node = self.doc.node(*types_node_id);
420 if let NodeValue::Map(map) = &types_node.content {
421 for (key, &node_id) in map.iter() {
422 if let ObjectKey::String(name) = key {
423 let type_name: Identifier = name
424 .parse()
425 .map_err(|_| ConversionError::InvalidTypeName(key.clone()))?;
426 let schema_id = self.convert_node(node_id)?;
427 self.schema.types.insert(type_name, schema_id);
428 } else {
429 return Err(ConversionError::InvalidTypeName(key.clone()));
430 }
431 }
432 } else {
433 return Err(ConversionError::InvalidExtensionValue {
434 extension: "types".to_string(),
435 path: "$types must be a map".to_string(),
436 });
437 }
438 }
439 Ok(())
440 }
441
442 fn validate_non_productive_reference_cycles(&self) -> Result<(), ConversionError> {
443 #[derive(Clone, Copy, PartialEq, Eq)]
444 enum Mark {
445 Visiting,
446 Done,
447 }
448
449 fn next_ref_target(schema: &SchemaDocument, node_id: SchemaNodeId) -> Option<SchemaNodeId> {
450 let node = schema.node(node_id);
451 let SchemaNodeContent::Reference(type_ref) = &node.content else {
452 return None;
453 };
454 schema.resolve_reference(type_ref)
455 }
456
457 fn display_node(schema: &SchemaDocument, id: SchemaNodeId) -> String {
458 let reference = TypeReference::Resolved(id);
459 if let Some(name) = schema.reference_name(&reference) {
460 format!("$types.{}", name)
461 } else {
462 format!("node#{}", id.0)
463 }
464 }
465
466 fn visit(
467 schema: &SchemaDocument,
468 node_id: SchemaNodeId,
469 marks: &mut IndexMap<SchemaNodeId, Mark>,
470 stack: &mut Vec<SchemaNodeId>,
471 ) -> Result<(), ConversionError> {
472 if matches!(marks.get(&node_id), Some(Mark::Done)) {
473 return Ok(());
474 }
475 if matches!(marks.get(&node_id), Some(Mark::Visiting)) {
476 let start = stack.iter().position(|sid| *sid == node_id).unwrap_or(0);
477 let mut cycle: Vec<String> = stack[start..]
478 .iter()
479 .map(|sid| display_node(schema, *sid))
480 .collect();
481 cycle.push(display_node(schema, node_id));
482 return Err(ConversionError::NonProductiveReferenceCycle(
483 cycle.join(" -> "),
484 ));
485 }
486
487 marks.insert(node_id, Mark::Visiting);
488 stack.push(node_id);
489 if let Some(next_id) = next_ref_target(schema, node_id) {
490 visit(schema, next_id, marks, stack)?;
491 }
492 stack.pop();
493 marks.insert(node_id, Mark::Done);
494 Ok(())
495 }
496
497 let mut marks = IndexMap::new();
498 let mut stack = Vec::new();
499 for index in 0..self.schema.nodes.len() {
500 visit(&self.schema, SchemaNodeId(index), &mut marks, &mut stack)?;
501 }
502 Ok(())
503 }
504
505 fn convert_node(&mut self, node_id: NodeId) -> Result<SchemaNodeId, ConversionError> {
507 self.convert_node_inner(node_id, false)
508 }
509
510 fn convert_node_allow_non_type_codegen(
511 &mut self,
512 node_id: NodeId,
513 ) -> Result<SchemaNodeId, ConversionError> {
514 self.convert_node_inner(node_id, true)
515 }
516
517 fn convert_node_inner(
518 &mut self,
519 node_id: NodeId,
520 allow_non_type_codegen: bool,
521 ) -> Result<SchemaNodeId, ConversionError> {
522 let parsed: ParsedSchemaNode = self.doc.parse(node_id)?;
524 let ParsedSchemaNode {
525 content: parsed_content,
526 metadata: parsed_metadata,
527 ext_types: parsed_ext_types,
528 codegen: parsed_codegen,
529 } = parsed;
530
531 let content = self.convert_content(parsed_content)?;
533 let metadata = self.convert_metadata(parsed_metadata)?;
534 let ext_types = self.convert_ext_types(parsed_ext_types)?;
535 let type_codegen =
536 self.convert_type_codegen(parsed_codegen, &content, allow_non_type_codegen)?;
537
538 let schema_id = self.schema.create_node(content);
540 let schema_node = self.schema.node_mut(schema_id);
541 schema_node.metadata = metadata;
542 schema_node.ext_types = ext_types;
543 schema_node.type_codegen = type_codegen;
544
545 self.source_map.insert(
547 schema_id,
548 SchemaSource {
549 uri: self.base_uri.clone(),
550 node_id,
551 },
552 );
553 Ok(schema_id)
554 }
555
556 fn convert_type_codegen(
557 &self,
558 codegen_node_id: Option<NodeId>,
559 content: &SchemaNodeContent,
560 allow_non_type_codegen: bool,
561 ) -> Result<TypeCodegen, ConversionError> {
562 let Some(codegen_node_id) = codegen_node_id else {
563 return Ok(TypeCodegen::None);
564 };
565
566 if allow_non_type_codegen
567 && !matches!(
568 content,
569 SchemaNodeContent::Record(_) | SchemaNodeContent::Union(_)
570 )
571 {
572 return Ok(TypeCodegen::None);
573 }
574
575 match content {
576 SchemaNodeContent::Union(_) => Ok(TypeCodegen::Union(
577 self.doc.parse::<UnionCodegen>(codegen_node_id)?,
578 )),
579 _ => Ok(TypeCodegen::Record(
580 self.doc.parse::<RecordCodegen>(codegen_node_id)?,
581 )),
582 }
583 }
584
585 fn convert_content(
587 &mut self,
588 content: ParsedSchemaNodeContent,
589 ) -> Result<SchemaNodeContent, ConversionError> {
590 match content {
591 ParsedSchemaNodeContent::Any => Ok(SchemaNodeContent::Any),
592 ParsedSchemaNodeContent::Boolean => Ok(SchemaNodeContent::Boolean),
593 ParsedSchemaNodeContent::Null => Ok(SchemaNodeContent::Null),
594 ParsedSchemaNodeContent::Text(schema) => Ok(SchemaNodeContent::Text(schema)),
595 ParsedSchemaNodeContent::Reference(type_ref) => {
596 Ok(SchemaNodeContent::Reference(type_ref))
597 }
598
599 ParsedSchemaNodeContent::Integer(parsed) => Ok(SchemaNodeContent::Integer(
600 self.convert_integer_schema(parsed)?,
601 )),
602 ParsedSchemaNodeContent::Float(parsed) => {
603 Ok(SchemaNodeContent::Float(self.convert_float_schema(parsed)?))
604 }
605 ParsedSchemaNodeContent::Literal(node_id) => {
606 Ok(SchemaNodeContent::Literal(self.node_to_document(node_id)?))
607 }
608 ParsedSchemaNodeContent::Array(parsed) => {
609 Ok(SchemaNodeContent::Array(self.convert_array_schema(parsed)?))
610 }
611 ParsedSchemaNodeContent::Map(parsed) => {
612 Ok(SchemaNodeContent::Map(self.convert_map_schema(parsed)?))
613 }
614 ParsedSchemaNodeContent::Record(parsed) => Ok(SchemaNodeContent::Record(
615 self.convert_record_schema(parsed)?,
616 )),
617 ParsedSchemaNodeContent::Tuple(parsed) => {
618 Ok(SchemaNodeContent::Tuple(self.convert_tuple_schema(parsed)?))
619 }
620 ParsedSchemaNodeContent::Union(parsed) => {
621 Ok(SchemaNodeContent::Union(self.convert_union_schema(parsed)?))
622 }
623 }
624 }
625
626 fn convert_integer_schema(
628 &self,
629 parsed: ParsedIntegerSchema,
630 ) -> Result<IntegerSchema, ConversionError> {
631 let (min, max) = if let Some(range_str) = &parsed.range {
632 parse_integer_range(range_str)?
633 } else {
634 (Bound::Unbounded, Bound::Unbounded)
635 };
636
637 Ok(IntegerSchema {
638 min,
639 max,
640 multiple_of: parsed.multiple_of,
641 })
642 }
643
644 fn convert_float_schema(
646 &self,
647 parsed: ParsedFloatSchema,
648 ) -> Result<FloatSchema, ConversionError> {
649 let (min, max) = if let Some(range_str) = &parsed.range {
650 parse_float_range(range_str)?
651 } else {
652 (Bound::Unbounded, Bound::Unbounded)
653 };
654
655 let precision = match parsed.precision.as_deref() {
656 Some("f32") => FloatPrecision::F32,
657 Some("f64") | None => FloatPrecision::F64,
658 Some(other) => {
659 return Err(ConversionError::InvalidPrecision(other.to_string()));
660 }
661 };
662
663 Ok(FloatSchema {
664 min,
665 max,
666 multiple_of: parsed.multiple_of,
667 precision,
668 })
669 }
670
671 fn convert_array_schema(
673 &mut self,
674 parsed: ParsedArraySchema,
675 ) -> Result<ArraySchema, ConversionError> {
676 let item = self.convert_node(parsed.item)?;
677 let contains = parsed
678 .contains
679 .map(|id| self.convert_node(id))
680 .transpose()?;
681
682 Ok(ArraySchema {
683 item,
684 min_length: parsed.min_length,
685 max_length: parsed.max_length,
686 unique: parsed.unique,
687 contains,
688 binding_style: parsed.binding_style,
689 })
690 }
691
692 fn convert_map_schema(
694 &mut self,
695 parsed: ParsedMapSchema,
696 ) -> Result<MapSchema, ConversionError> {
697 let key = self.convert_node(parsed.key)?;
698 let value = self.convert_node(parsed.value)?;
699
700 Ok(MapSchema {
701 key,
702 value,
703 min_size: parsed.min_size,
704 max_size: parsed.max_size,
705 })
706 }
707
708 fn convert_tuple_schema(
710 &mut self,
711 parsed: ParsedTupleSchema,
712 ) -> Result<TupleSchema, ConversionError> {
713 let elements: Vec<SchemaNodeId> = parsed
714 .elements
715 .iter()
716 .map(|&id| self.convert_node(id))
717 .collect::<Result<_, _>>()?;
718
719 Ok(TupleSchema {
720 elements,
721 binding_style: parsed.binding_style,
722 })
723 }
724
725 fn convert_record_schema(
727 &mut self,
728 parsed: ParsedRecordSchema,
729 ) -> Result<RecordSchema, ConversionError> {
730 let mut properties = IndexMap::new();
731
732 for (field_name, field_parsed) in parsed.properties {
733 let schema = self.convert_node_allow_non_type_codegen(field_parsed.schema)?;
734 properties.insert(
735 field_name,
736 RecordFieldSchema {
737 schema,
738 optional: field_parsed.optional,
739 binding_style: field_parsed.binding_style,
740 field_codegen: field_parsed.codegen.unwrap_or_default(),
741 },
742 );
743 }
744
745 let flatten = parsed
747 .flatten
748 .into_iter()
749 .map(|id| self.convert_node(id))
750 .collect::<Result<Vec<_>, _>>()?;
751
752 let unknown_fields = self.convert_unknown_fields_policy(parsed.unknown_fields)?;
753
754 Ok(RecordSchema {
755 properties,
756 flatten,
757 unknown_fields,
758 })
759 }
760
761 fn convert_union_schema(
763 &mut self,
764 parsed: ParsedUnionSchema,
765 ) -> Result<UnionSchema, ConversionError> {
766 let ParsedUnionSchema {
767 variants: parsed_variants,
768 unambiguous,
769 interop,
770 deny_untagged,
771 } = parsed;
772 let mut variants = IndexMap::new();
773
774 for (variant_name, variant_node_id) in parsed_variants {
775 let schema = self.convert_node(variant_node_id)?;
776 variants.insert(variant_name, schema);
777 }
778
779 Ok(UnionSchema {
780 variants,
781 unambiguous,
782 interop,
783 deny_untagged,
784 })
785 }
786
787 fn convert_unknown_fields_policy(
789 &mut self,
790 parsed: ParsedUnknownFieldsPolicy,
791 ) -> Result<UnknownFieldsPolicy, ConversionError> {
792 match parsed {
793 ParsedUnknownFieldsPolicy::Deny => Ok(UnknownFieldsPolicy::Deny),
794 ParsedUnknownFieldsPolicy::Allow => Ok(UnknownFieldsPolicy::Allow),
795 ParsedUnknownFieldsPolicy::Schema(node_id) => {
796 let schema = self.convert_node(node_id)?;
797 Ok(UnknownFieldsPolicy::Schema(schema))
798 }
799 }
800 }
801
802 fn convert_metadata(
804 &mut self,
805 parsed: ParsedSchemaMetadata,
806 ) -> Result<SchemaMetadata, ConversionError> {
807 let default = parsed
808 .default
809 .map(|id| self.node_to_document(id))
810 .transpose()?;
811
812 let examples = parsed
813 .examples
814 .map(|ids| {
815 ids.into_iter()
816 .map(|id| self.node_to_document(id))
817 .collect::<Result<Vec<_>, _>>()
818 })
819 .transpose()?;
820
821 Ok(SchemaMetadata {
822 description: parsed.description,
823 deprecated: parsed.deprecated,
824 default,
825 examples,
826 })
827 }
828
829 fn convert_ext_types(
831 &mut self,
832 parsed: IndexMap<Identifier, ParsedExtTypeSchema>,
833 ) -> Result<IndexMap<Identifier, ExtTypeSchema>, ConversionError> {
834 let mut result = IndexMap::new();
835
836 for (name, parsed_schema) in parsed {
837 let schema = self.convert_node(parsed_schema.schema)?;
838 result.insert(
839 name,
840 ExtTypeSchema {
841 schema,
842 optional: parsed_schema.optional,
843 binding_style: parsed_schema.binding_style,
844 },
845 );
846 }
847
848 Ok(result)
849 }
850
851 fn node_to_document(&self, node_id: NodeId) -> Result<EureDocument, ConversionError> {
853 let mut new_doc = EureDocument::new();
854 let root_id = new_doc.get_root_id();
855 self.copy_node_to(&mut new_doc, root_id, node_id)?;
856 Ok(new_doc)
857 }
858
859 fn copy_node_to(
861 &self,
862 dest: &mut EureDocument,
863 dest_node_id: NodeId,
864 src_node_id: NodeId,
865 ) -> Result<(), ConversionError> {
866 let src_node = self.doc.node(src_node_id);
867
868 let children_to_copy: Vec<_> = match &src_node.content {
870 NodeValue::Primitive(prim) => {
871 dest.set_content(dest_node_id, NodeValue::Primitive(prim.clone()));
872 vec![]
873 }
874 NodeValue::Array(arr) => {
875 dest.set_content(dest_node_id, NodeValue::empty_array());
876 arr.to_vec()
877 }
878 NodeValue::Tuple(tup) => {
879 dest.set_content(dest_node_id, NodeValue::empty_tuple());
880 tup.to_vec()
881 }
882 NodeValue::Map(map) => {
883 dest.set_content(dest_node_id, NodeValue::empty_map());
884 map.iter()
885 .map(|(k, &v)| (k.clone(), v))
886 .collect::<Vec<_>>()
887 .into_iter()
888 .map(|(_, v)| v)
889 .collect()
890 }
891 NodeValue::PartialMap(_) => {
892 return Err(ConversionError::UnsupportedLiteralValue {
893 node_id: src_node_id,
894 kind: ValueKind::PartialMap,
895 });
896 }
897 NodeValue::Hole(_) => {
898 return Err(ConversionError::UnsupportedLiteralValue {
899 node_id: src_node_id,
900 kind: ValueKind::Hole,
901 });
902 }
903 };
904
905 let src_node = self.doc.node(src_node_id);
912 match &src_node.content {
913 NodeValue::Array(_) => {
914 for child_id in children_to_copy {
915 let new_child_id = dest.add_array_element(None, dest_node_id)?.node_id;
916 self.copy_node_to(dest, new_child_id, child_id)?;
917 }
918 }
919 NodeValue::Tuple(_) => {
920 for (index, child_id) in children_to_copy.into_iter().enumerate() {
921 let new_child_id = dest.add_tuple_element(index as u8, dest_node_id)?.node_id;
922 self.copy_node_to(dest, new_child_id, child_id)?;
923 }
924 }
925 NodeValue::Map(map) => {
926 for (key, &child_id) in map.iter() {
927 let new_child_id = dest.add_map_child(key.clone(), dest_node_id)?.node_id;
928 self.copy_node_to(dest, new_child_id, child_id)?;
929 }
930 }
931 _ => {}
932 }
933
934 Ok(())
935 }
936}
937
938fn parse_integer_range(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
940 let s = s.trim();
941
942 if s.starts_with('[') || s.starts_with('(') {
944 return parse_interval_integer(s);
945 }
946
947 if let Some(eq_pos) = s.find("..=") {
949 let left = &s[..eq_pos];
950 let right = &s[eq_pos + 3..];
951 let min = if left.is_empty() {
952 Bound::Unbounded
953 } else {
954 Bound::Inclusive(parse_bigint(left)?)
955 };
956 let max = if right.is_empty() {
957 Bound::Unbounded
958 } else {
959 Bound::Inclusive(parse_bigint(right)?)
960 };
961 Ok((min, max))
962 } else if let Some(dot_pos) = s.find("..") {
963 let left = &s[..dot_pos];
964 let right = &s[dot_pos + 2..];
965 let min = if left.is_empty() {
966 Bound::Unbounded
967 } else {
968 Bound::Inclusive(parse_bigint(left)?)
969 };
970 let max = if right.is_empty() {
971 Bound::Unbounded
972 } else {
973 Bound::Exclusive(parse_bigint(right)?)
974 };
975 Ok((min, max))
976 } else {
977 Err(ConversionError::InvalidRangeString(s.to_string()))
978 }
979}
980
981fn parse_interval_integer(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
983 let left_inclusive = s.starts_with('[');
984 let right_inclusive = s.ends_with(']');
985
986 let inner = &s[1..s.len() - 1];
987 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
988 if parts.len() != 2 {
989 return Err(ConversionError::InvalidRangeString(s.to_string()));
990 }
991
992 let min = if parts[0].is_empty() {
993 Bound::Unbounded
994 } else if left_inclusive {
995 Bound::Inclusive(parse_bigint(parts[0])?)
996 } else {
997 Bound::Exclusive(parse_bigint(parts[0])?)
998 };
999
1000 let max = if parts[1].is_empty() {
1001 Bound::Unbounded
1002 } else if right_inclusive {
1003 Bound::Inclusive(parse_bigint(parts[1])?)
1004 } else {
1005 Bound::Exclusive(parse_bigint(parts[1])?)
1006 };
1007
1008 Ok((min, max))
1009}
1010
1011fn parse_float_range(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
1013 let s = s.trim();
1014
1015 if s.starts_with('[') || s.starts_with('(') {
1017 return parse_interval_float(s);
1018 }
1019
1020 if let Some(eq_pos) = s.find("..=") {
1022 let left = &s[..eq_pos];
1023 let right = &s[eq_pos + 3..];
1024 let min = if left.is_empty() {
1025 Bound::Unbounded
1026 } else {
1027 Bound::Inclusive(parse_f64(left)?)
1028 };
1029 let max = if right.is_empty() {
1030 Bound::Unbounded
1031 } else {
1032 Bound::Inclusive(parse_f64(right)?)
1033 };
1034 Ok((min, max))
1035 } else if let Some(dot_pos) = s.find("..") {
1036 let left = &s[..dot_pos];
1037 let right = &s[dot_pos + 2..];
1038 let min = if left.is_empty() {
1039 Bound::Unbounded
1040 } else {
1041 Bound::Inclusive(parse_f64(left)?)
1042 };
1043 let max = if right.is_empty() {
1044 Bound::Unbounded
1045 } else {
1046 Bound::Exclusive(parse_f64(right)?)
1047 };
1048 Ok((min, max))
1049 } else {
1050 Err(ConversionError::InvalidRangeString(s.to_string()))
1051 }
1052}
1053
1054fn parse_interval_float(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
1056 let left_inclusive = s.starts_with('[');
1057 let right_inclusive = s.ends_with(']');
1058
1059 let inner = &s[1..s.len() - 1];
1060 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
1061 if parts.len() != 2 {
1062 return Err(ConversionError::InvalidRangeString(s.to_string()));
1063 }
1064
1065 let min = if parts[0].is_empty() {
1066 Bound::Unbounded
1067 } else if left_inclusive {
1068 Bound::Inclusive(parse_f64(parts[0])?)
1069 } else {
1070 Bound::Exclusive(parse_f64(parts[0])?)
1071 };
1072
1073 let max = if parts[1].is_empty() {
1074 Bound::Unbounded
1075 } else if right_inclusive {
1076 Bound::Inclusive(parse_f64(parts[1])?)
1077 } else {
1078 Bound::Exclusive(parse_f64(parts[1])?)
1079 };
1080
1081 Ok((min, max))
1082}
1083
1084fn collect_document_node_paths(doc: &EureDocument) -> IndexMap<NodeId, EurePath> {
1085 fn dfs(
1086 doc: &EureDocument,
1087 node_id: NodeId,
1088 path: &mut Vec<PathSegment>,
1089 out: &mut IndexMap<NodeId, EurePath>,
1090 visited: &mut std::collections::HashSet<NodeId>,
1091 ) {
1092 if !visited.insert(node_id) {
1093 return;
1094 }
1095 out.insert(node_id, EurePath(path.clone()));
1096
1097 let node = doc.node(node_id);
1098 for (ext, &child_id) in node.extensions.iter() {
1099 path.push(PathSegment::Extension(ext.clone()));
1100 dfs(doc, child_id, path, out, visited);
1101 path.pop();
1102 }
1103 match &node.content {
1104 NodeValue::Array(array) => {
1105 for (index, &child_id) in array.iter().enumerate() {
1106 path.push(PathSegment::ArrayIndex(ArrayIndexKind::Specific(index)));
1107 dfs(doc, child_id, path, out, visited);
1108 path.pop();
1109 }
1110 }
1111 NodeValue::Tuple(tuple) => {
1112 for (index, &child_id) in tuple.iter().enumerate() {
1113 path.push(PathSegment::TupleIndex(index as u8));
1114 dfs(doc, child_id, path, out, visited);
1115 path.pop();
1116 }
1117 }
1118 NodeValue::Map(map) => {
1119 for (key, &child_id) in map.iter() {
1120 path.push(PathSegment::Value(key.clone()));
1121 dfs(doc, child_id, path, out, visited);
1122 path.pop();
1123 }
1124 }
1125 NodeValue::PartialMap(map) => {
1126 for (key, &child_id) in map.iter() {
1127 path.push(PathSegment::from_partial_object_key(key.clone()));
1128 dfs(doc, child_id, path, out, visited);
1129 path.pop();
1130 }
1131 }
1132 NodeValue::Primitive(_) | NodeValue::Hole(_) => {}
1133 }
1134 }
1135
1136 let mut out = IndexMap::new();
1137 let mut path = Vec::new();
1138 let mut visited = std::collections::HashSet::new();
1139 dfs(doc, doc.get_root_id(), &mut path, &mut out, &mut visited);
1140 out
1141}
1142
1143fn collect_loaded_document_node_paths(
1144 loaded: &LoadedSchemaSet,
1145) -> IndexMap<ResolvedSchemaUri, IndexMap<NodeId, EurePath>> {
1146 loaded
1147 .documents
1148 .iter()
1149 .map(|(uri, doc)| (uri.clone(), collect_document_node_paths(doc)))
1150 .collect()
1151}
1152
1153fn schema_node_fallback_path(schema_id: SchemaNodeId) -> EurePath {
1154 EurePath(vec![PathSegment::Value(ObjectKey::String(format!(
1155 "schema-node-{}",
1156 schema_id.0
1157 )))])
1158}
1159
1160fn build_layout_strategies(
1161 schema: &SchemaDocument,
1162 source_map: &SchemaSourceMap,
1163 source_node_paths: &IndexMap<ResolvedSchemaUri, IndexMap<NodeId, EurePath>>,
1164) -> LayoutStrategies {
1165 let mut layout = LayoutStrategies::default();
1166
1167 for (schema_id, source) in source_map {
1168 if let Some(path) = source_node_paths
1169 .get(&source.uri)
1170 .and_then(|paths| paths.get(&source.node_id))
1171 {
1172 layout.schema_node_paths.insert(*schema_id, path.clone());
1173 }
1174 }
1175
1176 for schema_index in 0..schema.nodes.len() {
1177 let schema_id = SchemaNodeId(schema_index);
1178 let schema_node = schema.node(schema_id);
1179
1180 let node_path = layout
1181 .schema_node_paths
1182 .get(&schema_id)
1183 .cloned()
1184 .unwrap_or_else(|| schema_node_fallback_path(schema_id));
1185
1186 if let SchemaNodeContent::Array(array_schema) = &schema_node.content
1187 && let Some(style) = array_schema.binding_style
1188 {
1189 layout.by_path.insert(node_path.clone(), style);
1190 }
1191 if let SchemaNodeContent::Tuple(tuple_schema) = &schema_node.content
1192 && let Some(style) = tuple_schema.binding_style
1193 {
1194 layout.by_path.insert(node_path.clone(), style);
1195 }
1196
1197 if let SchemaNodeContent::Record(record_schema) = &schema_node.content {
1198 let mut order = Vec::new();
1199 for (field_name, field_schema) in &record_schema.properties {
1200 order.push(PathSegment::Value(ObjectKey::String(field_name.clone())));
1201 if let Some(style) = field_schema.binding_style {
1202 let child_path = layout
1203 .schema_node_paths
1204 .get(&field_schema.schema)
1205 .cloned()
1206 .unwrap_or_else(|| schema_node_fallback_path(field_schema.schema));
1207 layout.by_path.insert(child_path, style);
1208 }
1209 }
1210 for ext_name in schema_node.ext_types.keys() {
1211 order.push(PathSegment::Extension(ext_name.clone()));
1212 }
1213 if !order.is_empty() {
1214 layout.order_by_path.insert(node_path.clone(), order);
1215 }
1216 }
1217
1218 for ext_schema in schema_node.ext_types.values() {
1219 if let Some(style) = ext_schema.binding_style {
1220 let ext_path = layout
1221 .schema_node_paths
1222 .get(&ext_schema.schema)
1223 .cloned()
1224 .unwrap_or_else(|| schema_node_fallback_path(ext_schema.schema));
1225 layout.by_path.insert(ext_path, style);
1226 }
1227 }
1228 }
1229
1230 layout
1231}
1232
1233fn parse_bigint(s: &str) -> Result<BigInt, ConversionError> {
1234 s.parse()
1235 .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid integer: {}", s)))
1236}
1237
1238fn parse_f64(s: &str) -> Result<f64, ConversionError> {
1239 s.parse()
1240 .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid float: {}", s)))
1241}
1242
1243pub(crate) fn convert_with_visited(
1280 loaded: &LoadedSchemaSet,
1281 base_uri: ResolvedSchemaUri,
1282 doc: &EureDocument,
1283 visited: &mut IndexSet<ResolvedSchemaUri>,
1284) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
1285 if !visited.insert(base_uri.clone()) {
1286 return Err(ConversionError::ImportCycle {
1287 cycle: visited.iter().cloned().collect(),
1288 attempted: base_uri,
1289 });
1290 }
1291 let result = Converter::new(doc, loaded, base_uri.clone()).run(visited);
1292 visited.shift_remove(&base_uri);
1293 result
1294}
1295
1296pub fn loaded_schema_set_to_schema(
1298 loaded: &LoadedSchemaSet,
1299) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
1300 let root_doc =
1301 loaded
1302 .document(&loaded.root_uri)
1303 .ok_or_else(|| ConversionError::ImportNotLoaded {
1304 base: loaded.root_uri.clone(),
1305 alias: "<root>".to_string(),
1306 raw_path: loaded.root_uri.to_string(),
1307 })?;
1308 let mut visited = IndexSet::new();
1309 convert_with_visited(loaded, loaded.root_uri.clone(), root_doc, &mut visited)
1310}
1311
1312pub fn loaded_schema_set_to_schema_with_layout(
1314 loaded: &LoadedSchemaSet,
1315) -> Result<(SchemaDocument, LayoutStrategies, SchemaSourceMap), ConversionError> {
1316 let (schema, source_map) = loaded_schema_set_to_schema(loaded)?;
1317 let source_node_paths = collect_loaded_document_node_paths(loaded);
1318 let layout = build_layout_strategies(&schema, &source_map, &source_node_paths);
1319 Ok((schema, layout, source_map))
1320}
1321
1322pub fn document_to_schema_with_layout(
1326 doc: &EureDocument,
1327) -> Result<(SchemaDocument, LayoutStrategies, SchemaSourceMap), ConversionError> {
1328 let root_uri = ResolvedSchemaUri::Inline("<root>".to_string());
1329 let loaded = LoadedSchemaSet::new(root_uri, doc.clone());
1330 loaded_schema_set_to_schema_with_layout(&loaded)
1331}
1332
1333pub fn document_to_schema(
1336 doc: &EureDocument,
1337) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
1338 let (schema, _layout, source_map) = document_to_schema_with_layout(doc)?;
1339 Ok((schema, source_map))
1340}
1341
1342fn remap_node_content(content: &SchemaNodeContent, remap: &[SchemaNodeId]) -> SchemaNodeContent {
1349 let map_id = |id: SchemaNodeId| remap[id.0];
1350 match content {
1351 SchemaNodeContent::Reference(TypeReference::Resolved(id)) => {
1352 SchemaNodeContent::Reference(TypeReference::Resolved(map_id(*id)))
1353 }
1354 SchemaNodeContent::Reference(TypeReference::Named { namespace, name }) => {
1355 SchemaNodeContent::Reference(TypeReference::Named {
1356 namespace: namespace.clone(),
1357 name: name.clone(),
1358 })
1359 }
1360 SchemaNodeContent::Array(a) => SchemaNodeContent::Array(ArraySchema {
1361 item: map_id(a.item),
1362 min_length: a.min_length,
1363 max_length: a.max_length,
1364 unique: a.unique,
1365 contains: a.contains.map(map_id),
1366 binding_style: a.binding_style,
1367 }),
1368 SchemaNodeContent::Map(m) => SchemaNodeContent::Map(MapSchema {
1369 key: map_id(m.key),
1370 value: map_id(m.value),
1371 min_size: m.min_size,
1372 max_size: m.max_size,
1373 }),
1374 SchemaNodeContent::Record(r) => {
1375 let mut properties = IndexMap::new();
1376 for (name, field) in &r.properties {
1377 properties.insert(
1378 name.clone(),
1379 RecordFieldSchema {
1380 schema: map_id(field.schema),
1381 optional: field.optional,
1382 binding_style: field.binding_style,
1383 field_codegen: field.field_codegen.clone(),
1384 },
1385 );
1386 }
1387 let flatten: Vec<SchemaNodeId> = r.flatten.iter().map(|id| map_id(*id)).collect();
1388 let unknown_fields = match &r.unknown_fields {
1389 UnknownFieldsPolicy::Schema(id) => UnknownFieldsPolicy::Schema(map_id(*id)),
1390 UnknownFieldsPolicy::Deny => UnknownFieldsPolicy::Deny,
1391 UnknownFieldsPolicy::Allow => UnknownFieldsPolicy::Allow,
1392 };
1393 SchemaNodeContent::Record(RecordSchema {
1394 properties,
1395 flatten,
1396 unknown_fields,
1397 })
1398 }
1399 SchemaNodeContent::Tuple(t) => SchemaNodeContent::Tuple(TupleSchema {
1400 elements: t.elements.iter().map(|id| map_id(*id)).collect(),
1401 binding_style: t.binding_style,
1402 }),
1403 SchemaNodeContent::Union(u) => {
1404 let mut variants = IndexMap::new();
1405 for (name, &id) in &u.variants {
1406 variants.insert(name.clone(), map_id(id));
1407 }
1408 SchemaNodeContent::Union(UnionSchema {
1409 variants,
1410 unambiguous: u.unambiguous.clone(),
1411 interop: u.interop.clone(),
1412 deny_untagged: u.deny_untagged.clone(),
1413 })
1414 }
1415 SchemaNodeContent::Any => SchemaNodeContent::Any,
1417 SchemaNodeContent::Boolean => SchemaNodeContent::Boolean,
1418 SchemaNodeContent::Null => SchemaNodeContent::Null,
1419 SchemaNodeContent::Text(t) => SchemaNodeContent::Text(t.clone()),
1420 SchemaNodeContent::Integer(i) => SchemaNodeContent::Integer(i.clone()),
1421 SchemaNodeContent::Float(f) => SchemaNodeContent::Float(f.clone()),
1422 SchemaNodeContent::Literal(d) => SchemaNodeContent::Literal(d.clone()),
1423 }
1424}
1425
1426fn remap_ext_types(
1427 ext_types: &IndexMap<Identifier, ExtTypeSchema>,
1428 remap: &[SchemaNodeId],
1429) -> IndexMap<Identifier, ExtTypeSchema> {
1430 let mut out = IndexMap::new();
1431 for (name, ext) in ext_types {
1432 out.insert(
1433 name.clone(),
1434 ExtTypeSchema {
1435 schema: remap[ext.schema.0],
1436 optional: ext.optional,
1437 binding_style: ext.binding_style,
1438 },
1439 );
1440 }
1441 out
1442}
1443
1444#[cfg(test)]
1445mod tests {
1446 use super::*;
1447 use crate::identifiers::{EXT_TYPE, OPTIONAL};
1448 use eure_document::document::node::NodeMap;
1449 use eure_document::eure;
1450 use eure_document::text::Text;
1451 use eure_document::value::PrimitiveValue;
1452
1453 fn create_schema_with_field_ext_type(ext_type_content: NodeValue) -> EureDocument {
1455 let mut doc = EureDocument::new();
1456 let root_id = doc.get_root_id();
1457
1458 let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1460 Text::inline_implicit("text"),
1461 )));
1462
1463 let ext_type_id = doc.create_node(ext_type_content);
1465 doc.node_mut(field_value_id)
1466 .extensions
1467 .insert(EXT_TYPE.clone(), ext_type_id);
1468
1469 let mut root_map = NodeMap::default();
1471 root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
1472 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1473
1474 doc
1475 }
1476
1477 #[test]
1478 fn extract_ext_types_not_map() {
1479 let doc = create_schema_with_field_ext_type(NodeValue::Primitive(PrimitiveValue::Integer(
1482 1.into(),
1483 )));
1484
1485 let err = document_to_schema(&doc).unwrap_err();
1486 use eure_document::parse::ParseErrorKind;
1487 use eure_document::value::ValueKind;
1488 assert_eq!(
1489 err,
1490 ConversionError::ParseError(ParseError {
1491 node_id: NodeId(2),
1492 kind: ParseErrorKind::TypeMismatch {
1493 expected: ValueKind::Map,
1494 actual: ValueKind::Integer,
1495 }
1496 })
1497 );
1498 }
1499
1500 #[test]
1501 fn extract_ext_types_invalid_key() {
1502 let mut doc = EureDocument::new();
1505 let root_id = doc.get_root_id();
1506
1507 let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1509 Text::inline_implicit("text"),
1510 )));
1511
1512 let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1515 Text::inline_implicit("text"),
1516 )));
1517 let mut ext_type_map = NodeMap::default();
1518 ext_type_map.insert(ObjectKey::Number(0.into()), ext_type_value_id);
1519
1520 let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
1521 doc.node_mut(field_value_id)
1522 .extensions
1523 .insert(EXT_TYPE.clone(), ext_type_id);
1524
1525 let mut root_map = NodeMap::default();
1527 root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
1528 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1529
1530 let err = document_to_schema(&doc).unwrap_err();
1531 use eure_document::parse::ParseErrorKind;
1532 assert_eq!(
1533 err,
1534 ConversionError::ParseError(ParseError {
1535 node_id: ext_type_value_id,
1537 kind: ParseErrorKind::InvalidKeyType(ObjectKey::Number(0.into()))
1538 })
1539 );
1540 }
1541
1542 #[test]
1543 fn extract_ext_types_invalid_optional() {
1544 let mut doc = EureDocument::new();
1547 let root_id = doc.get_root_id();
1548
1549 let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1551 Text::inline_implicit("text"),
1552 )));
1553
1554 let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1556 Text::inline_implicit("text"),
1557 )));
1558 let optional_node_id =
1559 doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
1560 doc.node_mut(ext_type_value_id)
1561 .extensions
1562 .insert(OPTIONAL.clone(), optional_node_id);
1563
1564 let mut ext_type_map = NodeMap::default();
1566 ext_type_map.insert(ObjectKey::String("desc".to_string()), ext_type_value_id);
1567
1568 let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
1569 doc.node_mut(field_value_id)
1570 .extensions
1571 .insert(EXT_TYPE.clone(), ext_type_id);
1572
1573 let mut root_map = NodeMap::default();
1575 root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
1576 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1577
1578 let err = document_to_schema(&doc).unwrap_err();
1579 use eure_document::parse::ParseErrorKind;
1580 use eure_document::value::ValueKind;
1581 assert_eq!(
1582 err,
1583 ConversionError::ParseError(ParseError {
1584 node_id: NodeId(3),
1585 kind: ParseErrorKind::TypeMismatch {
1586 expected: ValueKind::Bool,
1587 actual: ValueKind::Integer,
1588 }
1589 })
1590 );
1591 }
1592
1593 #[test]
1594 fn literal_variant_with_inline_code() {
1595 let mut doc = EureDocument::new();
1599 let root_id = doc.get_root_id();
1600
1601 let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1603 Text::plaintext("literal"),
1604 )));
1605
1606 doc.node_mut(root_id).content =
1609 NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("any")));
1610
1611 doc.node_mut(root_id)
1613 .extensions
1614 .insert("variant".parse().unwrap(), variant_value_id);
1615
1616 let (schema, _source_map) =
1617 document_to_schema(&doc).expect("Schema conversion should succeed");
1618
1619 let root_content = &schema.node(schema.root).content;
1621 match root_content {
1622 SchemaNodeContent::Literal(doc) => {
1623 match &doc.root().content {
1625 NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1626 assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
1627 }
1628 _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
1629 }
1630 }
1631 SchemaNodeContent::Any => {
1632 panic!("BUG: Got Any instead of Literal - $variant extension not detected!");
1633 }
1634 other => panic!("Expected Literal, got {:?}", other),
1635 }
1636 }
1637
1638 #[test]
1639 fn literal_variant_parsed_from_eure() {
1640 let doc = eure!({
1641 = @code("any")
1642 %variant = "literal"
1643 });
1644
1645 let (schema, _source_map) =
1646 document_to_schema(&doc).expect("Schema conversion should succeed");
1647
1648 let root_content = &schema.node(schema.root).content;
1649 match root_content {
1650 SchemaNodeContent::Literal(doc) => match &doc.root().content {
1651 NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1652 assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
1653 }
1654 _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
1655 },
1656 SchemaNodeContent::Any => {
1657 panic!(
1658 "BUG: Got Any instead of Literal - $variant extension not respected for primitive"
1659 );
1660 }
1661 other => panic!("Expected Literal, got {:?}", other),
1662 }
1663 }
1664
1665 #[test]
1666 fn literal_variant_rejects_partial_map() {
1667 let mut doc = EureDocument::new();
1668 let root_id = doc.get_root_id();
1669
1670 let value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
1671 let mut map = eure_document::map::PartialNodeMap::new();
1672 map.push(
1673 eure_document::value::PartialObjectKey::Hole(Some("x".parse().unwrap())),
1674 value_id,
1675 );
1676 doc.node_mut(root_id).content = NodeValue::PartialMap(map);
1677
1678 let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1679 Text::plaintext("literal"),
1680 )));
1681 doc.node_mut(root_id)
1682 .extensions
1683 .insert("variant".parse().unwrap(), variant_value_id);
1684
1685 let uri = ResolvedSchemaUri::Inline("<test>".to_string());
1686 let loaded = LoadedSchemaSet::new(uri.clone(), doc.clone());
1687 assert_eq!(
1688 Converter::new(&doc, &loaded, uri)
1689 .node_to_document(root_id)
1690 .unwrap_err(),
1691 ConversionError::UnsupportedLiteralValue {
1692 node_id: root_id,
1693 kind: ValueKind::PartialMap,
1694 }
1695 );
1696 }
1697
1698 #[test]
1699 fn union_with_literal_any_variant() {
1700 let mut doc = EureDocument::new();
1705 let root_id = doc.get_root_id();
1706
1707 let any_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1710 Text::inline_implicit("any"),
1711 )));
1712 let literal_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1714 Text::plaintext("literal"),
1715 )));
1716 doc.node_mut(any_variant_node)
1717 .extensions
1718 .insert("variant".parse().unwrap(), literal_ext);
1719
1720 let literal_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1722 Text::inline_implicit("any"),
1723 )));
1724
1725 let mut variants_map = NodeMap::default();
1727 variants_map.insert(ObjectKey::String("any".to_string()), any_variant_node);
1728 variants_map.insert(
1729 ObjectKey::String("literal".to_string()),
1730 literal_variant_node,
1731 );
1732 let variants_node = doc.create_node(NodeValue::Map(variants_map));
1733
1734 let union_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1736 Text::plaintext("union"),
1737 )));
1738 let untagged_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1739 Text::plaintext("untagged"),
1740 )));
1741 let mut interop_map = NodeMap::default();
1742 interop_map.insert(ObjectKey::String("variant-repr".to_string()), untagged_ext);
1743 let interop_ext = doc.create_node(NodeValue::Map(interop_map));
1744
1745 let mut root_map = NodeMap::default();
1747 root_map.insert(ObjectKey::String("variants".to_string()), variants_node);
1748
1749 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1750 doc.node_mut(root_id)
1751 .extensions
1752 .insert("variant".parse().unwrap(), union_ext);
1753 doc.node_mut(root_id)
1754 .extensions
1755 .insert("interop".parse().unwrap(), interop_ext);
1756
1757 let (schema, _source_map) =
1758 document_to_schema(&doc).expect("Schema conversion should succeed");
1759
1760 let root_content = &schema.node(schema.root).content;
1762 match root_content {
1763 SchemaNodeContent::Union(union_schema) => {
1764 let any_variant_id = union_schema
1766 .variants
1767 .get("any")
1768 .expect("'any' variant missing");
1769 let any_content = &schema.node(*any_variant_id).content;
1770 match any_content {
1771 SchemaNodeContent::Literal(doc) => match &doc.root().content {
1772 NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1773 assert_eq!(
1774 t.as_str(),
1775 "any",
1776 "'any' variant should be Literal(\"any\")"
1777 );
1778 }
1779 _ => panic!("'any' variant: expected Text, got {:?}", doc),
1780 },
1781 SchemaNodeContent::Any => {
1782 panic!(
1783 "BUG: 'any' variant is Any instead of Literal(\"any\") - $variant extension not detected!"
1784 );
1785 }
1786 other => panic!("'any' variant: expected Literal, got {:?}", other),
1787 }
1788
1789 let literal_variant_id = union_schema
1791 .variants
1792 .get("literal")
1793 .expect("'literal' variant missing");
1794 let literal_content = &schema.node(*literal_variant_id).content;
1795 match literal_content {
1796 SchemaNodeContent::Any => {
1797 }
1799 other => panic!("'literal' variant: expected Any, got {:?}", other),
1800 }
1801 }
1802 other => panic!("Expected Union, got {:?}", other),
1803 }
1804 }
1805
1806 #[test]
1807 fn extracts_layout_style_rules_from_binding_style_extensions() {
1808 let mut doc = EureDocument::new();
1809 let root_id = doc.get_root_id();
1810 doc.node_mut(root_id).content = NodeValue::empty_map();
1811
1812 let item_id = doc
1813 .add_map_child(ObjectKey::String("item".to_string()), root_id)
1814 .expect("insert item")
1815 .node_id;
1816 doc.node_mut(item_id).content =
1817 NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("integer")));
1818
1819 let style_id = doc
1820 .add_extension("binding-style".parse().unwrap(), item_id)
1821 .expect("insert binding-style")
1822 .node_id;
1823 doc.node_mut(style_id).content =
1824 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("binding-block")));
1825
1826 let (_schema, layout, _source_map) =
1827 document_to_schema_with_layout(&doc).expect("conversion succeeds");
1828
1829 let expected_path = EurePath(vec![PathSegment::Value(ObjectKey::String(
1830 "item".to_string(),
1831 ))]);
1832 let style = layout.by_path.get(&expected_path).expect("style for item");
1833 assert_eq!(*style, crate::BindingStyle::BindingBlock);
1834 }
1835
1836 #[test]
1837 fn preserves_record_property_order_in_layout_rules() {
1838 let doc = eure!({
1839 b = @code("integer")
1840 a = @code("integer")
1841 });
1842
1843 let (_schema, layout, _source_map) =
1844 document_to_schema_with_layout(&doc).expect("conversion succeeds");
1845
1846 let expected = vec![
1847 PathSegment::Value(ObjectKey::String("b".to_string())),
1848 PathSegment::Value(ObjectKey::String("a".to_string())),
1849 ];
1850 let root_order = layout
1851 .order_by_path
1852 .get(&EurePath::root())
1853 .expect("root order rule");
1854 assert_eq!(*root_order, expected);
1855 }
1856
1857 #[test]
1858 fn preserves_type_codegen_on_non_record_non_union_type_nodes() {
1859 let mut doc = EureDocument::new();
1860 let root_id = doc.get_root_id();
1861
1862 let type_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1863 Text::inline_implicit("text"),
1864 )));
1865 let type_name_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1866 Text::plaintext("BadTypeName"),
1867 )));
1868
1869 let mut codegen_map = NodeMap::default();
1870 codegen_map.insert(ObjectKey::String("type".to_string()), type_name_node);
1871 let codegen_node = doc.create_node(NodeValue::Map(codegen_map));
1872 doc.node_mut(type_node)
1873 .extensions
1874 .insert("codegen".parse().unwrap(), codegen_node);
1875
1876 let mut types_map = NodeMap::default();
1877 types_map.insert(ObjectKey::String("bad".to_string()), type_node);
1878 let types_node = doc.create_node(NodeValue::Map(types_map));
1879 doc.node_mut(root_id)
1880 .extensions
1881 .insert("types".parse().unwrap(), types_node);
1882 doc.node_mut(root_id).content =
1883 NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("text")));
1884
1885 let (schema, _source_map) = document_to_schema(&doc)
1886 .expect("type-level $codegen on non-union type nodes should be preserved");
1887
1888 let bad_ident: Identifier = "bad".parse().unwrap();
1889 let bad_type_id = schema.types.get(&bad_ident).expect("type `bad`");
1890 let type_node = schema.node(*bad_type_id);
1891 let TypeCodegen::Record(record) = &type_node.type_codegen else {
1892 panic!("expected non-union type codegen to use Record variant");
1893 };
1894 assert_eq!(record.type_name.as_deref(), Some("BadTypeName"));
1895 }
1896
1897 #[test]
1898 fn rejects_non_productive_reference_cycles() {
1899 let doc = eure!({
1900 %types.a = @code("$types.b")
1901 %types.b = @code("$types.a")
1902 data = @code("$types.a")
1903 });
1904
1905 let err = document_to_schema(&doc).expect_err("cycle must be rejected");
1906 match err {
1907 ConversionError::NonProductiveReferenceCycle(path) => {
1908 assert!(path.contains("$types.a"));
1909 assert!(path.contains("$types.b"));
1910 }
1911 other => panic!("expected NonProductiveReferenceCycle, got {:?}", other),
1912 }
1913 }
1914
1915 fn to_inline_uri(s: &str) -> ResolvedSchemaUri {
1920 ResolvedSchemaUri::Inline(s.to_string())
1921 }
1922
1923 fn loaded_single(name: &str, doc: EureDocument) -> LoadedSchemaSet {
1924 LoadedSchemaSet::new(to_inline_uri(name), doc)
1925 }
1926
1927 #[test]
1928 fn import_resolves_namespaced_reference() {
1929 let common = eure!({
1931 %types.username = @code("text")
1932 });
1933 let user = eure!({
1935 %import.common = "common"
1936 %types.user {
1937 name = @code("$types.common.username")
1938 }
1939 });
1940 let user_uri = to_inline_uri("user");
1941 let common_uri = to_inline_uri("common");
1942 let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
1943 loaded.insert_document(common_uri.clone(), common);
1944 loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
1945
1946 let (schema, _) = loaded_schema_set_to_schema(&loaded).expect("conversion succeeds");
1947
1948 let user_id: Identifier = "user".parse().unwrap();
1949 let common_id: Identifier = "common".parse().unwrap();
1950 assert!(schema.types.contains_key(&user_id));
1951 assert!(schema.imports.contains_key(&common_id));
1952 let username: Identifier = "username".parse().unwrap();
1953 let imported_username = schema.imports[&common_id].all_types[&username];
1954
1955 let user_node_id = schema.types.get(&user_id).unwrap();
1957 let SchemaNodeContent::Record(rec) = &schema.node(*user_node_id).content else {
1958 panic!("user must be a record");
1959 };
1960 let name_field = rec.properties.get("name").unwrap();
1961 let SchemaNodeContent::Reference(tr) = &schema.node(name_field.schema).content else {
1962 panic!("name field must be a Reference");
1963 };
1964 assert_eq!(*tr, TypeReference::Resolved(imported_username));
1965 }
1966
1967 #[test]
1968 fn imported_type_does_not_collide_with_similar_local_name() {
1969 let common = eure!({
1970 %types.User = @code("text")
1971 });
1972 let user = eure!({
1973 %import.common = "common"
1974 %types.common__User = @code("integer")
1975 %types.wrapper {
1976 imported = @code("$types.common.User")
1977 local = @code("$types.common__User")
1978 }
1979 });
1980 let user_uri = to_inline_uri("user");
1981 let common_uri = to_inline_uri("common");
1982 let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
1983 loaded.insert_document(common_uri.clone(), common);
1984 loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
1985
1986 let (schema, _) = loaded_schema_set_to_schema(&loaded).expect("conversion succeeds");
1987 let local_name: Identifier = "common__User".parse().unwrap();
1988 let imported_name: Identifier = "User".parse().unwrap();
1989 let common_alias: Identifier = "common".parse().unwrap();
1990 let local_id = schema.types[&local_name];
1991 let imported_id = schema.imports[&common_alias].all_types[&imported_name];
1992 assert_ne!(local_id, imported_id);
1993
1994 let wrapper_name: Identifier = "wrapper".parse().unwrap();
1995 let SchemaNodeContent::Record(rec) = &schema.node(schema.types[&wrapper_name]).content
1996 else {
1997 panic!("wrapper must be a record");
1998 };
1999 let SchemaNodeContent::Reference(imported_ref) =
2000 &schema.node(rec.properties["imported"].schema).content
2001 else {
2002 panic!("imported field must be a reference");
2003 };
2004 let SchemaNodeContent::Reference(local_ref) =
2005 &schema.node(rec.properties["local"].schema).content
2006 else {
2007 panic!("local field must be a reference");
2008 };
2009 assert_eq!(*imported_ref, TypeReference::Resolved(imported_id));
2010 assert_eq!(*local_ref, TypeReference::Resolved(local_id));
2011 }
2012
2013 #[test]
2014 fn import_cycle_is_rejected() {
2015 let a = eure!({
2016 %import.b = "b"
2017 });
2018 let b = eure!({
2019 %import.a = "a"
2020 });
2021 let a_uri = to_inline_uri("a");
2022 let b_uri = to_inline_uri("b");
2023 let mut loaded = LoadedSchemaSet::new(a_uri.clone(), a);
2024 loaded.insert_document(b_uri.clone(), b);
2025 loaded.insert_import(a_uri.clone(), "b".parse().unwrap(), b_uri.clone());
2026 loaded.insert_import(b_uri, "a".parse().unwrap(), a_uri);
2027
2028 let err = loaded_schema_set_to_schema(&loaded).expect_err("cycle must be rejected");
2029 match err {
2030 ConversionError::ImportCycle { cycle, attempted } => {
2031 let formatted = format_cycle(&cycle, &attempted);
2032 assert!(formatted.contains("a"));
2033 assert!(formatted.contains("b"));
2034 }
2035 other => panic!("expected ImportCycle, got {:?}", other),
2036 }
2037 }
2038
2039 #[test]
2040 fn unknown_import_namespace_is_rejected() {
2041 let user = eure!({
2042 %types.user {
2043 name = @code("$types.common.username")
2044 }
2045 });
2046 let err = loaded_schema_set_to_schema(&loaded_single("user", user))
2047 .expect_err("missing import must be rejected");
2048 match err {
2049 ConversionError::UnknownImportNamespace { namespace, name } => {
2050 assert_eq!(namespace, "common");
2051 assert_eq!(name, "username");
2052 }
2053 other => panic!("expected UnknownImportNamespace, got {:?}", other),
2054 }
2055 }
2056
2057 #[test]
2058 fn type_not_exported_is_rejected() {
2059 let common = eure!({
2062 %types.username = @code("text")
2063 %types."internal-helper" = @code("text")
2064 %export = ["username"]
2065 });
2066 let user = eure!({
2067 %import.common = "common"
2068 %types.user {
2069 helper = @code("$types.common.internal-helper")
2070 }
2071 });
2072 let user_uri = to_inline_uri("user");
2073 let common_uri = to_inline_uri("common");
2074 let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
2075 loaded.insert_document(common_uri.clone(), common);
2076 loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
2077
2078 let err =
2079 loaded_schema_set_to_schema(&loaded).expect_err("non-exported type must be rejected");
2080 match err {
2081 ConversionError::TypeNotExported { namespace, name } => {
2082 assert_eq!(namespace, "common");
2083 assert_eq!(name, "internal-helper");
2084 }
2085 other => panic!("expected TypeNotExported, got {:?}", other),
2086 }
2087 }
2088
2089 #[test]
2090 fn omitted_export_exposes_all_local_types() {
2091 let common = eure!({
2093 %types.username = @code("text")
2094 %types.email = @code("text")
2095 });
2096 let user = eure!({
2097 %import.common = "common"
2098 %types.user {
2099 name = @code("$types.common.username")
2100 mail = @code("$types.common.email")
2101 }
2102 });
2103 let user_uri = to_inline_uri("user");
2104 let common_uri = to_inline_uri("common");
2105 let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
2106 loaded.insert_document(common_uri.clone(), common);
2107 loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
2108 let _ = loaded_schema_set_to_schema(&loaded).expect("omitted export exposes all types");
2109 }
2110
2111 #[test]
2112 fn explicit_export_lists_become_the_export_set() {
2113 let common = eure!({
2114 %types.username = @code("text")
2115 %types.email = @code("text")
2116 %export = ["username"]
2117 });
2118 let user = eure!({
2121 %import.common = "common"
2122 });
2123 let user_uri = to_inline_uri("user");
2124 let common_uri = to_inline_uri("common");
2125 let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
2126 loaded.insert_document(common_uri.clone(), common);
2127 loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
2128 let (schema, _) = loaded_schema_set_to_schema(&loaded).expect("conversion succeeds");
2129
2130 let common_id: Identifier = "common".parse().unwrap();
2131 let username: Identifier = "username".parse().unwrap();
2132 let email: Identifier = "email".parse().unwrap();
2133 assert!(schema.imports[&common_id].all_types.contains_key(&username));
2134 assert!(schema.imports[&common_id].all_types.contains_key(&email));
2135 assert!(schema.imports[&common_id].exports.contains(&username));
2136 assert!(!schema.imports[&common_id].exports.contains(&email));
2137 }
2138
2139 #[test]
2140 fn export_lists_must_match_locally_declared_types() {
2141 let common = eure!({
2142 %types.username = @code("text")
2143 %export = ["nope"]
2144 });
2145 let err = loaded_schema_set_to_schema(&loaded_single("common", common))
2146 .expect_err("$export must reference a declared type");
2147 match err {
2148 ConversionError::ExportedNameNotDeclared { name } => {
2149 assert_eq!(name, "nope");
2150 }
2151 other => panic!("expected ExportedNameNotDeclared, got {:?}", other),
2152 }
2153 }
2154}