use crate::parse::{
ParsedArraySchema, ParsedExtTypeSchema, ParsedFloatSchema, ParsedIntegerSchema,
ParsedMapSchema, ParsedRecordSchema, ParsedSchemaMetadata, ParsedSchemaNode,
ParsedSchemaNodeContent, ParsedTupleSchema, ParsedUnionSchema, ParsedUnknownFieldsPolicy,
};
use crate::type_path_trace::LayoutStrategies;
use crate::{
ArraySchema, Bound, CodegenDefaults, ExtTypeSchema, FloatPrecision, FloatSchema, IntegerSchema,
MapSchema, RecordCodegen, RecordFieldSchema, RecordSchema, RootCodegen, SchemaDocument,
SchemaMetadata, SchemaNodeContent, SchemaNodeId, TupleSchema, TypeCodegen, UnionCodegen,
UnionSchema, UnknownFieldsPolicy,
};
use eure_document::document::node::{Node, NodeValue};
use eure_document::document::{EureDocument, InsertErrorKind, NodeId};
use eure_document::identifier::Identifier;
use eure_document::parse::ParseError;
use eure_document::path::{ArrayIndexKind, EurePath, PathSegment};
use eure_document::value::{ObjectKey, ValueKind};
use indexmap::IndexMap;
use num_bigint::BigInt;
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq)]
pub enum ConversionError {
#[error("Invalid type name: {0}")]
InvalidTypeName(ObjectKey),
#[error("unsupported literal value at node {node_id:?}: {kind}")]
UnsupportedLiteralValue { node_id: NodeId, kind: ValueKind },
#[error("document insert error while copying literal value: {0}")]
DocumentInsert(#[from] InsertErrorKind),
#[error("Invalid extension value: {extension} at path {path}")]
InvalidExtensionValue { extension: String, path: String },
#[error("Invalid range string: {0}")]
InvalidRangeString(String),
#[error("Invalid precision: {0} (expected \"f32\" or \"f64\")")]
InvalidPrecision(String),
#[error("Undefined type reference: {0}")]
UndefinedTypeReference(String),
#[error("non-productive reference cycle detected: {0}")]
NonProductiveReferenceCycle(String),
#[error(
"invalid `$codegen` extension at node {node_id:?}: supported only for record/union, got {schema_kind}"
)]
InvalidTypeCodegenTarget {
node_id: NodeId,
schema_kind: String,
},
#[error("Parse error: {0}")]
ParseError(#[from] ParseError),
}
pub type SchemaSourceMap = IndexMap<SchemaNodeId, NodeId>;
struct Converter<'a> {
doc: &'a EureDocument,
schema: SchemaDocument,
source_map: SchemaSourceMap,
}
impl<'a> Converter<'a> {
fn new(doc: &'a EureDocument) -> Self {
Self {
doc,
schema: SchemaDocument::new(),
source_map: IndexMap::new(),
}
}
fn convert(mut self) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
let root_id = self.doc.get_root_id();
let root_node = self.doc.node(root_id);
self.convert_types(root_node)?;
self.convert_root_codegen(root_node)?;
self.schema.root = self.convert_node_allow_non_type_codegen(root_id)?;
self.validate_type_references()?;
self.validate_non_productive_reference_cycles()?;
Ok((self.schema, self.source_map))
}
fn convert_root_codegen(&mut self, node: &Node) -> Result<(), ConversionError> {
let codegen_ident: Identifier = "codegen".parse().unwrap();
let codegen_defaults_ident: Identifier = "codegen-defaults".parse().unwrap();
if let Some(node_id) = node.extensions.get(&codegen_ident) {
let rec = self.doc.parse_record(*node_id)?;
self.schema.root_codegen = RootCodegen {
type_name: rec.parse_field_optional::<String>("type")?,
};
}
if let Some(node_id) = node.extensions.get(&codegen_defaults_ident) {
self.schema.codegen_defaults = self.doc.parse::<CodegenDefaults>(*node_id)?;
}
Ok(())
}
fn convert_types(&mut self, node: &Node) -> Result<(), ConversionError> {
let types_ident: Identifier = "types".parse().unwrap();
if let Some(types_node_id) = node.extensions.get(&types_ident) {
let types_node = self.doc.node(*types_node_id);
if let NodeValue::Map(map) = &types_node.content {
for (key, &node_id) in map.iter() {
if let ObjectKey::String(name) = key {
let type_name: Identifier = name
.parse()
.map_err(|_| ConversionError::InvalidTypeName(key.clone()))?;
let schema_id = self.convert_node(node_id)?;
self.schema.types.insert(type_name, schema_id);
} else {
return Err(ConversionError::InvalidTypeName(key.clone()));
}
}
} else {
return Err(ConversionError::InvalidExtensionValue {
extension: "types".to_string(),
path: "$types must be a map".to_string(),
});
}
}
Ok(())
}
fn validate_type_references(&self) -> Result<(), ConversionError> {
for node in &self.schema.nodes {
if let SchemaNodeContent::Reference(type_ref) = &node.content
&& type_ref.namespace.is_none()
&& !self.schema.types.contains_key(&type_ref.name)
{
return Err(ConversionError::UndefinedTypeReference(
type_ref.name.to_string(),
));
}
}
Ok(())
}
fn validate_non_productive_reference_cycles(&self) -> Result<(), ConversionError> {
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mark {
Visiting,
Done,
}
fn next_ref_target(schema: &SchemaDocument, node_id: SchemaNodeId) -> Option<SchemaNodeId> {
let node = schema.node(node_id);
let SchemaNodeContent::Reference(type_ref) = &node.content else {
return None;
};
if type_ref.namespace.is_some() {
return None;
}
schema.get_type(&type_ref.name)
}
fn display_node(schema: &SchemaDocument, id: SchemaNodeId) -> String {
if let Some((name, _)) = schema.types.iter().find(|(_, sid)| **sid == id) {
format!("$types.{}", name)
} else {
format!("node#{}", id.0)
}
}
fn visit(
schema: &SchemaDocument,
node_id: SchemaNodeId,
marks: &mut IndexMap<SchemaNodeId, Mark>,
stack: &mut Vec<SchemaNodeId>,
) -> Result<(), ConversionError> {
if matches!(marks.get(&node_id), Some(Mark::Done)) {
return Ok(());
}
if matches!(marks.get(&node_id), Some(Mark::Visiting)) {
let start = stack.iter().position(|sid| *sid == node_id).unwrap_or(0);
let mut cycle: Vec<String> = stack[start..]
.iter()
.map(|sid| display_node(schema, *sid))
.collect();
cycle.push(display_node(schema, node_id));
return Err(ConversionError::NonProductiveReferenceCycle(
cycle.join(" -> "),
));
}
marks.insert(node_id, Mark::Visiting);
stack.push(node_id);
if let Some(next_id) = next_ref_target(schema, node_id) {
visit(schema, next_id, marks, stack)?;
}
stack.pop();
marks.insert(node_id, Mark::Done);
Ok(())
}
let mut marks = IndexMap::new();
let mut stack = Vec::new();
for index in 0..self.schema.nodes.len() {
visit(&self.schema, SchemaNodeId(index), &mut marks, &mut stack)?;
}
Ok(())
}
fn convert_node(&mut self, node_id: NodeId) -> Result<SchemaNodeId, ConversionError> {
self.convert_node_inner(node_id, false)
}
fn convert_node_allow_non_type_codegen(
&mut self,
node_id: NodeId,
) -> Result<SchemaNodeId, ConversionError> {
self.convert_node_inner(node_id, true)
}
fn convert_node_inner(
&mut self,
node_id: NodeId,
allow_non_type_codegen: bool,
) -> Result<SchemaNodeId, ConversionError> {
let parsed: ParsedSchemaNode = self.doc.parse(node_id)?;
let ParsedSchemaNode {
content: parsed_content,
metadata: parsed_metadata,
ext_types: parsed_ext_types,
codegen: parsed_codegen,
} = parsed;
let content = self.convert_content(parsed_content)?;
let metadata = self.convert_metadata(parsed_metadata)?;
let ext_types = self.convert_ext_types(parsed_ext_types)?;
let type_codegen =
self.convert_type_codegen(parsed_codegen, &content, allow_non_type_codegen)?;
let schema_id = self.schema.create_node(content);
let schema_node = self.schema.node_mut(schema_id);
schema_node.metadata = metadata;
schema_node.ext_types = ext_types;
schema_node.type_codegen = type_codegen;
self.source_map.insert(schema_id, node_id);
Ok(schema_id)
}
fn convert_type_codegen(
&self,
codegen_node_id: Option<NodeId>,
content: &SchemaNodeContent,
allow_non_type_codegen: bool,
) -> Result<TypeCodegen, ConversionError> {
let Some(codegen_node_id) = codegen_node_id else {
return Ok(TypeCodegen::None);
};
if allow_non_type_codegen
&& !matches!(
content,
SchemaNodeContent::Record(_) | SchemaNodeContent::Union(_)
)
{
return Ok(TypeCodegen::None);
}
match content {
SchemaNodeContent::Union(_) => Ok(TypeCodegen::Union(
self.doc.parse::<UnionCodegen>(codegen_node_id)?,
)),
_ => Ok(TypeCodegen::Record(
self.doc.parse::<RecordCodegen>(codegen_node_id)?,
)),
}
}
fn convert_content(
&mut self,
content: ParsedSchemaNodeContent,
) -> Result<SchemaNodeContent, ConversionError> {
match content {
ParsedSchemaNodeContent::Any => Ok(SchemaNodeContent::Any),
ParsedSchemaNodeContent::Boolean => Ok(SchemaNodeContent::Boolean),
ParsedSchemaNodeContent::Null => Ok(SchemaNodeContent::Null),
ParsedSchemaNodeContent::Text(schema) => Ok(SchemaNodeContent::Text(schema)),
ParsedSchemaNodeContent::Reference(type_ref) => {
Ok(SchemaNodeContent::Reference(type_ref))
}
ParsedSchemaNodeContent::Integer(parsed) => Ok(SchemaNodeContent::Integer(
self.convert_integer_schema(parsed)?,
)),
ParsedSchemaNodeContent::Float(parsed) => {
Ok(SchemaNodeContent::Float(self.convert_float_schema(parsed)?))
}
ParsedSchemaNodeContent::Literal(node_id) => {
Ok(SchemaNodeContent::Literal(self.node_to_document(node_id)?))
}
ParsedSchemaNodeContent::Array(parsed) => {
Ok(SchemaNodeContent::Array(self.convert_array_schema(parsed)?))
}
ParsedSchemaNodeContent::Map(parsed) => {
Ok(SchemaNodeContent::Map(self.convert_map_schema(parsed)?))
}
ParsedSchemaNodeContent::Record(parsed) => Ok(SchemaNodeContent::Record(
self.convert_record_schema(parsed)?,
)),
ParsedSchemaNodeContent::Tuple(parsed) => {
Ok(SchemaNodeContent::Tuple(self.convert_tuple_schema(parsed)?))
}
ParsedSchemaNodeContent::Union(parsed) => {
Ok(SchemaNodeContent::Union(self.convert_union_schema(parsed)?))
}
}
}
fn convert_integer_schema(
&self,
parsed: ParsedIntegerSchema,
) -> Result<IntegerSchema, ConversionError> {
let (min, max) = if let Some(range_str) = &parsed.range {
parse_integer_range(range_str)?
} else {
(Bound::Unbounded, Bound::Unbounded)
};
Ok(IntegerSchema {
min,
max,
multiple_of: parsed.multiple_of,
})
}
fn convert_float_schema(
&self,
parsed: ParsedFloatSchema,
) -> Result<FloatSchema, ConversionError> {
let (min, max) = if let Some(range_str) = &parsed.range {
parse_float_range(range_str)?
} else {
(Bound::Unbounded, Bound::Unbounded)
};
let precision = match parsed.precision.as_deref() {
Some("f32") => FloatPrecision::F32,
Some("f64") | None => FloatPrecision::F64,
Some(other) => {
return Err(ConversionError::InvalidPrecision(other.to_string()));
}
};
Ok(FloatSchema {
min,
max,
multiple_of: parsed.multiple_of,
precision,
})
}
fn convert_array_schema(
&mut self,
parsed: ParsedArraySchema,
) -> Result<ArraySchema, ConversionError> {
let item = self.convert_node(parsed.item)?;
let contains = parsed
.contains
.map(|id| self.convert_node(id))
.transpose()?;
Ok(ArraySchema {
item,
min_length: parsed.min_length,
max_length: parsed.max_length,
unique: parsed.unique,
contains,
binding_style: parsed.binding_style,
})
}
fn convert_map_schema(
&mut self,
parsed: ParsedMapSchema,
) -> Result<MapSchema, ConversionError> {
let key = self.convert_node(parsed.key)?;
let value = self.convert_node(parsed.value)?;
Ok(MapSchema {
key,
value,
min_size: parsed.min_size,
max_size: parsed.max_size,
})
}
fn convert_tuple_schema(
&mut self,
parsed: ParsedTupleSchema,
) -> Result<TupleSchema, ConversionError> {
let elements: Vec<SchemaNodeId> = parsed
.elements
.iter()
.map(|&id| self.convert_node(id))
.collect::<Result<_, _>>()?;
Ok(TupleSchema {
elements,
binding_style: parsed.binding_style,
})
}
fn convert_record_schema(
&mut self,
parsed: ParsedRecordSchema,
) -> Result<RecordSchema, ConversionError> {
let mut properties = IndexMap::new();
for (field_name, field_parsed) in parsed.properties {
let schema = self.convert_node_allow_non_type_codegen(field_parsed.schema)?;
properties.insert(
field_name,
RecordFieldSchema {
schema,
optional: field_parsed.optional,
binding_style: field_parsed.binding_style,
field_codegen: field_parsed.codegen.unwrap_or_default(),
},
);
}
let flatten = parsed
.flatten
.into_iter()
.map(|id| self.convert_node(id))
.collect::<Result<Vec<_>, _>>()?;
let unknown_fields = self.convert_unknown_fields_policy(parsed.unknown_fields)?;
Ok(RecordSchema {
properties,
flatten,
unknown_fields,
})
}
fn convert_union_schema(
&mut self,
parsed: ParsedUnionSchema,
) -> Result<UnionSchema, ConversionError> {
let ParsedUnionSchema {
variants: parsed_variants,
unambiguous,
interop,
deny_untagged,
} = parsed;
let mut variants = IndexMap::new();
for (variant_name, variant_node_id) in parsed_variants {
let schema = self.convert_node(variant_node_id)?;
variants.insert(variant_name, schema);
}
Ok(UnionSchema {
variants,
unambiguous,
interop,
deny_untagged,
})
}
fn convert_unknown_fields_policy(
&mut self,
parsed: ParsedUnknownFieldsPolicy,
) -> Result<UnknownFieldsPolicy, ConversionError> {
match parsed {
ParsedUnknownFieldsPolicy::Deny => Ok(UnknownFieldsPolicy::Deny),
ParsedUnknownFieldsPolicy::Allow => Ok(UnknownFieldsPolicy::Allow),
ParsedUnknownFieldsPolicy::Schema(node_id) => {
let schema = self.convert_node(node_id)?;
Ok(UnknownFieldsPolicy::Schema(schema))
}
}
}
fn convert_metadata(
&mut self,
parsed: ParsedSchemaMetadata,
) -> Result<SchemaMetadata, ConversionError> {
let default = parsed
.default
.map(|id| self.node_to_document(id))
.transpose()?;
let examples = parsed
.examples
.map(|ids| {
ids.into_iter()
.map(|id| self.node_to_document(id))
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
Ok(SchemaMetadata {
description: parsed.description,
deprecated: parsed.deprecated,
default,
examples,
})
}
fn convert_ext_types(
&mut self,
parsed: IndexMap<Identifier, ParsedExtTypeSchema>,
) -> Result<IndexMap<Identifier, ExtTypeSchema>, ConversionError> {
let mut result = IndexMap::new();
for (name, parsed_schema) in parsed {
let schema = self.convert_node(parsed_schema.schema)?;
result.insert(
name,
ExtTypeSchema {
schema,
optional: parsed_schema.optional,
binding_style: parsed_schema.binding_style,
},
);
}
Ok(result)
}
fn node_to_document(&self, node_id: NodeId) -> Result<EureDocument, ConversionError> {
let mut new_doc = EureDocument::new();
let root_id = new_doc.get_root_id();
self.copy_node_to(&mut new_doc, root_id, node_id)?;
Ok(new_doc)
}
fn copy_node_to(
&self,
dest: &mut EureDocument,
dest_node_id: NodeId,
src_node_id: NodeId,
) -> Result<(), ConversionError> {
let src_node = self.doc.node(src_node_id);
let children_to_copy: Vec<_> = match &src_node.content {
NodeValue::Primitive(prim) => {
dest.set_content(dest_node_id, NodeValue::Primitive(prim.clone()));
vec![]
}
NodeValue::Array(arr) => {
dest.set_content(dest_node_id, NodeValue::empty_array());
arr.to_vec()
}
NodeValue::Tuple(tup) => {
dest.set_content(dest_node_id, NodeValue::empty_tuple());
tup.to_vec()
}
NodeValue::Map(map) => {
dest.set_content(dest_node_id, NodeValue::empty_map());
map.iter()
.map(|(k, &v)| (k.clone(), v))
.collect::<Vec<_>>()
.into_iter()
.map(|(_, v)| v)
.collect()
}
NodeValue::PartialMap(_) => {
return Err(ConversionError::UnsupportedLiteralValue {
node_id: src_node_id,
kind: ValueKind::PartialMap,
});
}
NodeValue::Hole(_) => {
return Err(ConversionError::UnsupportedLiteralValue {
node_id: src_node_id,
kind: ValueKind::Hole,
});
}
};
let src_node = self.doc.node(src_node_id);
match &src_node.content {
NodeValue::Array(_) => {
for child_id in children_to_copy {
let new_child_id = dest.add_array_element(None, dest_node_id)?.node_id;
self.copy_node_to(dest, new_child_id, child_id)?;
}
}
NodeValue::Tuple(_) => {
for (index, child_id) in children_to_copy.into_iter().enumerate() {
let new_child_id = dest.add_tuple_element(index as u8, dest_node_id)?.node_id;
self.copy_node_to(dest, new_child_id, child_id)?;
}
}
NodeValue::Map(map) => {
for (key, &child_id) in map.iter() {
let new_child_id = dest.add_map_child(key.clone(), dest_node_id)?.node_id;
self.copy_node_to(dest, new_child_id, child_id)?;
}
}
_ => {}
}
Ok(())
}
}
fn parse_integer_range(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
let s = s.trim();
if s.starts_with('[') || s.starts_with('(') {
return parse_interval_integer(s);
}
if let Some(eq_pos) = s.find("..=") {
let left = &s[..eq_pos];
let right = &s[eq_pos + 3..];
let min = if left.is_empty() {
Bound::Unbounded
} else {
Bound::Inclusive(parse_bigint(left)?)
};
let max = if right.is_empty() {
Bound::Unbounded
} else {
Bound::Inclusive(parse_bigint(right)?)
};
Ok((min, max))
} else if let Some(dot_pos) = s.find("..") {
let left = &s[..dot_pos];
let right = &s[dot_pos + 2..];
let min = if left.is_empty() {
Bound::Unbounded
} else {
Bound::Inclusive(parse_bigint(left)?)
};
let max = if right.is_empty() {
Bound::Unbounded
} else {
Bound::Exclusive(parse_bigint(right)?)
};
Ok((min, max))
} else {
Err(ConversionError::InvalidRangeString(s.to_string()))
}
}
fn parse_interval_integer(s: &str) -> Result<(Bound<BigInt>, Bound<BigInt>), ConversionError> {
let left_inclusive = s.starts_with('[');
let right_inclusive = s.ends_with(']');
let inner = &s[1..s.len() - 1];
let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
if parts.len() != 2 {
return Err(ConversionError::InvalidRangeString(s.to_string()));
}
let min = if parts[0].is_empty() {
Bound::Unbounded
} else if left_inclusive {
Bound::Inclusive(parse_bigint(parts[0])?)
} else {
Bound::Exclusive(parse_bigint(parts[0])?)
};
let max = if parts[1].is_empty() {
Bound::Unbounded
} else if right_inclusive {
Bound::Inclusive(parse_bigint(parts[1])?)
} else {
Bound::Exclusive(parse_bigint(parts[1])?)
};
Ok((min, max))
}
fn parse_float_range(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
let s = s.trim();
if s.starts_with('[') || s.starts_with('(') {
return parse_interval_float(s);
}
if let Some(eq_pos) = s.find("..=") {
let left = &s[..eq_pos];
let right = &s[eq_pos + 3..];
let min = if left.is_empty() {
Bound::Unbounded
} else {
Bound::Inclusive(parse_f64(left)?)
};
let max = if right.is_empty() {
Bound::Unbounded
} else {
Bound::Inclusive(parse_f64(right)?)
};
Ok((min, max))
} else if let Some(dot_pos) = s.find("..") {
let left = &s[..dot_pos];
let right = &s[dot_pos + 2..];
let min = if left.is_empty() {
Bound::Unbounded
} else {
Bound::Inclusive(parse_f64(left)?)
};
let max = if right.is_empty() {
Bound::Unbounded
} else {
Bound::Exclusive(parse_f64(right)?)
};
Ok((min, max))
} else {
Err(ConversionError::InvalidRangeString(s.to_string()))
}
}
fn parse_interval_float(s: &str) -> Result<(Bound<f64>, Bound<f64>), ConversionError> {
let left_inclusive = s.starts_with('[');
let right_inclusive = s.ends_with(']');
let inner = &s[1..s.len() - 1];
let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
if parts.len() != 2 {
return Err(ConversionError::InvalidRangeString(s.to_string()));
}
let min = if parts[0].is_empty() {
Bound::Unbounded
} else if left_inclusive {
Bound::Inclusive(parse_f64(parts[0])?)
} else {
Bound::Exclusive(parse_f64(parts[0])?)
};
let max = if parts[1].is_empty() {
Bound::Unbounded
} else if right_inclusive {
Bound::Inclusive(parse_f64(parts[1])?)
} else {
Bound::Exclusive(parse_f64(parts[1])?)
};
Ok((min, max))
}
fn collect_document_node_paths(doc: &EureDocument) -> IndexMap<NodeId, EurePath> {
fn dfs(
doc: &EureDocument,
node_id: NodeId,
path: &mut Vec<PathSegment>,
out: &mut IndexMap<NodeId, EurePath>,
visited: &mut std::collections::HashSet<NodeId>,
) {
if !visited.insert(node_id) {
return;
}
out.insert(node_id, EurePath(path.clone()));
let node = doc.node(node_id);
for (ext, &child_id) in node.extensions.iter() {
path.push(PathSegment::Extension(ext.clone()));
dfs(doc, child_id, path, out, visited);
path.pop();
}
match &node.content {
NodeValue::Array(array) => {
for (index, &child_id) in array.iter().enumerate() {
path.push(PathSegment::ArrayIndex(ArrayIndexKind::Specific(index)));
dfs(doc, child_id, path, out, visited);
path.pop();
}
}
NodeValue::Tuple(tuple) => {
for (index, &child_id) in tuple.iter().enumerate() {
path.push(PathSegment::TupleIndex(index as u8));
dfs(doc, child_id, path, out, visited);
path.pop();
}
}
NodeValue::Map(map) => {
for (key, &child_id) in map.iter() {
path.push(PathSegment::Value(key.clone()));
dfs(doc, child_id, path, out, visited);
path.pop();
}
}
NodeValue::PartialMap(map) => {
for (key, &child_id) in map.iter() {
path.push(PathSegment::from_partial_object_key(key.clone()));
dfs(doc, child_id, path, out, visited);
path.pop();
}
}
NodeValue::Primitive(_) | NodeValue::Hole(_) => {}
}
}
let mut out = IndexMap::new();
let mut path = Vec::new();
let mut visited = std::collections::HashSet::new();
dfs(doc, doc.get_root_id(), &mut path, &mut out, &mut visited);
out
}
fn schema_node_fallback_path(schema_id: SchemaNodeId) -> EurePath {
EurePath(vec![PathSegment::Value(ObjectKey::String(format!(
"schema-node-{}",
schema_id.0
)))])
}
fn build_layout_strategies(
schema: &SchemaDocument,
source_map: &SchemaSourceMap,
source_node_paths: &IndexMap<NodeId, EurePath>,
) -> LayoutStrategies {
let mut layout = LayoutStrategies::default();
for (schema_id, source_node_id) in source_map {
if let Some(path) = source_node_paths.get(source_node_id) {
layout.schema_node_paths.insert(*schema_id, path.clone());
}
}
for schema_index in 0..schema.nodes.len() {
let schema_id = SchemaNodeId(schema_index);
let schema_node = schema.node(schema_id);
let node_path = layout
.schema_node_paths
.get(&schema_id)
.cloned()
.unwrap_or_else(|| schema_node_fallback_path(schema_id));
if let SchemaNodeContent::Array(array_schema) = &schema_node.content
&& let Some(style) = array_schema.binding_style
{
layout.by_path.insert(node_path.clone(), style);
}
if let SchemaNodeContent::Tuple(tuple_schema) = &schema_node.content
&& let Some(style) = tuple_schema.binding_style
{
layout.by_path.insert(node_path.clone(), style);
}
if let SchemaNodeContent::Record(record_schema) = &schema_node.content {
let mut order = Vec::new();
for (field_name, field_schema) in &record_schema.properties {
order.push(PathSegment::Value(ObjectKey::String(field_name.clone())));
if let Some(style) = field_schema.binding_style {
let child_path = layout
.schema_node_paths
.get(&field_schema.schema)
.cloned()
.unwrap_or_else(|| schema_node_fallback_path(field_schema.schema));
layout.by_path.insert(child_path, style);
}
}
for ext_name in schema_node.ext_types.keys() {
order.push(PathSegment::Extension(ext_name.clone()));
}
if !order.is_empty() {
layout.order_by_path.insert(node_path.clone(), order);
}
}
for ext_schema in schema_node.ext_types.values() {
if let Some(style) = ext_schema.binding_style {
let ext_path = layout
.schema_node_paths
.get(&ext_schema.schema)
.cloned()
.unwrap_or_else(|| schema_node_fallback_path(ext_schema.schema));
layout.by_path.insert(ext_path, style);
}
}
}
layout
}
fn parse_bigint(s: &str) -> Result<BigInt, ConversionError> {
s.parse()
.map_err(|_| ConversionError::InvalidRangeString(format!("Invalid integer: {}", s)))
}
fn parse_f64(s: &str) -> Result<f64, ConversionError> {
s.parse()
.map_err(|_| ConversionError::InvalidRangeString(format!("Invalid float: {}", s)))
}
pub fn document_to_schema_with_layout(
doc: &EureDocument,
) -> Result<(SchemaDocument, LayoutStrategies, SchemaSourceMap), ConversionError> {
let (schema, source_map) = Converter::new(doc).convert()?;
let source_node_paths = collect_document_node_paths(doc);
let layout = build_layout_strategies(&schema, &source_map, &source_node_paths);
Ok((schema, layout, source_map))
}
pub fn document_to_schema(
doc: &EureDocument,
) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
let (schema, _layout, source_map) = document_to_schema_with_layout(doc)?;
Ok((schema, source_map))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifiers::{EXT_TYPE, OPTIONAL};
use eure_document::document::node::NodeMap;
use eure_document::eure;
use eure_document::text::Text;
use eure_document::value::PrimitiveValue;
fn create_schema_with_field_ext_type(ext_type_content: NodeValue) -> EureDocument {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("text"),
)));
let ext_type_id = doc.create_node(ext_type_content);
doc.node_mut(field_value_id)
.extensions
.insert(EXT_TYPE.clone(), ext_type_id);
let mut root_map = NodeMap::default();
root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
doc.node_mut(root_id).content = NodeValue::Map(root_map);
doc
}
#[test]
fn extract_ext_types_not_map() {
let doc = create_schema_with_field_ext_type(NodeValue::Primitive(PrimitiveValue::Integer(
1.into(),
)));
let err = document_to_schema(&doc).unwrap_err();
use eure_document::parse::ParseErrorKind;
use eure_document::value::ValueKind;
assert_eq!(
err,
ConversionError::ParseError(ParseError {
node_id: NodeId(2),
kind: ParseErrorKind::TypeMismatch {
expected: ValueKind::Map,
actual: ValueKind::Integer,
}
})
);
}
#[test]
fn extract_ext_types_invalid_key() {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("text"),
)));
let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("text"),
)));
let mut ext_type_map = NodeMap::default();
ext_type_map.insert(ObjectKey::Number(0.into()), ext_type_value_id);
let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
doc.node_mut(field_value_id)
.extensions
.insert(EXT_TYPE.clone(), ext_type_id);
let mut root_map = NodeMap::default();
root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
doc.node_mut(root_id).content = NodeValue::Map(root_map);
let err = document_to_schema(&doc).unwrap_err();
use eure_document::parse::ParseErrorKind;
assert_eq!(
err,
ConversionError::ParseError(ParseError {
node_id: ext_type_value_id,
kind: ParseErrorKind::InvalidKeyType(ObjectKey::Number(0.into()))
})
);
}
#[test]
fn extract_ext_types_invalid_optional() {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
let field_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("text"),
)));
let ext_type_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("text"),
)));
let optional_node_id =
doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
doc.node_mut(ext_type_value_id)
.extensions
.insert(OPTIONAL.clone(), optional_node_id);
let mut ext_type_map = NodeMap::default();
ext_type_map.insert(ObjectKey::String("desc".to_string()), ext_type_value_id);
let ext_type_id = doc.create_node(NodeValue::Map(ext_type_map));
doc.node_mut(field_value_id)
.extensions
.insert(EXT_TYPE.clone(), ext_type_id);
let mut root_map = NodeMap::default();
root_map.insert(ObjectKey::String("name".to_string()), field_value_id);
doc.node_mut(root_id).content = NodeValue::Map(root_map);
let err = document_to_schema(&doc).unwrap_err();
use eure_document::parse::ParseErrorKind;
use eure_document::value::ValueKind;
assert_eq!(
err,
ConversionError::ParseError(ParseError {
node_id: NodeId(3),
kind: ParseErrorKind::TypeMismatch {
expected: ValueKind::Bool,
actual: ValueKind::Integer,
}
})
);
}
#[test]
fn literal_variant_with_inline_code() {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::plaintext("literal"),
)));
doc.node_mut(root_id).content =
NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("any")));
doc.node_mut(root_id)
.extensions
.insert("variant".parse().unwrap(), variant_value_id);
let (schema, _source_map) =
document_to_schema(&doc).expect("Schema conversion should succeed");
let root_content = &schema.node(schema.root).content;
match root_content {
SchemaNodeContent::Literal(doc) => {
match &doc.root().content {
NodeValue::Primitive(PrimitiveValue::Text(t)) => {
assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
}
_ => panic!("Expected Literal with Text primitive, got {:?}", doc),
}
}
SchemaNodeContent::Any => {
panic!("BUG: Got Any instead of Literal - $variant extension not detected!");
}
other => panic!("Expected Literal, got {:?}", other),
}
}
#[test]
fn literal_variant_parsed_from_eure() {
let doc = eure!({
= @code("any")
%variant = "literal"
});
let (schema, _source_map) =
document_to_schema(&doc).expect("Schema conversion should succeed");
let root_content = &schema.node(schema.root).content;
match root_content {
SchemaNodeContent::Literal(doc) => match &doc.root().content {
NodeValue::Primitive(PrimitiveValue::Text(t)) => {
assert_eq!(t.as_str(), "any", "Literal should contain 'any'");
}
_ => panic!("Expected Literal with Text primitive, got {:?}", doc),
},
SchemaNodeContent::Any => {
panic!(
"BUG: Got Any instead of Literal - $variant extension not respected for primitive"
);
}
other => panic!("Expected Literal, got {:?}", other),
}
}
#[test]
fn literal_variant_rejects_partial_map() {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
let value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(1.into())));
let mut map = eure_document::map::PartialNodeMap::new();
map.push(
eure_document::value::PartialObjectKey::Hole(Some("x".parse().unwrap())),
value_id,
);
doc.node_mut(root_id).content = NodeValue::PartialMap(map);
let variant_value_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::plaintext("literal"),
)));
doc.node_mut(root_id)
.extensions
.insert("variant".parse().unwrap(), variant_value_id);
assert_eq!(
Converter::new(&doc).node_to_document(root_id).unwrap_err(),
ConversionError::UnsupportedLiteralValue {
node_id: root_id,
kind: ValueKind::PartialMap,
}
);
}
#[test]
fn union_with_literal_any_variant() {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
let any_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("any"),
)));
let literal_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::plaintext("literal"),
)));
doc.node_mut(any_variant_node)
.extensions
.insert("variant".parse().unwrap(), literal_ext);
let literal_variant_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("any"),
)));
let mut variants_map = NodeMap::default();
variants_map.insert(ObjectKey::String("any".to_string()), any_variant_node);
variants_map.insert(
ObjectKey::String("literal".to_string()),
literal_variant_node,
);
let variants_node = doc.create_node(NodeValue::Map(variants_map));
let union_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::plaintext("union"),
)));
let untagged_ext = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::plaintext("untagged"),
)));
let mut interop_map = NodeMap::default();
interop_map.insert(ObjectKey::String("variant-repr".to_string()), untagged_ext);
let interop_ext = doc.create_node(NodeValue::Map(interop_map));
let mut root_map = NodeMap::default();
root_map.insert(ObjectKey::String("variants".to_string()), variants_node);
doc.node_mut(root_id).content = NodeValue::Map(root_map);
doc.node_mut(root_id)
.extensions
.insert("variant".parse().unwrap(), union_ext);
doc.node_mut(root_id)
.extensions
.insert("interop".parse().unwrap(), interop_ext);
let (schema, _source_map) =
document_to_schema(&doc).expect("Schema conversion should succeed");
let root_content = &schema.node(schema.root).content;
match root_content {
SchemaNodeContent::Union(union_schema) => {
let any_variant_id = union_schema
.variants
.get("any")
.expect("'any' variant missing");
let any_content = &schema.node(*any_variant_id).content;
match any_content {
SchemaNodeContent::Literal(doc) => match &doc.root().content {
NodeValue::Primitive(PrimitiveValue::Text(t)) => {
assert_eq!(
t.as_str(),
"any",
"'any' variant should be Literal(\"any\")"
);
}
_ => panic!("'any' variant: expected Text, got {:?}", doc),
},
SchemaNodeContent::Any => {
panic!(
"BUG: 'any' variant is Any instead of Literal(\"any\") - $variant extension not detected!"
);
}
other => panic!("'any' variant: expected Literal, got {:?}", other),
}
let literal_variant_id = union_schema
.variants
.get("literal")
.expect("'literal' variant missing");
let literal_content = &schema.node(*literal_variant_id).content;
match literal_content {
SchemaNodeContent::Any => {
}
other => panic!("'literal' variant: expected Any, got {:?}", other),
}
}
other => panic!("Expected Union, got {:?}", other),
}
}
#[test]
fn extracts_layout_style_rules_from_binding_style_extensions() {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
doc.node_mut(root_id).content = NodeValue::empty_map();
let item_id = doc
.add_map_child(ObjectKey::String("item".to_string()), root_id)
.expect("insert item")
.node_id;
doc.node_mut(item_id).content =
NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("integer")));
let style_id = doc
.add_extension("binding-style".parse().unwrap(), item_id)
.expect("insert binding-style")
.node_id;
doc.node_mut(style_id).content =
NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("binding-block")));
let (_schema, layout, _source_map) =
document_to_schema_with_layout(&doc).expect("conversion succeeds");
let expected_path = EurePath(vec![PathSegment::Value(ObjectKey::String(
"item".to_string(),
))]);
let style = layout.by_path.get(&expected_path).expect("style for item");
assert_eq!(*style, crate::BindingStyle::BindingBlock);
}
#[test]
fn preserves_record_property_order_in_layout_rules() {
let doc = eure!({
b = @code("integer")
a = @code("integer")
});
let (_schema, layout, _source_map) =
document_to_schema_with_layout(&doc).expect("conversion succeeds");
let expected = vec![
PathSegment::Value(ObjectKey::String("b".to_string())),
PathSegment::Value(ObjectKey::String("a".to_string())),
];
let root_order = layout
.order_by_path
.get(&EurePath::root())
.expect("root order rule");
assert_eq!(*root_order, expected);
}
#[test]
fn preserves_type_codegen_on_non_record_non_union_type_nodes() {
let mut doc = EureDocument::new();
let root_id = doc.get_root_id();
let type_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::inline_implicit("text"),
)));
let type_name_node = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(
Text::plaintext("BadTypeName"),
)));
let mut codegen_map = NodeMap::default();
codegen_map.insert(ObjectKey::String("type".to_string()), type_name_node);
let codegen_node = doc.create_node(NodeValue::Map(codegen_map));
doc.node_mut(type_node)
.extensions
.insert("codegen".parse().unwrap(), codegen_node);
let mut types_map = NodeMap::default();
types_map.insert(ObjectKey::String("bad".to_string()), type_node);
let types_node = doc.create_node(NodeValue::Map(types_map));
doc.node_mut(root_id)
.extensions
.insert("types".parse().unwrap(), types_node);
doc.node_mut(root_id).content =
NodeValue::Primitive(PrimitiveValue::Text(Text::inline_implicit("text")));
let (schema, _source_map) = document_to_schema(&doc)
.expect("type-level $codegen on non-union type nodes should be preserved");
let bad_ident: Identifier = "bad".parse().unwrap();
let bad_type_id = schema.types.get(&bad_ident).expect("type `bad`");
let type_node = schema.node(*bad_type_id);
let TypeCodegen::Record(record) = &type_node.type_codegen else {
panic!("expected non-union type codegen to use Record variant");
};
assert_eq!(record.type_name.as_deref(), Some("BadTypeName"));
}
#[test]
fn rejects_non_productive_reference_cycles() {
let doc = eure!({
%types.a = @code("$types.b")
%types.b = @code("$types.a")
data = @code("$types.a")
});
let err = document_to_schema(&doc).expect_err("cycle must be rejected");
match err {
ConversionError::NonProductiveReferenceCycle(path) => {
assert!(path.contains("$types.a"));
assert!(path.contains("$types.b"));
}
other => panic!("expected NonProductiveReferenceCycle, got {:?}", other),
}
}
}