1use crate::parse::{
46 ParsedArraySchema, ParsedExtTypeSchema, ParsedFloatSchema, ParsedIntegerSchema,
47 ParsedMapSchema, ParsedRecordSchema, ParsedSchemaMetadata, ParsedSchemaNode,
48 ParsedSchemaNodeContent, ParsedTupleSchema, ParsedUnionSchema, ParsedUnknownFieldsPolicy,
49};
50use crate::type_path_trace::LayoutStrategies;
51use crate::{
52 ArraySchema, Bound, CodegenDefaults, ExtTypeSchema, FloatPrecision, FloatSchema, IntegerSchema,
53 MapSchema, RecordCodegen, RecordFieldSchema, RecordSchema, RootCodegen, SchemaDocument,
54 SchemaMetadata, SchemaNodeContent, SchemaNodeId, TupleSchema, TypeCodegen, UnionCodegen,
55 UnionSchema, UnknownFieldsPolicy,
56};
57use eure_document::document::node::{Node, NodeValue};
58use eure_document::document::{EureDocument, InsertErrorKind, NodeId};
59use eure_document::identifier::Identifier;
60use eure_document::parse::ParseError;
61use eure_document::path::{EurePath, PathSegment};
62use eure_document::value::{ObjectKey, ValueKind};
63use indexmap::IndexMap;
64use num_bigint::BigInt;
65use thiserror::Error;
66
67#[derive(Debug, Error, Clone, PartialEq)]
69pub enum ConversionError {
70 #[error("Invalid type name: {0}")]
71 InvalidTypeName(ObjectKey),
72
73 #[error("unsupported literal value at node {node_id:?}: {kind}")]
74 UnsupportedLiteralValue { node_id: NodeId, kind: ValueKind },
75
76 #[error("document insert error while copying literal value: {0}")]
77 DocumentInsert(#[from] InsertErrorKind),
78
79 #[error("Invalid extension value: {extension} at path {path}")]
80 InvalidExtensionValue { extension: String, path: String },
81
82 #[error("Invalid range string: {0}")]
83 InvalidRangeString(String),
84
85 #[error("Invalid precision: {0} (expected \"f32\" or \"f64\")")]
86 InvalidPrecision(String),
87
88 #[error("Undefined type reference: {0}")]
89 UndefinedTypeReference(String),
90
91 #[error("non-productive reference cycle detected: {0}")]
92 NonProductiveReferenceCycle(String),
93
94 #[error(
95 "invalid `$codegen` extension at node {node_id:?}: supported only for record/union, got {schema_kind}"
96 )]
97 InvalidTypeCodegenTarget {
98 node_id: NodeId,
99 schema_kind: String,
100 },
101
102 #[error("Parse error: {0}")]
103 ParseError(#[from] ParseError),
104}
105
106pub type SchemaSourceMap = IndexMap<SchemaNodeId, NodeId>;
109
110struct Converter<'a> {
112 doc: &'a EureDocument,
113 schema: SchemaDocument,
114 source_map: SchemaSourceMap,
116}
117
118impl<'a> Converter<'a> {
119 fn new(doc: &'a EureDocument) -> Self {
120 Self {
121 doc,
122 schema: SchemaDocument::new(),
123 source_map: IndexMap::new(),
124 }
125 }
126
127 fn convert(mut self) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
129 let root_id = self.doc.get_root_id();
130 let root_node = self.doc.node(root_id);
131
132 self.convert_types(root_node)?;
134 self.convert_root_codegen(root_node)?;
135
136 self.schema.root = self.convert_node_allow_non_type_codegen(root_id)?;
141
142 self.validate_type_references()?;
144 self.validate_non_productive_reference_cycles()?;
145
146 Ok((self.schema, self.source_map))
147 }
148
149 fn convert_root_codegen(&mut self, node: &Node) -> Result<(), ConversionError> {
150 let codegen_ident: Identifier = "codegen".parse().unwrap();
151 let codegen_defaults_ident: Identifier = "codegen-defaults".parse().unwrap();
152
153 if let Some(node_id) = node.extensions.get(&codegen_ident) {
154 let rec = self.doc.parse_record(*node_id)?;
155 self.schema.root_codegen = RootCodegen {
156 type_name: rec.parse_field_optional::<String>("type")?,
157 };
158 }
159
160 if let Some(node_id) = node.extensions.get(&codegen_defaults_ident) {
161 self.schema.codegen_defaults = self.doc.parse::<CodegenDefaults>(*node_id)?;
162 }
163
164 Ok(())
165 }
166
167 fn convert_types(&mut self, node: &Node) -> Result<(), ConversionError> {
169 let types_ident: Identifier = "types".parse().unwrap();
170 if let Some(types_node_id) = node.extensions.get(&types_ident) {
171 let types_node = self.doc.node(*types_node_id);
172 if let NodeValue::Map(map) = &types_node.content {
173 for (key, &node_id) in map.iter() {
174 if let ObjectKey::String(name) = key {
175 let type_name: Identifier = name
176 .parse()
177 .map_err(|_| ConversionError::InvalidTypeName(key.clone()))?;
178 let schema_id = self.convert_node(node_id)?;
179 self.schema.types.insert(type_name, schema_id);
180 } else {
181 return Err(ConversionError::InvalidTypeName(key.clone()));
182 }
183 }
184 } else {
185 return Err(ConversionError::InvalidExtensionValue {
186 extension: "types".to_string(),
187 path: "$types must be a map".to_string(),
188 });
189 }
190 }
191 Ok(())
192 }
193
194 fn validate_type_references(&self) -> Result<(), ConversionError> {
196 for node in &self.schema.nodes {
197 if let SchemaNodeContent::Reference(type_ref) = &node.content
198 && type_ref.namespace.is_none()
199 && !self.schema.types.contains_key(&type_ref.name)
200 {
201 return Err(ConversionError::UndefinedTypeReference(
202 type_ref.name.to_string(),
203 ));
204 }
205 }
206 Ok(())
207 }
208
209 fn validate_non_productive_reference_cycles(&self) -> Result<(), ConversionError> {
210 #[derive(Clone, Copy, PartialEq, Eq)]
211 enum Mark {
212 Visiting,
213 Done,
214 }
215
216 fn next_ref_target(schema: &SchemaDocument, node_id: SchemaNodeId) -> Option<SchemaNodeId> {
217 let node = schema.node(node_id);
218 let SchemaNodeContent::Reference(type_ref) = &node.content else {
219 return None;
220 };
221 if type_ref.namespace.is_some() {
222 return None;
223 }
224 schema.get_type(&type_ref.name)
225 }
226
227 fn display_node(schema: &SchemaDocument, id: SchemaNodeId) -> String {
228 if let Some((name, _)) = schema.types.iter().find(|(_, sid)| **sid == id) {
229 format!("$types.{}", name)
230 } else {
231 format!("node#{}", id.0)
232 }
233 }
234
235 fn visit(
236 schema: &SchemaDocument,
237 node_id: SchemaNodeId,
238 marks: &mut IndexMap<SchemaNodeId, Mark>,
239 stack: &mut Vec<SchemaNodeId>,
240 ) -> Result<(), ConversionError> {
241 if matches!(marks.get(&node_id), Some(Mark::Done)) {
242 return Ok(());
243 }
244 if matches!(marks.get(&node_id), Some(Mark::Visiting)) {
245 let start = stack.iter().position(|sid| *sid == node_id).unwrap_or(0);
246 let mut cycle: Vec<String> = stack[start..]
247 .iter()
248 .map(|sid| display_node(schema, *sid))
249 .collect();
250 cycle.push(display_node(schema, node_id));
251 return Err(ConversionError::NonProductiveReferenceCycle(
252 cycle.join(" -> "),
253 ));
254 }
255
256 marks.insert(node_id, Mark::Visiting);
257 stack.push(node_id);
258 if let Some(next_id) = next_ref_target(schema, node_id) {
259 visit(schema, next_id, marks, stack)?;
260 }
261 stack.pop();
262 marks.insert(node_id, Mark::Done);
263 Ok(())
264 }
265
266 let mut marks = IndexMap::new();
267 let mut stack = Vec::new();
268 for index in 0..self.schema.nodes.len() {
269 visit(&self.schema, SchemaNodeId(index), &mut marks, &mut stack)?;
270 }
271 Ok(())
272 }
273
274 fn convert_node(&mut self, node_id: NodeId) -> Result<SchemaNodeId, ConversionError> {
276 self.convert_node_inner(node_id, false)
277 }
278
279 fn convert_node_allow_non_type_codegen(
280 &mut self,
281 node_id: NodeId,
282 ) -> Result<SchemaNodeId, ConversionError> {
283 self.convert_node_inner(node_id, true)
284 }
285
286 fn convert_node_inner(
287 &mut self,
288 node_id: NodeId,
289 allow_non_type_codegen: bool,
290 ) -> Result<SchemaNodeId, ConversionError> {
291 let parsed: ParsedSchemaNode = self.doc.parse(node_id)?;
293 let ParsedSchemaNode {
294 content: parsed_content,
295 metadata: parsed_metadata,
296 ext_types: parsed_ext_types,
297 codegen: parsed_codegen,
298 } = parsed;
299
300 let content = self.convert_content(parsed_content)?;
302 let metadata = self.convert_metadata(parsed_metadata)?;
303 let ext_types = self.convert_ext_types(parsed_ext_types)?;
304 let type_codegen =
305 self.convert_type_codegen(parsed_codegen, &content, allow_non_type_codegen)?;
306
307 let schema_id = self.schema.create_node(content);
309 let schema_node = self.schema.node_mut(schema_id);
310 schema_node.metadata = metadata;
311 schema_node.ext_types = ext_types;
312 schema_node.type_codegen = type_codegen;
313
314 self.source_map.insert(schema_id, node_id);
316 Ok(schema_id)
317 }
318
319 fn convert_type_codegen(
320 &self,
321 codegen_node_id: Option<NodeId>,
322 content: &SchemaNodeContent,
323 allow_non_type_codegen: bool,
324 ) -> Result<TypeCodegen, ConversionError> {
325 let Some(codegen_node_id) = codegen_node_id else {
326 return Ok(TypeCodegen::None);
327 };
328
329 if allow_non_type_codegen
330 && !matches!(
331 content,
332 SchemaNodeContent::Record(_) | SchemaNodeContent::Union(_)
333 )
334 {
335 return Ok(TypeCodegen::None);
336 }
337
338 match content {
339 SchemaNodeContent::Union(_) => Ok(TypeCodegen::Union(
340 self.doc.parse::<UnionCodegen>(codegen_node_id)?,
341 )),
342 _ => Ok(TypeCodegen::Record(
343 self.doc.parse::<RecordCodegen>(codegen_node_id)?,
344 )),
345 }
346 }
347
348 fn convert_content(
350 &mut self,
351 content: ParsedSchemaNodeContent,
352 ) -> Result<SchemaNodeContent, ConversionError> {
353 match content {
354 ParsedSchemaNodeContent::Any => Ok(SchemaNodeContent::Any),
355 ParsedSchemaNodeContent::Boolean => Ok(SchemaNodeContent::Boolean),
356 ParsedSchemaNodeContent::Null => Ok(SchemaNodeContent::Null),
357 ParsedSchemaNodeContent::Text(schema) => Ok(SchemaNodeContent::Text(schema)),
358 ParsedSchemaNodeContent::Reference(type_ref) => {
359 Ok(SchemaNodeContent::Reference(type_ref))
360 }
361
362 ParsedSchemaNodeContent::Integer(parsed) => Ok(SchemaNodeContent::Integer(
363 self.convert_integer_schema(parsed)?,
364 )),
365 ParsedSchemaNodeContent::Float(parsed) => {
366 Ok(SchemaNodeContent::Float(self.convert_float_schema(parsed)?))
367 }
368 ParsedSchemaNodeContent::Literal(node_id) => {
369 Ok(SchemaNodeContent::Literal(self.node_to_document(node_id)?))
370 }
371 ParsedSchemaNodeContent::Array(parsed) => {
372 Ok(SchemaNodeContent::Array(self.convert_array_schema(parsed)?))
373 }
374 ParsedSchemaNodeContent::Map(parsed) => {
375 Ok(SchemaNodeContent::Map(self.convert_map_schema(parsed)?))
376 }
377 ParsedSchemaNodeContent::Record(parsed) => Ok(SchemaNodeContent::Record(
378 self.convert_record_schema(parsed)?,
379 )),
380 ParsedSchemaNodeContent::Tuple(parsed) => {
381 Ok(SchemaNodeContent::Tuple(self.convert_tuple_schema(parsed)?))
382 }
383 ParsedSchemaNodeContent::Union(parsed) => {
384 Ok(SchemaNodeContent::Union(self.convert_union_schema(parsed)?))
385 }
386 }
387 }
388
389 fn convert_integer_schema(
391 &self,
392 parsed: ParsedIntegerSchema,
393 ) -> Result<IntegerSchema, ConversionError> {
394 let (min, max) = if let Some(range_str) = &parsed.range {
395 parse_integer_range(range_str)?
396 } else {
397 (Bound::Unbounded, Bound::Unbounded)
398 };
399
400 Ok(IntegerSchema {
401 min,
402 max,
403 multiple_of: parsed.multiple_of,
404 })
405 }
406
407 fn convert_float_schema(
409 &self,
410 parsed: ParsedFloatSchema,
411 ) -> Result<FloatSchema, ConversionError> {
412 let (min, max) = if let Some(range_str) = &parsed.range {
413 parse_float_range(range_str)?
414 } else {
415 (Bound::Unbounded, Bound::Unbounded)
416 };
417
418 let precision = match parsed.precision.as_deref() {
419 Some("f32") => FloatPrecision::F32,
420 Some("f64") | None => FloatPrecision::F64,
421 Some(other) => {
422 return Err(ConversionError::InvalidPrecision(other.to_string()));
423 }
424 };
425
426 Ok(FloatSchema {
427 min,
428 max,
429 multiple_of: parsed.multiple_of,
430 precision,
431 })
432 }
433
434 fn convert_array_schema(
436 &mut self,
437 parsed: ParsedArraySchema,
438 ) -> Result<ArraySchema, ConversionError> {
439 let item = self.convert_node(parsed.item)?;
440 let contains = parsed
441 .contains
442 .map(|id| self.convert_node(id))
443 .transpose()?;
444
445 Ok(ArraySchema {
446 item,
447 min_length: parsed.min_length,
448 max_length: parsed.max_length,
449 unique: parsed.unique,
450 contains,
451 binding_style: parsed.binding_style,
452 })
453 }
454
455 fn convert_map_schema(
457 &mut self,
458 parsed: ParsedMapSchema,
459 ) -> Result<MapSchema, ConversionError> {
460 let key = self.convert_node(parsed.key)?;
461 let value = self.convert_node(parsed.value)?;
462
463 Ok(MapSchema {
464 key,
465 value,
466 min_size: parsed.min_size,
467 max_size: parsed.max_size,
468 })
469 }
470
471 fn convert_tuple_schema(
473 &mut self,
474 parsed: ParsedTupleSchema,
475 ) -> Result<TupleSchema, ConversionError> {
476 let elements: Vec<SchemaNodeId> = parsed
477 .elements
478 .iter()
479 .map(|&id| self.convert_node(id))
480 .collect::<Result<_, _>>()?;
481
482 Ok(TupleSchema {
483 elements,
484 binding_style: parsed.binding_style,
485 })
486 }
487
488 fn convert_record_schema(
490 &mut self,
491 parsed: ParsedRecordSchema,
492 ) -> Result<RecordSchema, ConversionError> {
493 let mut properties = IndexMap::new();
494
495 for (field_name, field_parsed) in parsed.properties {
496 let schema = self.convert_node_allow_non_type_codegen(field_parsed.schema)?;
497 properties.insert(
498 field_name,
499 RecordFieldSchema {
500 schema,
501 optional: field_parsed.optional,
502 binding_style: field_parsed.binding_style,
503 field_codegen: field_parsed.codegen.unwrap_or_default(),
504 },
505 );
506 }
507
508 let flatten = parsed
510 .flatten
511 .into_iter()
512 .map(|id| self.convert_node(id))
513 .collect::<Result<Vec<_>, _>>()?;
514
515 let unknown_fields = self.convert_unknown_fields_policy(parsed.unknown_fields)?;
516
517 Ok(RecordSchema {
518 properties,
519 flatten,
520 unknown_fields,
521 })
522 }
523
524 fn convert_union_schema(
526 &mut self,
527 parsed: ParsedUnionSchema,
528 ) -> Result<UnionSchema, ConversionError> {
529 let ParsedUnionSchema {
530 variants: parsed_variants,
531 unambiguous,
532 interop,
533 deny_untagged,
534 } = parsed;
535 let mut variants = IndexMap::new();
536
537 for (variant_name, variant_node_id) in parsed_variants {
538 let schema = self.convert_node(variant_node_id)?;
539 variants.insert(variant_name, schema);
540 }
541
542 Ok(UnionSchema {
543 variants,
544 unambiguous,
545 interop,
546 deny_untagged,
547 })
548 }
549
550 fn convert_unknown_fields_policy(
552 &mut self,
553 parsed: ParsedUnknownFieldsPolicy,
554 ) -> Result<UnknownFieldsPolicy, ConversionError> {
555 match parsed {
556 ParsedUnknownFieldsPolicy::Deny => Ok(UnknownFieldsPolicy::Deny),
557 ParsedUnknownFieldsPolicy::Allow => Ok(UnknownFieldsPolicy::Allow),
558 ParsedUnknownFieldsPolicy::Schema(node_id) => {
559 let schema = self.convert_node(node_id)?;
560 Ok(UnknownFieldsPolicy::Schema(schema))
561 }
562 }
563 }
564
565 fn convert_metadata(
567 &mut self,
568 parsed: ParsedSchemaMetadata,
569 ) -> Result<SchemaMetadata, ConversionError> {
570 let default = parsed
571 .default
572 .map(|id| self.node_to_document(id))
573 .transpose()?;
574
575 let examples = parsed
576 .examples
577 .map(|ids| {
578 ids.into_iter()
579 .map(|id| self.node_to_document(id))
580 .collect::<Result<Vec<_>, _>>()
581 })
582 .transpose()?;
583
584 Ok(SchemaMetadata {
585 description: parsed.description,
586 deprecated: parsed.deprecated,
587 default,
588 examples,
589 })
590 }
591
592 fn convert_ext_types(
594 &mut self,
595 parsed: IndexMap<Identifier, ParsedExtTypeSchema>,
596 ) -> Result<IndexMap<Identifier, ExtTypeSchema>, ConversionError> {
597 let mut result = IndexMap::new();
598
599 for (name, parsed_schema) in parsed {
600 let schema = self.convert_node(parsed_schema.schema)?;
601 result.insert(
602 name,
603 ExtTypeSchema {
604 schema,
605 optional: parsed_schema.optional,
606 binding_style: parsed_schema.binding_style,
607 },
608 );
609 }
610
611 Ok(result)
612 }
613
614 fn node_to_document(&self, node_id: NodeId) -> Result<EureDocument, ConversionError> {
616 let mut new_doc = EureDocument::new();
617 let root_id = new_doc.get_root_id();
618 self.copy_node_to(&mut new_doc, root_id, node_id)?;
619 Ok(new_doc)
620 }
621
622 fn copy_node_to(
624 &self,
625 dest: &mut EureDocument,
626 dest_node_id: NodeId,
627 src_node_id: NodeId,
628 ) -> Result<(), ConversionError> {
629 let src_node = self.doc.node(src_node_id);
630
631 let children_to_copy: Vec<_> = match &src_node.content {
633 NodeValue::Primitive(prim) => {
634 dest.set_content(dest_node_id, NodeValue::Primitive(prim.clone()));
635 vec![]
636 }
637 NodeValue::Array(arr) => {
638 dest.set_content(dest_node_id, NodeValue::empty_array());
639 arr.to_vec()
640 }
641 NodeValue::Tuple(tup) => {
642 dest.set_content(dest_node_id, NodeValue::empty_tuple());
643 tup.to_vec()
644 }
645 NodeValue::Map(map) => {
646 dest.set_content(dest_node_id, NodeValue::empty_map());
647 map.iter()
648 .map(|(k, &v)| (k.clone(), v))
649 .collect::<Vec<_>>()
650 .into_iter()
651 .map(|(_, v)| v)
652 .collect()
653 }
654 NodeValue::PartialMap(_) => {
655 return Err(ConversionError::UnsupportedLiteralValue {
656 node_id: src_node_id,
657 kind: ValueKind::PartialMap,
658 });
659 }
660 NodeValue::Hole(_) => {
661 return Err(ConversionError::UnsupportedLiteralValue {
662 node_id: src_node_id,
663 kind: ValueKind::Hole,
664 });
665 }
666 };
667
668 let src_node = self.doc.node(src_node_id);
675 match &src_node.content {
676 NodeValue::Array(_) => {
677 for child_id in children_to_copy {
678 let new_child_id = dest.add_array_element(None, dest_node_id)?.node_id;
679 self.copy_node_to(dest, new_child_id, child_id)?;
680 }
681 }
682 NodeValue::Tuple(_) => {
683 for (index, child_id) in children_to_copy.into_iter().enumerate() {
684 let new_child_id = dest.add_tuple_element(index as u8, dest_node_id)?.node_id;
685 self.copy_node_to(dest, new_child_id, child_id)?;
686 }
687 }
688 NodeValue::Map(map) => {
689 for (key, &child_id) in map.iter() {
690 let new_child_id = dest.add_map_child(key.clone(), dest_node_id)?.node_id;
691 self.copy_node_to(dest, new_child_id, child_id)?;
692 }
693 }
694 _ => {}
695 }
696
697 Ok(())
698 }
699}
700
701fn parse_integer_range(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
703 let s = s.trim();
704
705 if s.starts_with('[') || s.starts_with('(') {
707 return parse_interval_integer(s);
708 }
709
710 if let Some(eq_pos) = s.find("..=") {
712 let left = &s[..eq_pos];
713 let right = &s[eq_pos + 3..];
714 let min = if left.is_empty() {
715 Bound::Unbounded
716 } else {
717 Bound::Inclusive(parse_bigint(left)?)
718 };
719 let max = if right.is_empty() {
720 Bound::Unbounded
721 } else {
722 Bound::Inclusive(parse_bigint(right)?)
723 };
724 Ok((min, max))
725 } else if let Some(dot_pos) = s.find("..") {
726 let left = &s[..dot_pos];
727 let right = &s[dot_pos + 2..];
728 let min = if left.is_empty() {
729 Bound::Unbounded
730 } else {
731 Bound::Inclusive(parse_bigint(left)?)
732 };
733 let max = if right.is_empty() {
734 Bound::Unbounded
735 } else {
736 Bound::Exclusive(parse_bigint(right)?)
737 };
738 Ok((min, max))
739 } else {
740 Err(ConversionError::InvalidRangeString(s.to_string()))
741 }
742}
743
744fn parse_interval_integer(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
746 let left_inclusive = s.starts_with('[');
747 let right_inclusive = s.ends_with(']');
748
749 let inner = &s[1..s.len() - 1];
750 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
751 if parts.len() != 2 {
752 return Err(ConversionError::InvalidRangeString(s.to_string()));
753 }
754
755 let min = if parts[0].is_empty() {
756 Bound::Unbounded
757 } else if left_inclusive {
758 Bound::Inclusive(parse_bigint(parts[0])?)
759 } else {
760 Bound::Exclusive(parse_bigint(parts[0])?)
761 };
762
763 let max = if parts[1].is_empty() {
764 Bound::Unbounded
765 } else if right_inclusive {
766 Bound::Inclusive(parse_bigint(parts[1])?)
767 } else {
768 Bound::Exclusive(parse_bigint(parts[1])?)
769 };
770
771 Ok((min, max))
772}
773
774fn parse_float_range(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
776 let s = s.trim();
777
778 if s.starts_with('[') || s.starts_with('(') {
780 return parse_interval_float(s);
781 }
782
783 if let Some(eq_pos) = s.find("..=") {
785 let left = &s[..eq_pos];
786 let right = &s[eq_pos + 3..];
787 let min = if left.is_empty() {
788 Bound::Unbounded
789 } else {
790 Bound::Inclusive(parse_f64(left)?)
791 };
792 let max = if right.is_empty() {
793 Bound::Unbounded
794 } else {
795 Bound::Inclusive(parse_f64(right)?)
796 };
797 Ok((min, max))
798 } else if let Some(dot_pos) = s.find("..") {
799 let left = &s[..dot_pos];
800 let right = &s[dot_pos + 2..];
801 let min = if left.is_empty() {
802 Bound::Unbounded
803 } else {
804 Bound::Inclusive(parse_f64(left)?)
805 };
806 let max = if right.is_empty() {
807 Bound::Unbounded
808 } else {
809 Bound::Exclusive(parse_f64(right)?)
810 };
811 Ok((min, max))
812 } else {
813 Err(ConversionError::InvalidRangeString(s.to_string()))
814 }
815}
816
817fn parse_interval_float(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
819 let left_inclusive = s.starts_with('[');
820 let right_inclusive = s.ends_with(']');
821
822 let inner = &s[1..s.len() - 1];
823 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
824 if parts.len() != 2 {
825 return Err(ConversionError::InvalidRangeString(s.to_string()));
826 }
827
828 let min = if parts[0].is_empty() {
829 Bound::Unbounded
830 } else if left_inclusive {
831 Bound::Inclusive(parse_f64(parts[0])?)
832 } else {
833 Bound::Exclusive(parse_f64(parts[0])?)
834 };
835
836 let max = if parts[1].is_empty() {
837 Bound::Unbounded
838 } else if right_inclusive {
839 Bound::Inclusive(parse_f64(parts[1])?)
840 } else {
841 Bound::Exclusive(parse_f64(parts[1])?)
842 };
843
844 Ok((min, max))
845}
846
847fn collect_document_node_paths(doc: &EureDocument) -> IndexMap<NodeId, EurePath> {
848 fn dfs(
849 doc: &EureDocument,
850 node_id: NodeId,
851 path: &mut Vec<PathSegment>,
852 out: &mut IndexMap<NodeId, EurePath>,
853 visited: &mut std::collections::HashSet<NodeId>,
854 ) {
855 if !visited.insert(node_id) {
856 return;
857 }
858 out.insert(node_id, EurePath(path.clone()));
859
860 let node = doc.node(node_id);
861 for (ext, &child_id) in node.extensions.iter() {
862 path.push(PathSegment::Extension(ext.clone()));
863 dfs(doc, child_id, path, out, visited);
864 path.pop();
865 }
866 match &node.content {
867 NodeValue::Array(array) => {
868 for (index, &child_id) in array.iter().enumerate() {
869 path.push(PathSegment::ArrayIndex(Some(index)));
870 dfs(doc, child_id, path, out, visited);
871 path.pop();
872 }
873 }
874 NodeValue::Tuple(tuple) => {
875 for (index, &child_id) in tuple.iter().enumerate() {
876 path.push(PathSegment::TupleIndex(index as u8));
877 dfs(doc, child_id, path, out, visited);
878 path.pop();
879 }
880 }
881 NodeValue::Map(map) => {
882 for (key, &child_id) in map.iter() {
883 path.push(PathSegment::Value(key.clone()));
884 dfs(doc, child_id, path, out, visited);
885 path.pop();
886 }
887 }
888 NodeValue::PartialMap(map) => {
889 for (key, &child_id) in map.iter() {
890 path.push(PathSegment::from_partial_object_key(key.clone()));
891 dfs(doc, child_id, path, out, visited);
892 path.pop();
893 }
894 }
895 NodeValue::Primitive(_) | NodeValue::Hole(_) => {}
896 }
897 }
898
899 let mut out = IndexMap::new();
900 let mut path = Vec::new();
901 let mut visited = std::collections::HashSet::new();
902 dfs(doc, doc.get_root_id(), &mut path, &mut out, &mut visited);
903 out
904}
905
906fn schema_node_fallback_path(schema_id: SchemaNodeId) -> EurePath {
907 EurePath(vec![PathSegment::Value(ObjectKey::String(format!(
908 "schema-node-{}",
909 schema_id.0
910 )))])
911}
912
913fn build_layout_strategies(
914 schema: &SchemaDocument,
915 source_map: &SchemaSourceMap,
916 source_node_paths: &IndexMap<NodeId, EurePath>,
917) -> LayoutStrategies {
918 let mut layout = LayoutStrategies::default();
919
920 for (schema_id, source_node_id) in source_map {
921 if let Some(path) = source_node_paths.get(source_node_id) {
922 layout.schema_node_paths.insert(*schema_id, path.clone());
923 }
924 }
925
926 for schema_index in 0..schema.nodes.len() {
927 let schema_id = SchemaNodeId(schema_index);
928 let schema_node = schema.node(schema_id);
929
930 let node_path = layout
931 .schema_node_paths
932 .get(&schema_id)
933 .cloned()
934 .unwrap_or_else(|| schema_node_fallback_path(schema_id));
935
936 if let SchemaNodeContent::Array(array_schema) = &schema_node.content
937 && let Some(style) = array_schema.binding_style
938 {
939 layout.by_path.insert(node_path.clone(), style);
940 }
941 if let SchemaNodeContent::Tuple(tuple_schema) = &schema_node.content
942 && let Some(style) = tuple_schema.binding_style
943 {
944 layout.by_path.insert(node_path.clone(), style);
945 }
946
947 if let SchemaNodeContent::Record(record_schema) = &schema_node.content {
948 let mut order = Vec::new();
949 for (field_name, field_schema) in &record_schema.properties {
950 order.push(PathSegment::Value(ObjectKey::String(field_name.clone())));
951 if let Some(style) = field_schema.binding_style {
952 let child_path = layout
953 .schema_node_paths
954 .get(&field_schema.schema)
955 .cloned()
956 .unwrap_or_else(|| schema_node_fallback_path(field_schema.schema));
957 layout.by_path.insert(child_path, style);
958 }
959 }
960 for ext_name in schema_node.ext_types.keys() {
961 order.push(PathSegment::Extension(ext_name.clone()));
962 }
963 if !order.is_empty() {
964 layout.order_by_path.insert(node_path.clone(), order);
965 }
966 }
967
968 for ext_schema in schema_node.ext_types.values() {
969 if let Some(style) = ext_schema.binding_style {
970 let ext_path = layout
971 .schema_node_paths
972 .get(&ext_schema.schema)
973 .cloned()
974 .unwrap_or_else(|| schema_node_fallback_path(ext_schema.schema));
975 layout.by_path.insert(ext_path, style);
976 }
977 }
978 }
979
980 layout
981}
982
983fn parse_bigint(s: &str) -> Result<BigInt, ConversionError> {
984 s.parse()
985 .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid integer: {}", s)))
986}
987
988fn parse_f64(s: &str) -> Result<f64, ConversionError> {
989 s.parse()
990 .map_err(|_| ConversionError::InvalidRangeString(format!("Invalid float: {}", s)))
991}
992
993pub fn document_to_schema_with_layout(
1027 doc: &EureDocument,
1028) -> Result<(SchemaDocument, LayoutStrategies, SchemaSourceMap), ConversionError> {
1029 let (schema, source_map) = Converter::new(doc).convert()?;
1030 let source_node_paths = collect_document_node_paths(doc);
1031 let layout = build_layout_strategies(&schema, &source_map, &source_node_paths);
1032 Ok((schema, layout, source_map))
1033}
1034
1035pub fn document_to_schema(
1036 doc: &EureDocument,
1037) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
1038 let (schema, _layout, source_map) = document_to_schema_with_layout(doc)?;
1039 Ok((schema, source_map))
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045 use crate::identifiers::{EXT_TYPE, OPTIONAL};
1046 use eure_document::document::node::NodeMap;
1047 use eure_document::eure;
1048 use eure_document::text::Text;
1049 use eure_document::value::PrimitiveValue;
1050
1051 fn create_schema_with_field_ext_type(ext_type_content: NodeValue) -> EureDocument {
1053 let mut doc = EureDocument::new();
1054 let root_id = doc.get_root_id();
1055
1056 let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1058 Text::inline_implicit("text"),
1059 )));
1060
1061 let ext_type_id = doc.create_node(ext_type_content);
1063 doc.node_mut(field_value_id)
1064 .extensions
1065 .insert(EXT_TYPE.clone(), ext_type_id);
1066
1067 let mut root_map = NodeMap::default();
1069 root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
1070 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1071
1072 doc
1073 }
1074
1075 #[test]
1076 fn extract_ext_types_not_map() {
1077 let doc = create_schema_with_field_ext_type(NodeValue::Primitive(PrimitiveValue::Integer(
1080 1.into(),
1081 )));
1082
1083 let err = document_to_schema(&doc).unwrap_err();
1084 use eure_document::parse::ParseErrorKind;
1085 use eure_document::value::ValueKind;
1086 assert_eq!(
1087 err,
1088 ConversionError::ParseError(ParseError {
1089 node_id: NodeId(2),
1090 kind: ParseErrorKind::TypeMismatch {
1091 expected: ValueKind::Map,
1092 actual: ValueKind::Integer,
1093 }
1094 })
1095 );
1096 }
1097
1098 #[test]
1099 fn extract_ext_types_invalid_key() {
1100 let mut doc = EureDocument::new();
1103 let root_id = doc.get_root_id();
1104
1105 let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1107 Text::inline_implicit("text"),
1108 )));
1109
1110 let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1113 Text::inline_implicit("text"),
1114 )));
1115 let mut ext_type_map = NodeMap::default();
1116 ext_type_map.insert(ObjectKey::Number(0.into()), ext_type_value_id);
1117
1118 let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
1119 doc.node_mut(field_value_id)
1120 .extensions
1121 .insert(EXT_TYPE.clone(), ext_type_id);
1122
1123 let mut root_map = NodeMap::default();
1125 root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
1126 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1127
1128 let err = document_to_schema(&doc).unwrap_err();
1129 use eure_document::parse::ParseErrorKind;
1130 assert_eq!(
1131 err,
1132 ConversionError::ParseError(ParseError {
1133 node_id: ext_type_value_id,
1135 kind: ParseErrorKind::InvalidKeyType(ObjectKey::Number(0.into()))
1136 })
1137 );
1138 }
1139
1140 #[test]
1141 fn extract_ext_types_invalid_optional() {
1142 let mut doc = EureDocument::new();
1145 let root_id = doc.get_root_id();
1146
1147 let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1149 Text::inline_implicit("text"),
1150 )));
1151
1152 let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1154 Text::inline_implicit("text"),
1155 )));
1156 let optional_node_id =
1157 doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
1158 doc.node_mut(ext_type_value_id)
1159 .extensions
1160 .insert(OPTIONAL.clone(), optional_node_id);
1161
1162 let mut ext_type_map = NodeMap::default();
1164 ext_type_map.insert(ObjectKey::String("desc".to_string()), ext_type_value_id);
1165
1166 let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
1167 doc.node_mut(field_value_id)
1168 .extensions
1169 .insert(EXT_TYPE.clone(), ext_type_id);
1170
1171 let mut root_map = NodeMap::default();
1173 root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
1174 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1175
1176 let err = document_to_schema(&doc).unwrap_err();
1177 use eure_document::parse::ParseErrorKind;
1178 use eure_document::value::ValueKind;
1179 assert_eq!(
1180 err,
1181 ConversionError::ParseError(ParseError {
1182 node_id: NodeId(3),
1183 kind: ParseErrorKind::TypeMismatch {
1184 expected: ValueKind::Bool,
1185 actual: ValueKind::Integer,
1186 }
1187 })
1188 );
1189 }
1190
1191 #[test]
1192 fn literal_variant_with_inline_code() {
1193 let mut doc = EureDocument::new();
1197 let root_id = doc.get_root_id();
1198
1199 let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1201 Text::plaintext("literal"),
1202 )));
1203
1204 doc.node_mut(root_id).content =
1207 NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("any")));
1208
1209 doc.node_mut(root_id)
1211 .extensions
1212 .insert("variant".parse().unwrap(), variant_value_id);
1213
1214 let (schema, _source_map) =
1215 document_to_schema(&doc).expect("Schema conversion should succeed");
1216
1217 let root_content = &schema.node(schema.root).content;
1219 match root_content {
1220 SchemaNodeContent::Literal(doc) => {
1221 match &doc.root().content {
1223 NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1224 assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
1225 }
1226 _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
1227 }
1228 }
1229 SchemaNodeContent::Any => {
1230 panic!("BUG: Got Any instead of Literal - $variant extension not detected!");
1231 }
1232 other => panic!("Expected Literal, got {:?}", other),
1233 }
1234 }
1235
1236 #[test]
1237 fn literal_variant_parsed_from_eure() {
1238 let doc = eure!({
1239 = @code("any")
1240 %variant = "literal"
1241 });
1242
1243 let (schema, _source_map) =
1244 document_to_schema(&doc).expect("Schema conversion should succeed");
1245
1246 let root_content = &schema.node(schema.root).content;
1247 match root_content {
1248 SchemaNodeContent::Literal(doc) => match &doc.root().content {
1249 NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1250 assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
1251 }
1252 _ => panic!("Expected Literal with Text primitive, got {:?}", doc),
1253 },
1254 SchemaNodeContent::Any => {
1255 panic!(
1256 "BUG: Got Any instead of Literal - $variant extension not respected for primitive"
1257 );
1258 }
1259 other => panic!("Expected Literal, got {:?}", other),
1260 }
1261 }
1262
1263 #[test]
1264 fn literal_variant_rejects_partial_map() {
1265 let mut doc = EureDocument::new();
1266 let root_id = doc.get_root_id();
1267
1268 let value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
1269 let mut map = eure_document::map::PartialNodeMap::new();
1270 map.push(
1271 eure_document::value::PartialObjectKey::Hole(Some("x".parse().unwrap())),
1272 value_id,
1273 );
1274 doc.node_mut(root_id).content = NodeValue::PartialMap(map);
1275
1276 let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1277 Text::plaintext("literal"),
1278 )));
1279 doc.node_mut(root_id)
1280 .extensions
1281 .insert("variant".parse().unwrap(), variant_value_id);
1282
1283 assert_eq!(
1284 Converter::new(&doc).node_to_document(root_id).unwrap_err(),
1285 ConversionError::UnsupportedLiteralValue {
1286 node_id: root_id,
1287 kind: ValueKind::PartialMap,
1288 }
1289 );
1290 }
1291
1292 #[test]
1293 fn union_with_literal_any_variant() {
1294 let mut doc = EureDocument::new();
1299 let root_id = doc.get_root_id();
1300
1301 let any_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1304 Text::inline_implicit("any"),
1305 )));
1306 let literal_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1308 Text::plaintext("literal"),
1309 )));
1310 doc.node_mut(any_variant_node)
1311 .extensions
1312 .insert("variant".parse().unwrap(), literal_ext);
1313
1314 let literal_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1316 Text::inline_implicit("any"),
1317 )));
1318
1319 let mut variants_map = NodeMap::default();
1321 variants_map.insert(ObjectKey::String("any".to_string()), any_variant_node);
1322 variants_map.insert(
1323 ObjectKey::String("literal".to_string()),
1324 literal_variant_node,
1325 );
1326 let variants_node = doc.create_node(NodeValue::Map(variants_map));
1327
1328 let union_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1330 Text::plaintext("union"),
1331 )));
1332 let untagged_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1333 Text::plaintext("untagged"),
1334 )));
1335 let mut interop_map = NodeMap::default();
1336 interop_map.insert(ObjectKey::String("variant-repr".to_string()), untagged_ext);
1337 let interop_ext = doc.create_node(NodeValue::Map(interop_map));
1338
1339 let mut root_map = NodeMap::default();
1341 root_map.insert(ObjectKey::String("variants".to_string()), variants_node);
1342
1343 doc.node_mut(root_id).content = NodeValue::Map(root_map);
1344 doc.node_mut(root_id)
1345 .extensions
1346 .insert("variant".parse().unwrap(), union_ext);
1347 doc.node_mut(root_id)
1348 .extensions
1349 .insert("interop".parse().unwrap(), interop_ext);
1350
1351 let (schema, _source_map) =
1352 document_to_schema(&doc).expect("Schema conversion should succeed");
1353
1354 let root_content = &schema.node(schema.root).content;
1356 match root_content {
1357 SchemaNodeContent::Union(union_schema) => {
1358 let any_variant_id = union_schema
1360 .variants
1361 .get("any")
1362 .expect("'any' variant missing");
1363 let any_content = &schema.node(*any_variant_id).content;
1364 match any_content {
1365 SchemaNodeContent::Literal(doc) => match &doc.root().content {
1366 NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1367 assert_eq!(
1368 t.as_str(),
1369 "any",
1370 "'any' variant should be Literal(\"any\")"
1371 );
1372 }
1373 _ => panic!("'any' variant: expected Text, got {:?}", doc),
1374 },
1375 SchemaNodeContent::Any => {
1376 panic!(
1377 "BUG: 'any' variant is Any instead of Literal(\"any\") - $variant extension not detected!"
1378 );
1379 }
1380 other => panic!("'any' variant: expected Literal, got {:?}", other),
1381 }
1382
1383 let literal_variant_id = union_schema
1385 .variants
1386 .get("literal")
1387 .expect("'literal' variant missing");
1388 let literal_content = &schema.node(*literal_variant_id).content;
1389 match literal_content {
1390 SchemaNodeContent::Any => {
1391 }
1393 other => panic!("'literal' variant: expected Any, got {:?}", other),
1394 }
1395 }
1396 other => panic!("Expected Union, got {:?}", other),
1397 }
1398 }
1399
1400 #[test]
1401 fn extracts_layout_style_rules_from_binding_style_extensions() {
1402 let mut doc = EureDocument::new();
1403 let root_id = doc.get_root_id();
1404 doc.node_mut(root_id).content = NodeValue::empty_map();
1405
1406 let item_id = doc
1407 .add_map_child(ObjectKey::String("item".to_string()), root_id)
1408 .expect("insert item")
1409 .node_id;
1410 doc.node_mut(item_id).content =
1411 NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("integer")));
1412
1413 let style_id = doc
1414 .add_extension("binding-style".parse().unwrap(), item_id)
1415 .expect("insert binding-style")
1416 .node_id;
1417 doc.node_mut(style_id).content =
1418 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("section-binding")));
1419
1420 let (_schema, layout, _source_map) =
1421 document_to_schema_with_layout(&doc).expect("conversion succeeds");
1422
1423 let expected_path = EurePath(vec![PathSegment::Value(ObjectKey::String(
1424 "item".to_string(),
1425 ))]);
1426 let style = layout.by_path.get(&expected_path).expect("style for item");
1427 assert_eq!(*style, eure_document::layout::LayoutStyle::SectionBinding);
1428 }
1429
1430 #[test]
1431 fn preserves_record_property_order_in_layout_rules() {
1432 let doc = eure!({
1433 b = @code("integer")
1434 a = @code("integer")
1435 });
1436
1437 let (_schema, layout, _source_map) =
1438 document_to_schema_with_layout(&doc).expect("conversion succeeds");
1439
1440 let expected = vec![
1441 PathSegment::Value(ObjectKey::String("b".to_string())),
1442 PathSegment::Value(ObjectKey::String("a".to_string())),
1443 ];
1444 let root_order = layout
1445 .order_by_path
1446 .get(&EurePath::root())
1447 .expect("root order rule");
1448 assert_eq!(*root_order, expected);
1449 }
1450
1451 #[test]
1452 fn preserves_type_codegen_on_non_record_non_union_type_nodes() {
1453 let mut doc = EureDocument::new();
1454 let root_id = doc.get_root_id();
1455
1456 let type_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1457 Text::inline_implicit("text"),
1458 )));
1459 let type_name_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
1460 Text::plaintext("BadTypeName"),
1461 )));
1462
1463 let mut codegen_map = NodeMap::default();
1464 codegen_map.insert(ObjectKey::String("type".to_string()), type_name_node);
1465 let codegen_node = doc.create_node(NodeValue::Map(codegen_map));
1466 doc.node_mut(type_node)
1467 .extensions
1468 .insert("codegen".parse().unwrap(), codegen_node);
1469
1470 let mut types_map = NodeMap::default();
1471 types_map.insert(ObjectKey::String("bad".to_string()), type_node);
1472 let types_node = doc.create_node(NodeValue::Map(types_map));
1473 doc.node_mut(root_id)
1474 .extensions
1475 .insert("types".parse().unwrap(), types_node);
1476 doc.node_mut(root_id).content =
1477 NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("text")));
1478
1479 let (schema, _source_map) = document_to_schema(&doc)
1480 .expect("type-level $codegen on non-union type nodes should be preserved");
1481
1482 let bad_ident: Identifier = "bad".parse().unwrap();
1483 let bad_type_id = schema.types.get(&bad_ident).expect("type `bad`");
1484 let type_node = schema.node(*bad_type_id);
1485 let TypeCodegen::Record(record) = &type_node.type_codegen else {
1486 panic!("expected non-union type codegen to use Record variant");
1487 };
1488 assert_eq!(record.type_name.as_deref(), Some("BadTypeName"));
1489 }
1490
1491 #[test]
1492 fn rejects_non_productive_reference_cycles() {
1493 let doc = eure!({
1494 %types.a = @code("$types.b")
1495 %types.b = @code("$types.a")
1496 data = @code("$types.a")
1497 });
1498
1499 let err = document_to_schema(&doc).expect_err("cycle must be rejected");
1500 match err {
1501 ConversionError::NonProductiveReferenceCycle(path) => {
1502 assert!(path.contains("$types.a"));
1503 assert!(path.contains("$types.b"));
1504 }
1505 other => panic!("expected NonProductiveReferenceCycle, got {:?}", other),
1506 }
1507 }
1508}