1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
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;
/// Errors raised when entities do not conform to the schema
#[derive(Debug, Diagnostic, Error)]
pub enum EntitySchemaConformanceError {
/// Encountered attribute that shouldn't exist on entities of this type
#[error("attribute `{attr}` on `{uid}` should not exist according to the schema")]
UnexpectedEntityAttr {
/// Entity that had the unexpected attribute
uid: EntityUID,
/// Name of the attribute that was unexpected
attr: SmolStr,
},
/// Didn't encounter attribute that should exist
#[error("expected entity `{uid}` to have attribute `{attr}`, but it does not")]
MissingRequiredEntityAttr {
/// Entity that is missing a required attribute
uid: EntityUID,
/// Name of the attribute which was expected
attr: SmolStr,
},
/// The given attribute on the given entity had a different type than the
/// schema indicated
#[error("in attribute `{attr}` on `{uid}`, {err}")]
TypeMismatch {
/// Entity where the type mismatch occurred
uid: EntityUID,
/// Name of the attribute where the type mismatch occurred
attr: SmolStr,
/// Underlying error
#[diagnostic(transparent)]
err: TypeMismatchError,
},
/// Found a set whose elements don't all have the same type. This doesn't match
/// any possible schema.
#[error("in attribute `{attr}` on `{uid}`, {err}")]
HeterogeneousSet {
/// Entity where the error occurred
uid: EntityUID,
/// Name of the attribute where the error occurred
attr: SmolStr,
/// Underlying error
#[diagnostic(transparent)]
err: HeterogeneousSetError,
},
/// Found an ancestor of a type that's not allowed for that entity
#[error(
"`{uid}` is not allowed to have an ancestor of type `{ancestor_ty}` according to the schema"
)]
InvalidAncestorType {
/// Entity that has an invalid ancestor type
uid: EntityUID,
/// Ancestor type which was invalid
ancestor_ty: Box<EntityType>, // boxed to avoid this variant being very large (and thus all EntitySchemaConformanceErrors being large)
},
/// Encountered an entity of a type which is not declared in the schema.
/// Note that this error is only used for non-Action entity types.
#[error(transparent)]
#[diagnostic(transparent)]
UnexpectedEntityType(#[from] UnexpectedEntityTypeError),
/// Encountered an action which was not declared in the schema
#[error("found action entity `{uid}`, but it was not declared as an action in the schema")]
UndeclaredAction {
/// Action which was not declared in the schema
uid: EntityUID,
},
/// Encountered an action whose definition doesn't precisely match the
/// schema's declaration of that action
#[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 {
/// Action whose definition mismatched between entity data and schema
uid: EntityUID,
},
/// Error looking up an extension function. This error can occur when
/// checking entity conformance because that may require getting information
/// about any extension functions referenced in entity attribute values.
#[error("in attribute `{attr}` on `{uid}`, {err}")]
ExtensionFunctionLookup {
/// Entity where the error occurred
uid: EntityUID,
/// Name of the attribute where the error occurred
attr: SmolStr,
/// Underlying error
#[diagnostic(transparent)]
err: ExtensionFunctionLookupError,
},
}
/// Encountered an entity of a type which is not declared in the schema.
/// Note that this error is only used for non-Action entity types.
#[derive(Debug, Error)]
#[error("entity `{uid}` has type `{}` which is not declared in the schema", .uid.entity_type())]
pub struct UnexpectedEntityTypeError {
/// Entity that had the unexpected type
pub uid: EntityUID,
/// Suggested similar entity types that actually are declared in the schema (if any)
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>>()
))),
}
}
}
/// Struct used to check whether entities conform to a schema
#[derive(Debug, Clone)]
pub struct EntitySchemaConformanceChecker<'a, S: Schema> {
/// Schema to check conformance with
schema: &'a S,
/// Extensions which are active for the conformance checks
extensions: Extensions<'a>,
}
impl<'a, S: Schema> EntitySchemaConformanceChecker<'a, S> {
/// Create a new checker
pub fn new(schema: &'a S, extensions: Extensions<'a>) -> Self {
Self { schema, extensions }
}
/// Validate an entity against the schema, returning an
/// [`EntitySchemaConformanceError`] if it does not comply.
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() })?;
// check that the action exactly matches the schema's definition
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,
}
})?;
// Ensure that all required attributes for `etype` are actually
// included in `entity`
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 each attribute that actually appears in `entity`, ensure it
// complies with the schema
for (attr, val) in entity.attrs() {
match schema_etype.attr_type(attr) {
None => {
// `None` indicates the attribute shouldn't exist -- see
// docs on the `attr_type()` trait method
if !schema_etype.open_attributes() {
return Err(EntitySchemaConformanceError::UnexpectedEntityAttr {
uid: uid.clone(),
attr: attr.clone(),
});
}
}
Some(expected_ty) => {
// typecheck: ensure that the entity attribute value matches
// the expected type
match typecheck_value_against_schematype(val, &expected_ty, self.extensions)
{
Ok(()) => {} // typecheck passes
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 each ancestor that actually appears in `entity`, ensure the
// ancestor type is allowed by the schema
for ancestor_euid in entity.ancestors() {
let ancestor_type = ancestor_euid.entity_type();
if schema_etype.allowed_parent_types().contains(ancestor_type) {
// note that `allowed_parent_types()` was transitively
// closed, so it's actually `allowed_ancestor_types()`
//
// thus, the check passes in this case
} else {
return Err(EntitySchemaConformanceError::InvalidAncestorType {
uid: uid.clone(),
ancestor_ty: Box::new(ancestor_type.clone()),
});
}
}
}
Ok(())
}
}
/// Check whether the given `PartialValue` typechecks with the given `SchemaType`.
/// If the typecheck passes, return `Ok(())`.
/// If the typecheck fails, return an appropriate `Err`.
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 { .. }) => {
// this case should be unreachable for the case of `PartialValue`s
// which are entity attributes, because a `PartialValue` computed
// from a `RestrictedExpr` should only have trivial residuals.
// And as of this writing, there are no callers of this function that
// pass anything other than entity attributes.
// Nonetheless, rather than relying on these delicate invariants,
// it's safe to consider this as passing.
Ok(())
}
}
}
/// Check whether the given `RestrictedExpr` typechecks with the given `SchemaType`.
/// If the typecheck passes, return `Ok(())`.
/// If the typecheck fails, return an appropriate `Err`.
pub fn typecheck_restricted_expr_against_schematype(
expr: BorrowedRestrictedExpr<'_>,
expected_ty: &SchemaType,
extensions: Extensions<'_>,
) -> Result<(), TypecheckError> {
// TODO(#440): instead of computing the `SchemaType` of `expr` and then
// checking whether the schematypes are "consistent", wouldn't it be less
// confusing, more efficient, and maybe even more precise to just typecheck
// directly?
match schematype_of_restricted_expr(expr, extensions) {
Ok(actual_ty) => {
if actual_ty.is_consistent_with(expected_ty) {
// typecheck passes
Ok(())
} else {
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 { .. }) => {
// in this case we just don't have the information to know whether
// the attribute value (an unknown) matches the expected type.
// For now we consider this as passing -- we can't really report a
// type error.
Ok(())
}
Err(GetSchemaTypeError::NontrivialResidual { .. }) => {
// this case is unreachable according to the invariant in the comments
// on `schematype_of_restricted_expr()`.
// Nonetheless, rather than relying on that invariant, it's safe to
// treat this case like the case above and consider this as passing.
Ok(())
}
Err(GetSchemaTypeError::HeterogeneousSet(err)) => {
Err(TypecheckError::HeterogeneousSet(err))
}
Err(GetSchemaTypeError::ExtensionFunctionLookup(err)) => {
Err(TypecheckError::ExtensionFunctionLookup(err))
}
}
}
/// Errors returned by [`typecheck_value_against_schematype()`] and
/// [`typecheck_restricted_expr_against_schematype()`]
#[derive(Debug, Diagnostic, Error)]
pub enum TypecheckError {
/// The given value had a type different than what was expected
#[error(transparent)]
#[diagnostic(transparent)]
TypeMismatch(#[from] TypeMismatchError),
/// The given value contained a heterogeneous set, which doesn't conform to
/// any possible `SchemaType`
#[error(transparent)]
#[diagnostic(transparent)]
HeterogeneousSet(#[from] HeterogeneousSetError),
/// Error looking up an extension function. This error can occur when
/// typechecking a `RestrictedExpr` because that may require getting
/// information about any extension functions referenced in the
/// `RestrictedExpr`; and it can occur when typechecking a `PartialValue`
/// because that may require getting information about any extension
/// functions referenced in residuals.
#[error(transparent)]
#[diagnostic(transparent)]
ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
}