use crate::parse::{
ParsedArraySchema, ParsedExports, ParsedExtTypeSchema, ParsedFloatSchema, ParsedImports,
ParsedIntegerSchema, ParsedMapSchema, ParsedRecordSchema, ParsedSchemaMetadata,
ParsedSchemaNode, ParsedSchemaNodeContent, ParsedTupleSchema, ParsedUnionSchema,
ParsedUnknownFieldsPolicy, parse_root_exports, parse_root_imports,
};
use crate::resolver::{LoadedSchemaSet, ResolvedSchemaUri, ResolverError};
use crate::type_path_trace::LayoutStrategies;
use crate::{
ArraySchema, Bound, CodegenDefaults, ExtTypeSchema, FloatPrecision, FloatSchema, IntegerSchema,
MapSchema, RecordCodegen, RecordFieldSchema, RecordSchema, RootCodegen, SchemaDocument,
SchemaImport, SchemaMetadata, SchemaNodeContent, SchemaNodeId, TupleSchema, TypeCodegen,
TypeReference, 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, IndexSet};
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("failed to resolve schema import `{alias}` -> \"{raw_path}\": {source}")]
ImportResolverFailed {
alias: String,
raw_path: String,
#[source]
source: ResolverError,
},
#[error("schema import cycle: {}", format_cycle(.cycle, .attempted))]
ImportCycle {
cycle: Vec<ResolvedSchemaUri>,
attempted: ResolvedSchemaUri,
},
#[error(
"type reference uses unknown import namespace `{namespace}` (in `$types.{namespace}.{name}`)"
)]
UnknownImportNamespace { namespace: String, name: String },
#[error("type `{name}` is not exported by `{namespace}` (declared in $import)")]
TypeNotExported { namespace: String, name: String },
#[error("$export lists `{name}`, but no such locally-declared type exists")]
ExportedNameNotDeclared { name: String },
#[error("schema import `{alias}` -> \"{raw_path}\" was not loaded for {base}")]
ImportNotLoaded {
base: ResolvedSchemaUri,
alias: String,
raw_path: String,
},
#[error("Parse error: {0}")]
ParseError(#[from] ParseError),
}
fn format_cycle(cycle: &[ResolvedSchemaUri], attempted: &ResolvedSchemaUri) -> String {
let mut parts: Vec<String> = cycle.iter().map(|u| u.to_string()).collect();
parts.push(attempted.to_string());
parts.join(" -> ")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchemaSource {
pub uri: ResolvedSchemaUri,
pub node_id: NodeId,
}
pub type SchemaSourceMap = IndexMap<SchemaNodeId, SchemaSource>;
struct Converter<'a> {
doc: &'a EureDocument,
loaded: &'a LoadedSchemaSet,
schema: SchemaDocument,
source_map: SchemaSourceMap,
base_uri: ResolvedSchemaUri,
}
impl<'a> Converter<'a> {
fn new(
doc: &'a EureDocument,
loaded: &'a LoadedSchemaSet,
base_uri: ResolvedSchemaUri,
) -> Self {
Self {
doc,
loaded,
schema: SchemaDocument::new(),
source_map: IndexMap::new(),
base_uri,
}
}
fn run(
mut self,
visited: &mut IndexSet<ResolvedSchemaUri>,
) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
let root_id = self.doc.get_root_id();
self.process_imports(visited)?;
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.compute_exports()?;
self.validate_and_rewrite_type_references()?;
self.validate_non_productive_reference_cycles()?;
Ok((self.schema, self.source_map))
}
fn process_imports(
&mut self,
visited: &mut IndexSet<ResolvedSchemaUri>,
) -> Result<(), ConversionError> {
let root_id = self.doc.get_root_id();
let root_ctx = self.doc.parse_context(root_id);
let parsed: ParsedImports = parse_root_imports(&root_ctx)?;
for (alias, entry) in parsed.entries {
let imported_uri = self
.loaded
.import_target(&self.base_uri, &alias)
.cloned()
.ok_or_else(|| ConversionError::ImportNotLoaded {
base: self.base_uri.clone(),
alias: alias.to_string(),
raw_path: entry.raw_path.clone(),
})?;
let child_doc = self.loaded.document(&imported_uri).ok_or_else(|| {
ConversionError::ImportNotLoaded {
base: self.base_uri.clone(),
alias: alias.to_string(),
raw_path: entry.raw_path.clone(),
}
})?;
let (child_schema, child_source_map) =
convert_with_visited(self.loaded, imported_uri.clone(), child_doc, visited)?;
self.inline_imported_schema(&alias, imported_uri, child_schema, child_source_map)?;
}
Ok(())
}
fn inline_imported_schema(
&mut self,
alias: &Identifier,
child_uri: ResolvedSchemaUri,
child: SchemaDocument,
child_source_map: SchemaSourceMap,
) -> Result<(), ConversionError> {
let mut remap: Vec<SchemaNodeId> = Vec::with_capacity(child.nodes.len());
for _ in 0..child.nodes.len() {
let new_id = self.schema.create_node(SchemaNodeContent::Any);
remap.push(new_id);
}
for (i, child_node) in child.nodes.iter().enumerate() {
let parent_id = remap[i];
let new_content = remap_node_content(&child_node.content, &remap);
let new_ext_types = remap_ext_types(&child_node.ext_types, &remap);
let parent_node = self.schema.node_mut(parent_id);
parent_node.content = new_content;
parent_node.metadata = child_node.metadata.clone();
parent_node.ext_types = new_ext_types;
parent_node.type_codegen = child_node.type_codegen.clone();
}
let mut all_types = IndexMap::new();
for (name, child_id) in &child.types {
all_types.insert(name.clone(), remap[child_id.0]);
}
for (child_id, source) in child_source_map {
self.source_map.insert(remap[child_id.0], source);
}
self.schema.imports.insert(
alias.clone(),
SchemaImport {
uri: child_uri,
all_types,
exports: child.exports.clone(),
},
);
Ok(())
}
fn compute_exports(&mut self) -> Result<(), ConversionError> {
let root_ctx = self.doc.parse_context(self.doc.get_root_id());
let parsed = parse_root_exports(&root_ctx)?;
match parsed {
None => {
self.schema.exports = self.schema.types.keys().cloned().collect();
}
Some(ParsedExports::Explicit { names }) => {
let mut exports = IndexSet::new();
for name in names {
if !self.schema.types.contains_key(&name) {
return Err(ConversionError::ExportedNameNotDeclared {
name: name.to_string(),
});
}
exports.insert(name);
}
self.schema.exports = exports;
}
}
Ok(())
}
fn validate_and_rewrite_type_references(&mut self) -> Result<(), ConversionError> {
for index in 0..self.schema.nodes.len() {
let needs_rewrite = matches!(
&self.schema.nodes[index].content,
SchemaNodeContent::Reference(_)
);
if !needs_rewrite {
continue;
}
let placeholder = SchemaNodeContent::Any;
let original = std::mem::replace(&mut self.schema.nodes[index].content, placeholder);
let SchemaNodeContent::Reference(type_ref) = original else {
unreachable!("checked above");
};
let resolved = self.resolve_type_reference(type_ref)?;
self.schema.nodes[index].content = SchemaNodeContent::Reference(resolved);
}
Ok(())
}
fn resolve_type_reference(
&self,
type_ref: TypeReference,
) -> Result<TypeReference, ConversionError> {
match type_ref {
TypeReference::Resolved(_) => Ok(type_ref),
TypeReference::Named {
namespace: None,
name,
} => {
let target = self
.schema
.types
.get(&name)
.copied()
.ok_or_else(|| ConversionError::UndefinedTypeReference(name.to_string()))?;
Ok(TypeReference::Resolved(target))
}
TypeReference::Named {
namespace: Some(ns),
name,
} => {
let import = self.schema.imports.get(&ns).ok_or_else(|| {
ConversionError::UnknownImportNamespace {
namespace: ns.to_string(),
name: name.to_string(),
}
})?;
if !import.exports.contains(&name) {
return Err(ConversionError::TypeNotExported {
namespace: ns.to_string(),
name: name.to_string(),
});
}
let target = import.all_types.get(&name).copied().ok_or_else(|| {
ConversionError::UndefinedTypeReference(format!("{}.{}", ns, name))
})?;
Ok(TypeReference::Resolved(target))
}
}
}
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_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;
};
schema.resolve_reference(type_ref)
}
fn display_node(schema: &SchemaDocument, id: SchemaNodeId) -> String {
let reference = TypeReference::Resolved(id);
if let Some(name) = schema.reference_name(&reference) {
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,
SchemaSource {
uri: self.base_uri.clone(),
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 collect_loaded_document_node_paths(
loaded: &LoadedSchemaSet,
) -> IndexMap<ResolvedSchemaUri, IndexMap<NodeId, EurePath>> {
loaded
.documents
.iter()
.map(|(uri, doc)| (uri.clone(), collect_document_node_paths(doc)))
.collect()
}
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<ResolvedSchemaUri, IndexMap<NodeId, EurePath>>,
) -> LayoutStrategies {
let mut layout = LayoutStrategies::default();
for (schema_id, source) in source_map {
if let Some(path) = source_node_paths
.get(&source.uri)
.and_then(|paths| 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(crate) fn convert_with_visited(
loaded: &LoadedSchemaSet,
base_uri: ResolvedSchemaUri,
doc: &EureDocument,
visited: &mut IndexSet<ResolvedSchemaUri>,
) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
if !visited.insert(base_uri.clone()) {
return Err(ConversionError::ImportCycle {
cycle: visited.iter().cloned().collect(),
attempted: base_uri,
});
}
let result = Converter::new(doc, loaded, base_uri.clone()).run(visited);
visited.shift_remove(&base_uri);
result
}
pub fn loaded_schema_set_to_schema(
loaded: &LoadedSchemaSet,
) -> Result<(SchemaDocument, SchemaSourceMap), ConversionError> {
let root_doc =
loaded
.document(&loaded.root_uri)
.ok_or_else(|| ConversionError::ImportNotLoaded {
base: loaded.root_uri.clone(),
alias: "<root>".to_string(),
raw_path: loaded.root_uri.to_string(),
})?;
let mut visited = IndexSet::new();
convert_with_visited(loaded, loaded.root_uri.clone(), root_doc, &mut visited)
}
pub fn loaded_schema_set_to_schema_with_layout(
loaded: &LoadedSchemaSet,
) -> Result<(SchemaDocument, LayoutStrategies, SchemaSourceMap), ConversionError> {
let (schema, source_map) = loaded_schema_set_to_schema(loaded)?;
let source_node_paths = collect_loaded_document_node_paths(loaded);
let layout = build_layout_strategies(&schema, &source_map, &source_node_paths);
Ok((schema, layout, source_map))
}
pub fn document_to_schema_with_layout(
doc: &EureDocument,
) -> Result<(SchemaDocument, LayoutStrategies, SchemaSourceMap), ConversionError> {
let root_uri = ResolvedSchemaUri::Inline("<root>".to_string());
let loaded = LoadedSchemaSet::new(root_uri, doc.clone());
loaded_schema_set_to_schema_with_layout(&loaded)
}
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))
}
fn remap_node_content(content: &SchemaNodeContent, remap: &[SchemaNodeId]) -> SchemaNodeContent {
let map_id = |id: SchemaNodeId| remap[id.0];
match content {
SchemaNodeContent::Reference(TypeReference::Resolved(id)) => {
SchemaNodeContent::Reference(TypeReference::Resolved(map_id(*id)))
}
SchemaNodeContent::Reference(TypeReference::Named { namespace, name }) => {
SchemaNodeContent::Reference(TypeReference::Named {
namespace: namespace.clone(),
name: name.clone(),
})
}
SchemaNodeContent::Array(a) => SchemaNodeContent::Array(ArraySchema {
item: map_id(a.item),
min_length: a.min_length,
max_length: a.max_length,
unique: a.unique,
contains: a.contains.map(map_id),
binding_style: a.binding_style,
}),
SchemaNodeContent::Map(m) => SchemaNodeContent::Map(MapSchema {
key: map_id(m.key),
value: map_id(m.value),
min_size: m.min_size,
max_size: m.max_size,
}),
SchemaNodeContent::Record(r) => {
let mut properties = IndexMap::new();
for (name, field) in &r.properties {
properties.insert(
name.clone(),
RecordFieldSchema {
schema: map_id(field.schema),
optional: field.optional,
binding_style: field.binding_style,
field_codegen: field.field_codegen.clone(),
},
);
}
let flatten: Vec<SchemaNodeId> = r.flatten.iter().map(|id| map_id(*id)).collect();
let unknown_fields = match &r.unknown_fields {
UnknownFieldsPolicy::Schema(id) => UnknownFieldsPolicy::Schema(map_id(*id)),
UnknownFieldsPolicy::Deny => UnknownFieldsPolicy::Deny,
UnknownFieldsPolicy::Allow => UnknownFieldsPolicy::Allow,
};
SchemaNodeContent::Record(RecordSchema {
properties,
flatten,
unknown_fields,
})
}
SchemaNodeContent::Tuple(t) => SchemaNodeContent::Tuple(TupleSchema {
elements: t.elements.iter().map(|id| map_id(*id)).collect(),
binding_style: t.binding_style,
}),
SchemaNodeContent::Union(u) => {
let mut variants = IndexMap::new();
for (name, &id) in &u.variants {
variants.insert(name.clone(), map_id(id));
}
SchemaNodeContent::Union(UnionSchema {
variants,
unambiguous: u.unambiguous.clone(),
interop: u.interop.clone(),
deny_untagged: u.deny_untagged.clone(),
})
}
SchemaNodeContent::Any => SchemaNodeContent::Any,
SchemaNodeContent::Boolean => SchemaNodeContent::Boolean,
SchemaNodeContent::Null => SchemaNodeContent::Null,
SchemaNodeContent::Text(t) => SchemaNodeContent::Text(t.clone()),
SchemaNodeContent::Integer(i) => SchemaNodeContent::Integer(i.clone()),
SchemaNodeContent::Float(f) => SchemaNodeContent::Float(f.clone()),
SchemaNodeContent::Literal(d) => SchemaNodeContent::Literal(d.clone()),
}
}
fn remap_ext_types(
ext_types: &IndexMap<Identifier, ExtTypeSchema>,
remap: &[SchemaNodeId],
) -> IndexMap<Identifier, ExtTypeSchema> {
let mut out = IndexMap::new();
for (name, ext) in ext_types {
out.insert(
name.clone(),
ExtTypeSchema {
schema: remap[ext.schema.0],
optional: ext.optional,
binding_style: ext.binding_style,
},
);
}
out
}
#[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);
let uri = ResolvedSchemaUri::Inline("<test>".to_string());
let loaded = LoadedSchemaSet::new(uri.clone(), doc.clone());
assert_eq!(
Converter::new(&doc, &loaded, uri)
.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),
}
}
fn to_inline_uri(s: &str) -> ResolvedSchemaUri {
ResolvedSchemaUri::Inline(s.to_string())
}
fn loaded_single(name: &str, doc: EureDocument) -> LoadedSchemaSet {
LoadedSchemaSet::new(to_inline_uri(name), doc)
}
#[test]
fn import_resolves_namespaced_reference() {
let common = eure!({
%types.username = @code("text")
});
let user = eure!({
%import.common = "common"
%types.user {
name = @code("$types.common.username")
}
});
let user_uri = to_inline_uri("user");
let common_uri = to_inline_uri("common");
let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
loaded.insert_document(common_uri.clone(), common);
loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
let (schema, _) = loaded_schema_set_to_schema(&loaded).expect("conversion succeeds");
let user_id: Identifier = "user".parse().unwrap();
let common_id: Identifier = "common".parse().unwrap();
assert!(schema.types.contains_key(&user_id));
assert!(schema.imports.contains_key(&common_id));
let username: Identifier = "username".parse().unwrap();
let imported_username = schema.imports[&common_id].all_types[&username];
let user_node_id = schema.types.get(&user_id).unwrap();
let SchemaNodeContent::Record(rec) = &schema.node(*user_node_id).content else {
panic!("user must be a record");
};
let name_field = rec.properties.get("name").unwrap();
let SchemaNodeContent::Reference(tr) = &schema.node(name_field.schema).content else {
panic!("name field must be a Reference");
};
assert_eq!(*tr, TypeReference::Resolved(imported_username));
}
#[test]
fn imported_type_does_not_collide_with_similar_local_name() {
let common = eure!({
%types.User = @code("text")
});
let user = eure!({
%import.common = "common"
%types.common__User = @code("integer")
%types.wrapper {
imported = @code("$types.common.User")
local = @code("$types.common__User")
}
});
let user_uri = to_inline_uri("user");
let common_uri = to_inline_uri("common");
let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
loaded.insert_document(common_uri.clone(), common);
loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
let (schema, _) = loaded_schema_set_to_schema(&loaded).expect("conversion succeeds");
let local_name: Identifier = "common__User".parse().unwrap();
let imported_name: Identifier = "User".parse().unwrap();
let common_alias: Identifier = "common".parse().unwrap();
let local_id = schema.types[&local_name];
let imported_id = schema.imports[&common_alias].all_types[&imported_name];
assert_ne!(local_id, imported_id);
let wrapper_name: Identifier = "wrapper".parse().unwrap();
let SchemaNodeContent::Record(rec) = &schema.node(schema.types[&wrapper_name]).content
else {
panic!("wrapper must be a record");
};
let SchemaNodeContent::Reference(imported_ref) =
&schema.node(rec.properties["imported"].schema).content
else {
panic!("imported field must be a reference");
};
let SchemaNodeContent::Reference(local_ref) =
&schema.node(rec.properties["local"].schema).content
else {
panic!("local field must be a reference");
};
assert_eq!(*imported_ref, TypeReference::Resolved(imported_id));
assert_eq!(*local_ref, TypeReference::Resolved(local_id));
}
#[test]
fn import_cycle_is_rejected() {
let a = eure!({
%import.b = "b"
});
let b = eure!({
%import.a = "a"
});
let a_uri = to_inline_uri("a");
let b_uri = to_inline_uri("b");
let mut loaded = LoadedSchemaSet::new(a_uri.clone(), a);
loaded.insert_document(b_uri.clone(), b);
loaded.insert_import(a_uri.clone(), "b".parse().unwrap(), b_uri.clone());
loaded.insert_import(b_uri, "a".parse().unwrap(), a_uri);
let err = loaded_schema_set_to_schema(&loaded).expect_err("cycle must be rejected");
match err {
ConversionError::ImportCycle { cycle, attempted } => {
let formatted = format_cycle(&cycle, &attempted);
assert!(formatted.contains("a"));
assert!(formatted.contains("b"));
}
other => panic!("expected ImportCycle, got {:?}", other),
}
}
#[test]
fn unknown_import_namespace_is_rejected() {
let user = eure!({
%types.user {
name = @code("$types.common.username")
}
});
let err = loaded_schema_set_to_schema(&loaded_single("user", user))
.expect_err("missing import must be rejected");
match err {
ConversionError::UnknownImportNamespace { namespace, name } => {
assert_eq!(namespace, "common");
assert_eq!(name, "username");
}
other => panic!("expected UnknownImportNamespace, got {:?}", other),
}
}
#[test]
fn type_not_exported_is_rejected() {
let common = eure!({
%types.username = @code("text")
%types."internal-helper" = @code("text")
%export = ["username"]
});
let user = eure!({
%import.common = "common"
%types.user {
helper = @code("$types.common.internal-helper")
}
});
let user_uri = to_inline_uri("user");
let common_uri = to_inline_uri("common");
let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
loaded.insert_document(common_uri.clone(), common);
loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
let err =
loaded_schema_set_to_schema(&loaded).expect_err("non-exported type must be rejected");
match err {
ConversionError::TypeNotExported { namespace, name } => {
assert_eq!(namespace, "common");
assert_eq!(name, "internal-helper");
}
other => panic!("expected TypeNotExported, got {:?}", other),
}
}
#[test]
fn omitted_export_exposes_all_local_types() {
let common = eure!({
%types.username = @code("text")
%types.email = @code("text")
});
let user = eure!({
%import.common = "common"
%types.user {
name = @code("$types.common.username")
mail = @code("$types.common.email")
}
});
let user_uri = to_inline_uri("user");
let common_uri = to_inline_uri("common");
let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
loaded.insert_document(common_uri.clone(), common);
loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
let _ = loaded_schema_set_to_schema(&loaded).expect("omitted export exposes all types");
}
#[test]
fn explicit_export_lists_become_the_export_set() {
let common = eure!({
%types.username = @code("text")
%types.email = @code("text")
%export = ["username"]
});
let user = eure!({
%import.common = "common"
});
let user_uri = to_inline_uri("user");
let common_uri = to_inline_uri("common");
let mut loaded = LoadedSchemaSet::new(user_uri.clone(), user);
loaded.insert_document(common_uri.clone(), common);
loaded.insert_import(user_uri, "common".parse().unwrap(), common_uri);
let (schema, _) = loaded_schema_set_to_schema(&loaded).expect("conversion succeeds");
let common_id: Identifier = "common".parse().unwrap();
let username: Identifier = "username".parse().unwrap();
let email: Identifier = "email".parse().unwrap();
assert!(schema.imports[&common_id].all_types.contains_key(&username));
assert!(schema.imports[&common_id].all_types.contains_key(&email));
assert!(schema.imports[&common_id].exports.contains(&username));
assert!(!schema.imports[&common_id].exports.contains(&email));
}
#[test]
fn export_lists_must_match_locally_declared_types() {
let common = eure!({
%types.username = @code("text")
%export = ["nope"]
});
let err = loaded_schema_set_to_schema(&loaded_single("common", common))
.expect_err("$export must reference a declared type");
match err {
ConversionError::ExportedNameNotDeclared { name } => {
assert_eq!(name, "nope");
}
other => panic!("expected ExportedNameNotDeclared, got {:?}", other),
}
}
}