use sora_diagnostics::{Result, SoraError};
use crate::model::{DerivedFieldIr, FieldIr, StructIr, TableIr, TypeIr};
pub(super) fn validate_derived_field(
owner_kind: &'static str,
owner: &str,
field: &FieldIr,
derived_from: &DerivedFieldIr,
structs: &[StructIr],
tables: &[TableIr],
) -> Result<()> {
if owner_kind != "table" {
return Err(SoraError::InvalidSchema(format!(
"derived field `{}` is only supported on tables",
field.name
)));
}
let Some(owner_table) = tables.iter().find(|table| table.name == owner) else {
return Err(SoraError::InvalidSchema(format!(
"derived field owner table `{owner}` does not exist"
)));
};
let Some(source_table) = tables
.iter()
.find(|table| table.name == derived_from.source_table)
else {
return Err(SoraError::UnknownRefTable {
owner_kind,
owner: owner.to_owned(),
field: field.name.clone(),
table: derived_from.source_table.clone(),
});
};
let Some(parent_key_field) = owner_table
.fields
.iter()
.find(|candidate| candidate.name == derived_from.parent_key)
else {
return Err(SoraError::UnknownRefField {
owner_kind,
owner: owner.to_owned(),
field: field.name.clone(),
table: owner.to_owned(),
ref_field: derived_from.parent_key.clone(),
});
};
let Some(child_key_field) = source_table
.fields
.iter()
.find(|candidate| candidate.name == derived_from.child_key)
else {
return Err(SoraError::UnknownRefField {
owner_kind,
owner: owner.to_owned(),
field: field.name.clone(),
table: derived_from.source_table.clone(),
ref_field: derived_from.child_key.clone(),
});
};
if !types_compatible(&parent_key_field.ty, &child_key_field.ty, tables) {
return Err(SoraError::InvalidSchema(format!(
"derived field `{}` joins `{}` and `{}` with incompatible key types `{}` and `{}`",
field.name,
derived_from.parent_key,
derived_from.child_key,
parent_key_field.ty,
child_key_field.ty
)));
}
validate_derived_field_result_type(field, derived_from, source_table, structs, tables)?;
if let Some(order_by) = &derived_from.order_by
&& !source_table
.fields
.iter()
.any(|field| field.name == *order_by)
{
return Err(SoraError::UnknownRefField {
owner_kind,
owner: owner.to_owned(),
field: field.name.clone(),
table: derived_from.source_table.clone(),
ref_field: order_by.clone(),
});
}
Ok(())
}
fn validate_derived_field_result_type(
field: &FieldIr,
derived_from: &DerivedFieldIr,
source_table: &TableIr,
structs: &[StructIr],
tables: &[TableIr],
) -> Result<()> {
let value_ty = derived_field_value_type(&field.ty);
if let Some(value_field) = &derived_from.value_field {
let Some(source_field) = source_table
.fields
.iter()
.find(|candidate| candidate.name == *value_field)
else {
return Err(SoraError::UnknownRefField {
owner_kind: "table",
owner: source_table.name.clone(),
field: field.name.clone(),
table: source_table.name.clone(),
ref_field: value_field.clone(),
});
};
if !derived_field_value_types_compatible(&field.ty, value_ty, &source_field.ty, tables) {
return Err(SoraError::InvalidSchema(format!(
"derived field `{}` maps source field `{}` with incompatible type `{}` into `{}`",
field.name, source_field.name, source_field.ty, field.ty
)));
}
return Ok(());
}
let TypeIr::Struct(struct_name) = value_ty else {
return Err(SoraError::InvalidSchema(format!(
"derived field `{}` must assemble struct values or declare `from.field`",
field.name
)));
};
let Some(struct_ir) = structs.iter().find(|item| item.name == *struct_name) else {
return Err(SoraError::InvalidSchema(format!(
"derived field `{}` references unknown struct `{struct_name}`",
field.name
)));
};
for struct_field in &struct_ir.fields {
let Some(source_field) = source_table
.fields
.iter()
.find(|candidate| candidate.name == struct_field.name)
else {
return Err(SoraError::UnknownRefField {
owner_kind: "table",
owner: source_table.name.clone(),
field: field.name.clone(),
table: source_table.name.clone(),
ref_field: struct_field.name.clone(),
});
};
if !types_compatible(&struct_field.ty, &source_field.ty, tables) {
return Err(SoraError::InvalidSchema(format!(
"derived field `{}` maps source field `{}` with incompatible type `{}` into `{}`",
field.name, source_field.name, source_field.ty, struct_field.ty
)));
}
}
Ok(())
}
fn derived_field_value_type(ty: &TypeIr) -> &TypeIr {
match ty {
TypeIr::List(element) => element,
TypeIr::Optional(element) => element,
_ => ty,
}
}
fn derived_field_value_types_compatible(
target_field: &TypeIr,
target_value: &TypeIr,
source: &TypeIr,
tables: &[TableIr],
) -> bool {
types_compatible(target_value, source, tables)
|| matches!(target_field, TypeIr::Optional(_) if types_compatible(target_field, source, tables))
}
fn types_compatible(left: &TypeIr, right: &TypeIr, tables: &[TableIr]) -> bool {
resolve_ref_type(left, tables) == resolve_ref_type(right, tables)
}
fn resolve_ref_type<'a>(ty: &'a TypeIr, tables: &'a [TableIr]) -> Option<&'a TypeIr> {
let mut current = ty;
let max_depth = tables.len().saturating_mul(8).saturating_add(8);
for _ in 0..max_depth {
match current {
TypeIr::Ref { table, field } => {
current = &tables
.iter()
.find(|candidate| candidate.name == *table)?
.fields
.iter()
.find(|candidate| candidate.name == *field)?
.ty;
}
_ => return Some(current),
}
}
None
}