use crate::schema_canonical::{Field, Schema, TypeRepr};
use relon_analyzer::schema::SchemaDef;
use relon_parser::TypeNode;
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum SchemaLowerError {
#[error("field `{field}` has unsupported type `{ty}` (Phase 2.b layout supports Int / Float / Bool only)")]
UnsupportedFieldType {
field: String,
ty: String,
},
#[error("field `{field}` has no declared type")]
UntypedField {
field: String,
},
}
pub fn lower_schema_def(def: &SchemaDef, fallback_name: &str) -> Result<Schema, SchemaLowerError> {
let name = def
.name
.clone()
.unwrap_or_else(|| fallback_name.to_string());
if let Some(elements) = &def.tuple_elements {
let mut tys = Vec::with_capacity(elements.len());
for (idx, ty_node) in elements.iter().enumerate() {
tys.push(lower_type_node(&idx.to_string(), ty_node)?);
}
let mut schema = Schema::tuple(name, tys);
schema.generics = def.generics.clone();
return Ok(schema);
}
let mut fields = Vec::with_capacity(def.fields.len());
for f in &def.fields {
let ty_node = f
.type_hint
.as_ref()
.ok_or_else(|| SchemaLowerError::UntypedField {
field: f.name.clone(),
})?;
let ty = lower_type_node(&f.name, ty_node)?;
fields.push(Field {
name: f.name.clone(),
ty,
default: None,
});
}
Ok(Schema {
name,
generics: def.generics.clone(),
fields,
is_tuple: false,
})
}
pub fn lower_type_node(field_name: &str, ty: &TypeNode) -> Result<TypeRepr, SchemaLowerError> {
let unsupported = || SchemaLowerError::UnsupportedFieldType {
field: field_name.to_string(),
ty: format_type_head(ty),
};
if ty.path.len() != 1 || !ty.generics.is_empty() || ty.variant_fields.is_some() {
return Err(unsupported());
}
match ty.path[0].as_str() {
"Int" => Ok(TypeRepr::Int),
"Float" => Ok(TypeRepr::Float),
"Bool" => Ok(TypeRepr::Bool),
_ => Err(unsupported()),
}
}
fn format_type_head(t: &TypeNode) -> String {
if t.path.is_empty() {
return "<empty>".to_string();
}
let mut s = t.path.join(".");
if !t.generics.is_empty() {
s.push('<');
for (i, g) in t.generics.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
s.push_str(&format_type_head(g));
}
s.push('>');
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use relon_analyzer::schema::SchemaFieldDef;
use relon_parser::{Expr, Node, NodeId, TokenRange, TypeNode};
use std::sync::Arc;
fn dummy_range() -> TokenRange {
TokenRange::default()
}
fn dummy_node() -> Arc<Node> {
Arc::new(Node {
id: NodeId::SYNTHETIC,
expr: Arc::new(Expr::Int(0)),
decorators: vec![],
directives: vec![],
type_hint: None,
range: dummy_range(),
doc_comment: None,
})
}
fn type_node(name: &str) -> TypeNode {
TypeNode {
path: vec![name.to_string()],
generics: vec![],
is_optional: false,
range: dummy_range(),
variant_fields: None,
doc_comment: None,
}
}
fn field(name: &str, ty: TypeNode) -> SchemaFieldDef {
SchemaFieldDef {
name: name.to_string(),
type_hint: Some(ty),
value_range: dummy_range(),
is_wildcard: true,
value_node: dummy_node(),
meta_decorators: vec![],
doc_comment: None,
}
}
fn schema_def(name: &str, fields: Vec<SchemaFieldDef>) -> SchemaDef {
SchemaDef {
name: Some(name.to_string()),
generics: vec![],
fields,
tuple_elements: None,
bases: vec![],
range: dummy_range(),
variants: vec![],
methods: vec![],
schema_no_auto_derives: vec![],
doc_comment: None,
}
}
#[test]
fn lowers_int_float_bool() {
let def = schema_def(
"Mix",
vec![
field("a", type_node("Int")),
field("b", type_node("Float")),
field("c", type_node("Bool")),
],
);
let s = lower_schema_def(&def, "fallback").expect("lower");
assert_eq!(s.name, "Mix");
assert_eq!(s.fields.len(), 3);
assert_eq!(s.fields[0].ty, TypeRepr::Int);
assert_eq!(s.fields[1].ty, TypeRepr::Float);
assert_eq!(s.fields[2].ty, TypeRepr::Bool);
}
#[test]
fn rejects_unknown_field_type() {
let def = schema_def("S", vec![field("custom", type_node("Bytes"))]);
let err = lower_schema_def(&def, "fallback").expect_err("must reject");
assert!(matches!(
err,
SchemaLowerError::UnsupportedFieldType { ref field, ref ty }
if field == "custom" && ty == "Bytes"
));
}
#[test]
fn rejects_string_field() {
let def = schema_def("S", vec![field("name", type_node("String"))]);
let err = lower_schema_def(&def, "fallback").expect_err("must reject");
assert!(matches!(
err,
SchemaLowerError::UnsupportedFieldType { ref field, ref ty }
if field == "name" && ty == "String"
));
}
#[test]
fn rejects_untyped_field() {
let mut def = schema_def("S", vec![field("x", type_node("Int"))]);
def.fields[0].type_hint = None;
let err = lower_schema_def(&def, "fallback").expect_err("must reject");
assert!(matches!(
err,
SchemaLowerError::UntypedField { ref field } if field == "x"
));
}
#[test]
fn anonymous_schema_uses_fallback_name() {
let mut def = schema_def("ignored", vec![field("v", type_node("Int"))]);
def.name = None;
let s = lower_schema_def(&def, "MainParams").expect("lower");
assert_eq!(s.name, "MainParams");
}
}