use std::collections::{BTreeMap, BTreeSet};
use sora_diagnostics::{Result, SoraError};
use sora_ir::model::{ConfigIr, FieldIr, StructIr, TableModeIr, TypeIr, UnionIr};
use crate::model::{ConfigData, Value};
pub(super) fn validate_field_value(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
field: &FieldIr,
path: &str,
value: &Value,
) -> Result<()> {
validate_typed_value(
ir,
config_data,
table,
path,
&field.ty,
ValueConstraints {
range: field.range,
length: field.length,
},
value,
)
}
#[derive(Clone, Copy)]
struct ValueConstraints {
range: Option<[i64; 2]>,
length: Option<[usize; 2]>,
}
fn validate_typed_value(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
ty: &TypeIr,
constraints: ValueConstraints,
value: &Value,
) -> Result<()> {
match ty {
TypeIr::Optional(element) if matches!(value, Value::Null) => Ok(()),
TypeIr::Optional(element) => {
validate_typed_value(ir, config_data, table, path, element, constraints, value)
}
TypeIr::Bool => expect_type(table, path, ty, value, matches!(value, Value::Bool(_))),
TypeIr::I8 => validate_integer_type::<i8>(table, path, ty, value, constraints.range),
TypeIr::U8 => validate_integer_type::<u8>(table, path, ty, value, constraints.range),
TypeIr::I16 => validate_integer_type::<i16>(table, path, ty, value, constraints.range),
TypeIr::U16 => validate_integer_type::<u16>(table, path, ty, value, constraints.range),
TypeIr::I32 => match value {
Value::Integer(number) if i32::try_from(*number).is_ok() => {
validate_integer_range(table, path, *number, constraints.range)
}
_ => type_mismatch(table, path, ty, value),
},
TypeIr::U32 => validate_integer_type::<u32>(table, path, ty, value, constraints.range),
TypeIr::I64 => match value {
Value::Integer(number) => {
validate_integer_range(table, path, *number, constraints.range)
}
_ => type_mismatch(table, path, ty, value),
},
TypeIr::Duration => match value {
Value::Integer(number) if *number >= 0 => {
validate_integer_range(table, path, *number, constraints.range)
}
_ => type_mismatch(table, path, ty, value),
},
TypeIr::F32 | TypeIr::F64 => match value {
Value::Integer(number) => {
validate_float_range(table, path, *number as f64, constraints.range)
}
Value::Float(number) => validate_float_range(table, path, *number, constraints.range),
_ => type_mismatch(table, path, ty, value),
},
TypeIr::String | TypeIr::Text => match value {
Value::String(value) => {
validate_length(table, path, value.chars().count(), constraints.length)
}
_ => type_mismatch(table, path, ty, value),
},
TypeIr::Enum(enum_name) => validate_enum(ir, table, path, ty, enum_name, value),
TypeIr::Struct(struct_name) => {
validate_struct(ir, config_data, table, path, ty, struct_name, value)
}
TypeIr::Union(union_name) => {
validate_union(ir, config_data, table, path, ty, union_name, value)
}
TypeIr::List(element) => {
validate_list(ir, config_data, table, path, element, constraints, value)
}
TypeIr::Set(element) => {
validate_list(ir, config_data, table, path, element, constraints, value)
}
TypeIr::Map {
key,
value: element,
} => validate_map(
ir,
config_data,
table,
path,
MapExpectation {
key,
value: element,
},
constraints,
value,
),
TypeIr::Array { element, len } => validate_array(
ir,
config_data,
table,
path,
ArrayExpectation {
ty,
element,
len: *len,
},
constraints,
value,
),
TypeIr::Ref {
table: ref_table,
field: ref_field,
} => validate_ref(config_data, table, path, ty, ref_table, ref_field, value),
}
}
fn validate_integer_type<T>(
table: &str,
path: &str,
ty: &TypeIr,
value: &Value,
range: Option<[i64; 2]>,
) -> Result<()>
where
T: TryFrom<i64>,
{
match value {
Value::Integer(number) if T::try_from(*number).is_ok() => {
validate_integer_range(table, path, *number, range)
}
_ => type_mismatch(table, path, ty, value),
}
}
fn validate_enum(
ir: &ConfigIr,
table: &str,
path: &str,
ty: &TypeIr,
enum_name: &str,
value: &Value,
) -> Result<()> {
let Value::String(item) = value else {
return type_mismatch(table, path, ty, value);
};
let is_valid = ir
.enums
.iter()
.find(|candidate| candidate.name == enum_name)
.is_some_and(|candidate| {
candidate.values.contains(item)
|| candidate.aliases.iter().any(|entry| entry.alias == *item)
});
if is_valid {
Ok(())
} else {
Err(SoraError::InvalidEnumValue {
table: table.to_owned(),
field: path.to_owned(),
value: item.clone(),
})
}
}
fn validate_struct(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
ty: &TypeIr,
struct_name: &str,
value: &Value,
) -> Result<()> {
let Value::Object(values) = value else {
return type_mismatch(table, path, ty, value);
};
let Some(struct_ir) = ir
.structs
.iter()
.find(|candidate| candidate.name == struct_name)
else {
return type_mismatch(table, path, ty, value);
};
validate_object_fields(ir, config_data, table, path, struct_ir, values)
}
fn validate_object_fields(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
struct_ir: &StructIr,
values: &BTreeMap<String, Value>,
) -> Result<()> {
let field_names = struct_ir
.fields
.iter()
.map(|field| field.name.as_str())
.collect::<BTreeSet<_>>();
for field_name in values.keys() {
if !field_names.contains(field_name.as_str()) {
return Err(SoraError::UnknownField {
table: table.to_owned(),
field: format!("{path}.{field_name}"),
});
}
}
for field in &struct_ir.fields {
let child_path = format!("{path}.{}", field.name);
match values.get(&field.name) {
Some(value) => {
validate_field_value(ir, config_data, table, field, &child_path, value)?;
}
None if field.is_required() => {
return Err(SoraError::MissingRequiredField {
table: table.to_owned(),
field: child_path,
});
}
None => {}
}
}
Ok(())
}
fn validate_union(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
ty: &TypeIr,
union_name: &str,
value: &Value,
) -> Result<()> {
let Value::Object(values) = value else {
return type_mismatch(table, path, ty, value);
};
let Some(union_ir) = ir
.unions
.iter()
.find(|candidate| candidate.name == union_name)
else {
return type_mismatch(table, path, ty, value);
};
validate_union_object(ir, config_data, table, path, ty, union_ir, values)
}
fn validate_union_object(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
ty: &TypeIr,
union_ir: &UnionIr,
values: &BTreeMap<String, Value>,
) -> Result<()> {
let Some(Value::String(variant_name)) = values.get(&union_ir.tag) else {
return type_mismatch(table, &format!("{path}.{}", union_ir.tag), ty, &Value::Null);
};
let Some(variant) = union_ir
.variants
.iter()
.find(|candidate| candidate.name == *variant_name)
else {
return Err(SoraError::InvalidEnumValue {
table: table.to_owned(),
field: format!("{path}.{}", union_ir.tag),
value: variant_name.clone(),
});
};
let field_names = variant
.fields
.iter()
.map(|field| field.name.as_str())
.chain(std::iter::once(union_ir.tag.as_str()))
.collect::<BTreeSet<_>>();
for field_name in values.keys() {
if !field_names.contains(field_name.as_str()) {
return Err(SoraError::UnknownField {
table: table.to_owned(),
field: format!("{path}.{field_name}"),
});
}
}
for field in &variant.fields {
let child_path = format!("{path}.{}", field.name);
match values.get(&field.name) {
Some(value) => {
validate_field_value(ir, config_data, table, field, &child_path, value)?;
}
None if field.is_required() => {
return Err(SoraError::MissingRequiredField {
table: table.to_owned(),
field: child_path,
});
}
None => {}
}
}
Ok(())
}
fn validate_list(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
element: &TypeIr,
constraints: ValueConstraints,
value: &Value,
) -> Result<()> {
let Value::List(items) = value else {
return type_mismatch(table, path, &TypeIr::List(Box::new(element.clone())), value);
};
validate_length(table, path, items.len(), constraints.length)?;
for (index, item) in items.iter().enumerate() {
validate_typed_value(
ir,
config_data,
table,
&format!("{path}[{index}]"),
element,
ValueConstraints {
range: constraints.range,
length: None,
},
item,
)?;
}
Ok(())
}
struct MapExpectation<'a> {
key: &'a TypeIr,
value: &'a TypeIr,
}
fn validate_map(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
expectation: MapExpectation<'_>,
constraints: ValueConstraints,
value: &Value,
) -> Result<()> {
let Value::List(items) = value else {
return type_mismatch(
table,
path,
&TypeIr::Map {
key: Box::new(expectation.key.clone()),
value: Box::new(expectation.value.clone()),
},
value,
);
};
validate_length(table, path, items.len(), constraints.length)?;
for (index, item) in items.iter().enumerate() {
let Value::List(pair) = item else {
return type_mismatch(table, &format!("{path}[{index}]"), expectation.value, item);
};
if pair.len() != 2 {
return Err(SoraError::InvalidSchema(format!(
"map value `{path}[{index}]` must contain exactly two elements"
)));
}
validate_typed_value(
ir,
config_data,
table,
&format!("{path}[{index}].key"),
expectation.key,
ValueConstraints {
range: None,
length: None,
},
&pair[0],
)?;
validate_typed_value(
ir,
config_data,
table,
&format!("{path}[{index}].value"),
expectation.value,
ValueConstraints {
range: None,
length: None,
},
&pair[1],
)?;
}
Ok(())
}
struct ArrayExpectation<'a> {
ty: &'a TypeIr,
element: &'a TypeIr,
len: usize,
}
fn validate_array(
ir: &ConfigIr,
config_data: &ConfigData,
table: &str,
path: &str,
expectation: ArrayExpectation<'_>,
constraints: ValueConstraints,
value: &Value,
) -> Result<()> {
let Value::List(items) = value else {
return type_mismatch(table, path, expectation.ty, value);
};
if items.len() != expectation.len {
return type_mismatch(table, path, expectation.ty, value);
}
validate_length(table, path, items.len(), constraints.length)?;
for (index, item) in items.iter().enumerate() {
validate_typed_value(
ir,
config_data,
table,
&format!("{path}[{index}]"),
expectation.element,
ValueConstraints {
range: constraints.range,
length: None,
},
item,
)?;
}
Ok(())
}
fn validate_ref(
config_data: &ConfigData,
table: &str,
path: &str,
ty: &TypeIr,
ref_table: &str,
ref_field: &str,
value: &Value,
) -> Result<()> {
if !matches!(value, Value::Integer(_) | Value::String(_)) {
return type_mismatch(table, path, ty, value);
}
let key = stable_key(value);
let exists = config_data
.tables
.iter()
.find(|candidate| candidate.name == ref_table)
.is_some_and(|table_data| {
table_data
.rows
.iter()
.filter_map(|row| row.values.get(ref_field))
.any(|candidate| stable_key(candidate) == key)
});
if exists {
Ok(())
} else {
Err(SoraError::MissingReference {
table: table.to_owned(),
field: path.to_owned(),
ref_table: ref_table.to_owned(),
ref_field: ref_field.to_owned(),
value: key,
})
}
}
fn expect_type(table: &str, path: &str, ty: &TypeIr, value: &Value, matches: bool) -> Result<()> {
if matches {
Ok(())
} else {
type_mismatch(table, path, ty, value)
}
}
fn type_mismatch(table: &str, path: &str, ty: &TypeIr, value: &Value) -> Result<()> {
Err(SoraError::TypeMismatch {
table: table.to_owned(),
field: path.to_owned(),
expected: ty.to_string(),
actual: value.kind_name().to_owned(),
})
}
fn validate_length(
table: &str,
path: &str,
actual: usize,
length: Option<[usize; 2]>,
) -> Result<()> {
let Some([min, max]) = length else {
return Ok(());
};
if actual < min || actual > max {
Err(SoraError::LengthOutOfBounds {
table: table.to_owned(),
field: path.to_owned(),
actual,
min,
max,
})
} else {
Ok(())
}
}
fn validate_integer_range(
table: &str,
path: &str,
value: i64,
range: Option<[i64; 2]>,
) -> Result<()> {
let Some([min, max]) = range else {
return Ok(());
};
if value < min || value > max {
Err(SoraError::RangeOutOfBounds {
table: table.to_owned(),
field: path.to_owned(),
value: value.to_string(),
min,
max,
})
} else {
Ok(())
}
}
fn validate_float_range(
table: &str,
path: &str,
value: f64,
range: Option<[i64; 2]>,
) -> Result<()> {
let Some([min, max]) = range else {
return Ok(());
};
if value < min as f64 || value > max as f64 {
Err(SoraError::RangeOutOfBounds {
table: table.to_owned(),
field: path.to_owned(),
value: value.to_string(),
min,
max,
})
} else {
Ok(())
}
}
pub(super) fn stable_key(value: &Value) -> String {
match value {
Value::Bool(value) => value.to_string(),
Value::Integer(value) => value.to_string(),
Value::Float(value) => value.to_string(),
Value::String(value) => value.clone(),
Value::List(_) => "<list>".to_owned(),
Value::Object(_) => "<object>".to_owned(),
Value::Null => "<null>".to_owned(),
}
}
pub(super) fn table_mode_name(mode: TableModeIr) -> &'static str {
match mode {
TableModeIr::List => "list",
TableModeIr::Map => "map",
TableModeIr::Singleton => "singleton",
}
}