use std::collections::BTreeMap;
use super::{
schematype_of_restricted_expr, EntityTypeDescription, GetSchemaTypeError,
HeterogeneousSetError, Schema, SchemaType, TypeMismatchError,
};
use crate::ast::{
BorrowedRestrictedExpr, Entity, EntityType, EntityUID, PartialValue,
PartialValueToRestrictedExprError, RestrictedExpr,
};
use crate::extensions::{ExtensionFunctionLookupError, Extensions};
use either::Either;
use miette::Diagnostic;
use smol_str::SmolStr;
use thiserror::Error;
#[derive(Debug, Diagnostic, Error)]
pub enum EntitySchemaConformanceError {
#[error("attribute `{attr}` on `{uid}` should not exist according to the schema")]
UnexpectedEntityAttr {
uid: EntityUID,
attr: SmolStr,
},
#[error("expected entity `{uid}` to have attribute `{attr}`, but it does not")]
MissingRequiredEntityAttr {
uid: EntityUID,
attr: SmolStr,
},
#[error("in attribute `{attr}` on `{uid}`, {err}")]
TypeMismatch {
uid: EntityUID,
attr: SmolStr,
#[diagnostic(transparent)]
err: TypeMismatchError,
},
#[error("in attribute `{attr}` on `{uid}`, {err}")]
HeterogeneousSet {
uid: EntityUID,
attr: SmolStr,
#[diagnostic(transparent)]
err: HeterogeneousSetError,
},
#[error(
"`{uid}` is not allowed to have an ancestor of type `{ancestor_ty}` according to the schema"
)]
InvalidAncestorType {
uid: EntityUID,
ancestor_ty: Box<EntityType>, },
#[error(transparent)]
#[diagnostic(transparent)]
UnexpectedEntityType(#[from] UnexpectedEntityTypeError),
#[error("found action entity `{uid}`, but it was not declared as an action in the schema")]
UndeclaredAction {
uid: EntityUID,
},
#[error("definition of action `{uid}` does not match its schema declaration")]
#[diagnostic(help(
"to use the schema's definition of `{uid}`, simply omit it from the entities input data"
))]
ActionDeclarationMismatch {
uid: EntityUID,
},
#[error("in attribute `{attr}` on `{uid}`, {err}")]
ExtensionFunctionLookup {
uid: EntityUID,
attr: SmolStr,
#[diagnostic(transparent)]
err: ExtensionFunctionLookupError,
},
}
#[derive(Debug, Error)]
#[error("entity `{uid}` has type `{}` which is not declared in the schema", .uid.entity_type())]
pub struct UnexpectedEntityTypeError {
pub uid: EntityUID,
pub suggested_types: Vec<EntityType>,
}
impl Diagnostic for UnexpectedEntityTypeError {
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
match self.suggested_types.as_slice() {
[] => None,
[ty] => Some(Box::new(format!("did you mean `{ty}`?"))),
tys => Some(Box::new(format!(
"did you mean one of {:?}?",
tys.iter().map(ToString::to_string).collect::<Vec<String>>()
))),
}
}
}
#[derive(Debug, Clone)]
pub struct EntitySchemaConformanceChecker<'a, S: Schema> {
schema: &'a S,
extensions: Extensions<'a>,
}
impl<'a, S: Schema> EntitySchemaConformanceChecker<'a, S> {
pub fn new(schema: &'a S, extensions: Extensions<'a>) -> Self {
Self { schema, extensions }
}
pub fn validate_entity(&self, entity: &Entity) -> Result<(), EntitySchemaConformanceError> {
let uid = entity.uid();
let etype = uid.entity_type();
if etype.is_action() {
let schema_action = self
.schema
.action(uid)
.ok_or(EntitySchemaConformanceError::UndeclaredAction { uid: uid.clone() })?;
if !entity.deep_eq(&schema_action) {
return Err(EntitySchemaConformanceError::ActionDeclarationMismatch {
uid: uid.clone(),
});
}
} else {
let schema_etype = self.schema.entity_type(etype).ok_or_else(|| {
let suggested_types = match etype {
EntityType::Specified(name) => self
.schema
.entity_types_with_basename(name.basename())
.collect(),
EntityType::Unspecified => vec![],
};
UnexpectedEntityTypeError {
uid: uid.clone(),
suggested_types,
}
})?;
for required_attr in schema_etype.required_attrs() {
if entity.get(&required_attr).is_none() {
return Err(EntitySchemaConformanceError::MissingRequiredEntityAttr {
uid: uid.clone(),
attr: required_attr,
});
}
}
for (attr, val) in entity.attrs() {
match schema_etype.attr_type(attr) {
None => {
if !schema_etype.open_attributes() {
return Err(EntitySchemaConformanceError::UnexpectedEntityAttr {
uid: uid.clone(),
attr: attr.clone(),
});
}
}
Some(expected_ty) => {
match typecheck_value_against_schematype(val, &expected_ty, self.extensions)
{
Ok(()) => {} Err(TypecheckError::TypeMismatch(err)) => {
return Err(EntitySchemaConformanceError::TypeMismatch {
uid: uid.clone(),
attr: attr.clone(),
err,
});
}
Err(TypecheckError::HeterogeneousSet(err)) => {
return Err(EntitySchemaConformanceError::HeterogeneousSet {
uid: uid.clone(),
attr: attr.clone(),
err,
});
}
Err(TypecheckError::ExtensionFunctionLookup(err)) => {
return Err(
EntitySchemaConformanceError::ExtensionFunctionLookup {
uid: uid.clone(),
attr: attr.clone(),
err,
},
);
}
}
}
}
}
for ancestor_euid in entity.ancestors() {
let ancestor_type = ancestor_euid.entity_type();
if schema_etype.allowed_parent_types().contains(ancestor_type) {
} else {
return Err(EntitySchemaConformanceError::InvalidAncestorType {
uid: uid.clone(),
ancestor_ty: Box::new(ancestor_type.clone()),
});
}
}
}
Ok(())
}
}
pub fn typecheck_value_against_schematype(
value: &PartialValue,
expected_ty: &SchemaType,
extensions: Extensions<'_>,
) -> Result<(), TypecheckError> {
match RestrictedExpr::try_from(value.clone()) {
Ok(expr) => typecheck_restricted_expr_against_schematype(
expr.as_borrowed(),
expected_ty,
extensions,
),
Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
Ok(())
}
}
}
pub fn does_restricted_expr_implement_schematype(
expr: BorrowedRestrictedExpr<'_>,
expected_ty: &SchemaType,
) -> bool {
use SchemaType::*;
match expected_ty {
Bool => expr.as_bool().is_some(),
Long => expr.as_long().is_some(),
String => expr.as_string().is_some(),
EmptySet => expr.as_set_elements().is_some_and(|e| e.count() == 0),
Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => true,
Set { element_ty: elty } => match expr.as_set_elements() {
Some(mut els) => els.all(|e| does_restricted_expr_implement_schematype(e, elty)),
None => false,
},
Record { attrs, open_attrs } => match expr.as_record_pairs() {
Some(pairs) => {
let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
let all_req_schema_attrs_in_record = attrs.iter().all(|(k, v)| {
!v.required
|| match pairs_map.get(k) {
Some(inner_e) => {
does_restricted_expr_implement_schematype(*inner_e, &v.attr_type)
}
None => false,
}
});
let all_rec_attrs_match_schema =
pairs_map.iter().all(|(k, inner_e)| match attrs.get(*k) {
Some(sch_ty) => {
does_restricted_expr_implement_schematype(*inner_e, &sch_ty.attr_type)
}
None => *open_attrs,
});
all_rec_attrs_match_schema && all_req_schema_attrs_in_record
}
None => false,
},
Extension { name } => match expr.as_extn_fn_call() {
Some((actual_name, _)) => match name.id.as_ref() {
"ipaddr" => actual_name.id.as_ref() == "ip",
_ => name == actual_name,
},
None => false,
},
Entity { ty } => match expr.as_euid() {
Some(actual_euid) => actual_euid.entity_type() == ty,
None => false,
},
}
}
pub fn typecheck_restricted_expr_against_schematype(
expr: BorrowedRestrictedExpr<'_>,
expected_ty: &SchemaType,
extensions: Extensions<'_>,
) -> Result<(), TypecheckError> {
if does_restricted_expr_implement_schematype(expr, expected_ty) {
return Ok(());
}
match schematype_of_restricted_expr(expr, extensions) {
Ok(actual_ty) => Err(TypecheckError::TypeMismatch(TypeMismatchError {
expected: Box::new(expected_ty.clone()),
actual_ty: Some(Box::new(actual_ty)),
actual_val: Either::Right(Box::new(expr.to_owned())),
})),
Err(GetSchemaTypeError::UnknownInsufficientTypeInfo { .. }) => {
Ok(())
}
Err(GetSchemaTypeError::NontrivialResidual { .. }) => {
Ok(())
}
Err(GetSchemaTypeError::HeterogeneousSet(err)) => {
Err(TypecheckError::HeterogeneousSet(err))
}
Err(GetSchemaTypeError::ExtensionFunctionLookup(err)) => {
Err(TypecheckError::ExtensionFunctionLookup(err))
}
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum TypecheckError {
#[error(transparent)]
#[diagnostic(transparent)]
TypeMismatch(#[from] TypeMismatchError),
#[error(transparent)]
#[diagnostic(transparent)]
HeterogeneousSet(#[from] HeterogeneousSetError),
#[error(transparent)]
#[diagnostic(transparent)]
ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
}