use crate::ast::{Eid, EntityType, EntityUID};
use crate::entities::conformance::{
err::InvalidEnumEntityError, is_valid_enumerated_entity, validate_euids_in_partial_value,
ValidateEuidError,
};
use crate::extensions::{ExtensionFunctionLookupError, Extensions};
use crate::validator::{
ValidatorActionId, ValidatorEntityType, ValidatorEntityTypeKind, ValidatorSchema,
};
use crate::{ast, entities};
use miette::Diagnostic;
use nonempty::NonEmpty;
use smol_str::SmolStr;
use std::collections::hash_map::Values;
use std::collections::HashSet;
use std::iter::Cloned;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug)]
pub struct CoreSchema<'a> {
schema: &'a ValidatorSchema,
}
impl<'a> CoreSchema<'a> {
pub fn new(schema: &'a ValidatorSchema) -> Self {
Self { schema }
}
}
impl<'a> entities::Schema for CoreSchema<'a> {
type EntityTypeDescription = EntityTypeDescription;
type ActionEntityIterator = Cloned<Values<'a, ast::EntityUID, Arc<ast::Entity>>>;
fn entity_type(&self, entity_type: &ast::EntityType) -> Option<EntityTypeDescription> {
EntityTypeDescription::new(self.schema, entity_type)
}
fn action(&self, action: &ast::EntityUID) -> Option<Arc<ast::Entity>> {
self.schema.actions.get(action).cloned()
}
fn entity_types_with_basename<'b>(
&'b self,
basename: &'b ast::UnreservedId,
) -> Box<dyn Iterator<Item = ast::EntityType> + 'b> {
Box::new(self.schema.entity_types().filter_map(move |entity_type| {
if &entity_type.name().as_ref().basename() == basename {
Some(entity_type.name().clone())
} else {
None
}
}))
}
fn action_entities(&self) -> Self::ActionEntityIterator {
self.schema.actions.values().cloned()
}
}
#[derive(Debug)]
pub struct EntityTypeDescription {
core_type: ast::EntityType,
validator_type: ValidatorEntityType,
allowed_parent_types: Arc<HashSet<ast::EntityType>>,
}
impl EntityTypeDescription {
pub fn new(schema: &ValidatorSchema, type_name: &ast::EntityType) -> Option<Self> {
Some(Self {
core_type: type_name.clone(),
validator_type: schema.get_entity_type(type_name).cloned()?,
allowed_parent_types: {
let mut set = HashSet::new();
for possible_parent_et in schema.entity_types() {
if possible_parent_et.descendants.contains(type_name) {
set.insert(possible_parent_et.name().clone());
}
}
Arc::new(set)
},
})
}
}
impl entities::EntityTypeDescription for EntityTypeDescription {
fn enum_entity_eids(&self) -> Option<NonEmpty<Eid>> {
match &self.validator_type.kind {
ValidatorEntityTypeKind::Enum(choices) => Some(choices.clone().map(Eid::new)),
_ => None,
}
}
fn entity_type(&self) -> ast::EntityType {
self.core_type.clone()
}
fn attr_type(&self, attr: &str) -> Option<entities::SchemaType> {
let attr_type: &crate::validator::types::Type = &self.validator_type.attr(attr)?.attr_type;
#[expect(
clippy::expect_used,
reason = "`attr_type` is taken from a `ValidatorEntityType` which was constructed from a schema"
)]
let core_schema_type: entities::SchemaType = attr_type
.clone()
.try_into()
.expect("failed to convert validator type into Core SchemaType");
debug_assert!(attr_type.is_consistent_with(&core_schema_type));
Some(core_schema_type)
}
fn tag_type(&self) -> Option<entities::SchemaType> {
let tag_type: &crate::validator::types::Type = self.validator_type.tag_type()?;
#[expect(
clippy::expect_used,
reason = "`tag_type` is taken from a `ValidatorEntityType` which was constructed from a schema"
)]
let core_schema_type: entities::SchemaType = tag_type
.clone()
.try_into()
.expect("failed to convert validator type into Core SchemaType");
debug_assert!(tag_type.is_consistent_with(&core_schema_type));
Some(core_schema_type)
}
fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
Box::new(
self.validator_type
.attributes()
.iter()
.filter(|(_, ty)| ty.is_required)
.map(|(attr, _)| attr.clone()),
)
}
fn allowed_parent_types(&self) -> Arc<HashSet<ast::EntityType>> {
Arc::clone(&self.allowed_parent_types)
}
fn open_attributes(&self) -> bool {
self.validator_type.open_attributes().is_open()
}
}
impl ast::RequestSchema for ValidatorSchema {
type Error = RequestValidationError;
fn validate_request(
&self,
request: &ast::Request,
extensions: &Extensions<'_>,
) -> std::result::Result<(), Self::Error> {
let action_uid = request.action().uid();
self.validate_scope_variables(
request.principal().uid(),
action_uid,
request.resource().uid(),
)?;
if let (Some(context), Some(action)) = (request.context(), action_uid) {
self.validate_context(context, action, extensions)?;
}
Ok(())
}
fn validate_context<'a>(
&self,
context: &ast::Context,
action: &ast::EntityUID,
extensions: &Extensions<'a>,
) -> std::result::Result<(), RequestValidationError> {
let validator_action_id = self.get_action_id(action).ok_or_else(|| {
request_validation_errors::UndeclaredActionError {
action: Arc::new(action.clone()),
}
})?;
validate_euids_in_partial_value(&CoreSchema::new(self), &context.clone().into()).map_err(
|e| match e {
ValidateEuidError::InvalidEnumEntity(e) => {
RequestValidationError::InvalidEnumEntity(e)
}
ValidateEuidError::UndeclaredAction(e) => {
request_validation_errors::UndeclaredActionError {
action: Arc::new(e.uid),
}
.into()
}
},
)?;
let expected_context_ty = validator_action_id.context_type();
if !expected_context_ty
.typecheck_partial_value(&context.clone().into(), extensions)
.map_err(RequestValidationError::TypeOfContext)?
{
return Err(request_validation_errors::InvalidContextError {
context: context.clone(),
action: Arc::new(action.clone()),
}
.into());
}
Ok(())
}
fn validate_scope_variables(
&self,
principal: Option<&EntityUID>,
action: Option<&EntityUID>,
resource: Option<&EntityUID>,
) -> std::result::Result<(), RequestValidationError> {
if let Some(principal) = principal {
let principal_type = principal.entity_type();
if let Some(et) = self.get_entity_type(principal_type) {
if let ValidatorEntityType {
kind: ValidatorEntityTypeKind::Enum(choices),
..
} = et
{
is_valid_enumerated_entity(
&Vec::from(choices.clone().map(Eid::new)),
principal,
)
.map_err(RequestValidationError::InvalidEnumEntity)?;
}
} else {
return Err(request_validation_errors::UndeclaredPrincipalTypeError {
principal_ty: principal_type.clone(),
}
.into());
}
}
if let Some(resource) = resource {
let resource_type = resource.entity_type();
if let Some(et) = self.get_entity_type(resource_type) {
if let ValidatorEntityType {
kind: ValidatorEntityTypeKind::Enum(choices),
..
} = et
{
is_valid_enumerated_entity(&Vec::from(choices.clone().map(Eid::new)), resource)
.map_err(RequestValidationError::InvalidEnumEntity)?;
}
} else {
return Err(request_validation_errors::UndeclaredResourceTypeError {
resource_ty: resource_type.clone(),
}
.into());
}
}
if let Some(action) = action {
let validator_action_id = self.get_action_id(action).ok_or_else(|| {
request_validation_errors::UndeclaredActionError {
action: Arc::new(action.clone()),
}
})?;
if let Some(principal) = principal {
let principal_type = principal.entity_type();
validator_action_id
.check_principal_type(principal_type, &Arc::new(action.clone()))?;
}
if let Some(resource) = resource {
let resource_type = resource.entity_type();
validator_action_id
.check_resource_type(resource_type, &Arc::new(action.clone()))?;
}
}
Ok(())
}
}
impl ValidatorActionId {
pub(crate) fn check_principal_type(
&self,
principal_type: &EntityType,
action: &Arc<EntityUID>,
) -> Result<(), request_validation_errors::InvalidPrincipalTypeError> {
if !self.is_applicable_principal_type(principal_type) {
Err(request_validation_errors::InvalidPrincipalTypeError {
principal_ty: principal_type.clone(),
action: Arc::clone(action),
valid_principal_tys: self.applies_to_principals().cloned().collect(),
})
} else {
Ok(())
}
}
pub(crate) fn check_resource_type(
&self,
resource_type: &EntityType,
action: &Arc<EntityUID>,
) -> Result<(), request_validation_errors::InvalidResourceTypeError> {
if !self.is_applicable_resource_type(resource_type) {
Err(request_validation_errors::InvalidResourceTypeError {
resource_ty: resource_type.clone(),
action: Arc::clone(action),
valid_resource_tys: self.applies_to_resources().cloned().collect(),
})
} else {
Ok(())
}
}
}
impl ast::RequestSchema for CoreSchema<'_> {
type Error = RequestValidationError;
fn validate_request(
&self,
request: &ast::Request,
extensions: &Extensions<'_>,
) -> Result<(), Self::Error> {
self.schema.validate_request(request, extensions)
}
fn validate_context<'a>(
&self,
context: &ast::Context,
action: &EntityUID,
extensions: &Extensions<'a>,
) -> std::result::Result<(), Self::Error> {
self.schema.validate_context(context, action, extensions)
}
fn validate_scope_variables(
&self,
principal: Option<&EntityUID>,
action: Option<&EntityUID>,
resource: Option<&EntityUID>,
) -> std::result::Result<(), RequestValidationError> {
self.schema
.validate_scope_variables(principal, action, resource)
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum RequestValidationError {
#[error(transparent)]
#[diagnostic(transparent)]
UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
#[error(transparent)]
#[diagnostic(transparent)]
UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
#[error(transparent)]
#[diagnostic(transparent)]
UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
#[error(transparent)]
#[diagnostic(transparent)]
InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
#[error(transparent)]
#[diagnostic(transparent)]
InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
#[error(transparent)]
#[diagnostic(transparent)]
InvalidContext(#[from] request_validation_errors::InvalidContextError),
#[error("context is not valid: {0}")]
#[diagnostic(transparent)]
TypeOfContext(ExtensionFunctionLookupError),
#[error(transparent)]
#[diagnostic(transparent)]
InvalidEnumEntity(#[from] InvalidEnumEntityError),
}
pub mod request_validation_errors {
use crate::ast;
use itertools::Itertools;
use miette::Diagnostic;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("request's action `{action}` is not declared in the schema")]
pub struct UndeclaredActionError {
pub(crate) action: Arc<ast::EntityUID>,
}
impl Diagnostic for UndeclaredActionError {
impl_diagnostic_from_method_on_field!(action, loc);
}
impl UndeclaredActionError {
pub fn action(&self) -> &ast::EntityUID {
&self.action
}
}
#[derive(Debug, Error)]
#[error("principal type `{principal_ty}` is not declared in the schema")]
pub struct UndeclaredPrincipalTypeError {
pub(crate) principal_ty: ast::EntityType,
}
impl Diagnostic for UndeclaredPrincipalTypeError {
impl_diagnostic_from_method_on_field!(principal_ty, loc);
}
impl UndeclaredPrincipalTypeError {
pub fn principal_ty(&self) -> &ast::EntityType {
&self.principal_ty
}
}
#[derive(Debug, Error)]
#[error("resource type `{resource_ty}` is not declared in the schema")]
pub struct UndeclaredResourceTypeError {
pub(crate) resource_ty: ast::EntityType,
}
impl Diagnostic for UndeclaredResourceTypeError {
impl_diagnostic_from_method_on_field!(resource_ty, loc);
}
impl UndeclaredResourceTypeError {
pub fn resource_ty(&self) -> &ast::EntityType {
&self.resource_ty
}
}
#[derive(Debug, Error)]
#[error("principal type `{principal_ty}` is not valid for `{action}`")]
pub struct InvalidPrincipalTypeError {
pub(crate) principal_ty: ast::EntityType,
pub(crate) action: Arc<ast::EntityUID>,
pub(crate) valid_principal_tys: Vec<ast::EntityType>,
}
impl Diagnostic for InvalidPrincipalTypeError {
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
Some(Box::new(invalid_principal_type_help(
&self.valid_principal_tys,
&self.action,
)))
}
impl_diagnostic_from_method_on_field!(principal_ty, loc);
}
fn invalid_principal_type_help(
valid_principal_tys: &[ast::EntityType],
action: &ast::EntityUID,
) -> String {
if valid_principal_tys.is_empty() {
format!("no principal types are valid for `{action}`")
} else {
format!(
"valid principal types for `{action}`: {}",
valid_principal_tys
.iter()
.sorted_unstable()
.map(|et| format!("`{et}`"))
.join(", ")
)
}
}
impl InvalidPrincipalTypeError {
pub fn principal_ty(&self) -> &ast::EntityType {
&self.principal_ty
}
pub fn action(&self) -> &ast::EntityUID {
&self.action
}
pub fn valid_principal_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
self.valid_principal_tys.iter()
}
}
#[derive(Debug, Error)]
#[error("resource type `{resource_ty}` is not valid for `{action}`")]
pub struct InvalidResourceTypeError {
pub(super) resource_ty: ast::EntityType,
pub(super) action: Arc<ast::EntityUID>,
pub(super) valid_resource_tys: Vec<ast::EntityType>,
}
impl Diagnostic for InvalidResourceTypeError {
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
Some(Box::new(invalid_resource_type_help(
&self.valid_resource_tys,
&self.action,
)))
}
impl_diagnostic_from_method_on_field!(resource_ty, loc);
}
fn invalid_resource_type_help(
valid_resource_tys: &[ast::EntityType],
action: &ast::EntityUID,
) -> String {
if valid_resource_tys.is_empty() {
format!("no resource types are valid for `{action}`")
} else {
format!(
"valid resource types for `{action}`: {}",
valid_resource_tys
.iter()
.sorted_unstable()
.map(|et| format!("`{et}`"))
.join(", ")
)
}
}
impl InvalidResourceTypeError {
pub fn resource_ty(&self) -> &ast::EntityType {
&self.resource_ty
}
pub fn action(&self) -> &ast::EntityUID {
&self.action
}
pub fn valid_resource_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
self.valid_resource_tys.iter()
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("context `{}` is not valid for `{action}`", ast::BoundedToString::to_string_bounded(.context, BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR))]
pub struct InvalidContextError {
pub(crate) context: ast::Context,
pub(crate) action: Arc<ast::EntityUID>,
}
const BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR: usize = 5;
impl InvalidContextError {
pub fn context(&self) -> &ast::Context {
&self.context
}
pub fn action(&self) -> &ast::EntityUID {
&self.action
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextSchema(
crate::validator::types::Type,
);
impl entities::ContextSchema for ContextSchema {
fn context_type(&self) -> entities::SchemaType {
#[expect(
clippy::expect_used,
reason = "By `ContextSchema` invariant, `self.0` is representable as a schema type"
)]
self.0
.clone()
.try_into()
.expect("failed to convert validator type into Core SchemaType")
}
}
pub fn context_schema_for_action(
schema: &ValidatorSchema,
action: &ast::EntityUID,
) -> Option<ContextSchema> {
schema.context_type(action).cloned().map(ContextSchema)
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_utils::{expect_err, ExpectedErrorMessageBuilder};
use ast::{Context, Value};
use cool_asserts::assert_matches;
use serde_json::json;
#[track_caller]
fn schema_with_enums() -> ValidatorSchema {
let src = r#"
entity Fruit enum ["🍉", "🍓", "🍒"];
entity People;
action "eat" appliesTo {
principal: [People],
resource: [Fruit],
context: {
fruit?: Fruit,
}
};
"#;
ValidatorSchema::from_cedarschema_str(src, Extensions::none())
.expect("should be a valid schema")
.0
}
fn schema() -> ValidatorSchema {
let src = json!(
{ "": {
"entityTypes": {
"User": {
"memberOfTypes": [ "Group" ]
},
"Group": {
"memberOfTypes": []
},
"Photo": {
"memberOfTypes": [ "Album" ]
},
"Album": {
"memberOfTypes": []
}
},
"actions": {
"view_photo": {
"appliesTo": {
"principalTypes": ["User", "Group"],
"resourceTypes": ["Photo"]
}
},
"edit_photo": {
"appliesTo": {
"principalTypes": ["User", "Group"],
"resourceTypes": ["Photo"],
"context": {
"type": "Record",
"attributes": {
"admin_approval": {
"type": "Boolean",
"required": true,
}
}
}
}
}
}
}});
ValidatorSchema::from_json_value(src, Extensions::all_available())
.expect("failed to create ValidatorSchema")
}
#[test]
fn success_concrete_request_no_context() {
assert_matches!(
ast::Request::new(
(
ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
None
),
(
ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
None
),
(
ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
None
),
ast::Context::empty(),
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
);
}
#[test]
fn success_concrete_request_with_context() {
assert_matches!(
ast::Request::new(
(
ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
None
),
(
ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
None
),
(
ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
None
),
ast::Context::from_pairs(
[("admin_approval".into(), ast::RestrictedExpr::val(true))],
Extensions::all_available()
)
.unwrap(),
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
);
}
#[test]
fn success_principal_unknown() {
assert_matches!(
ast::Request::new_with_unknowns(
ast::EntityUIDEntry::unknown(),
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
None,
),
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
None,
),
Some(ast::Context::empty()),
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
);
}
#[test]
fn success_action_unknown() {
assert_matches!(
ast::Request::new_with_unknowns(
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
None,
),
ast::EntityUIDEntry::unknown(),
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
None,
),
Some(ast::Context::empty()),
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
);
}
#[test]
fn success_resource_unknown() {
assert_matches!(
ast::Request::new_with_unknowns(
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
None,
),
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
None,
),
ast::EntityUIDEntry::unknown(),
Some(ast::Context::empty()),
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
);
}
#[test]
fn success_context_unknown() {
assert_matches!(
ast::Request::new_with_unknowns(
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
None,
),
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
None,
),
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
None,
),
None,
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
)
}
#[test]
fn success_everything_unspecified() {
assert_matches!(
ast::Request::new_with_unknowns(
ast::EntityUIDEntry::unknown(),
ast::EntityUIDEntry::unknown(),
ast::EntityUIDEntry::unknown(),
None,
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
);
}
#[test]
fn success_unknown_action_but_invalid_types() {
assert_matches!(
ast::Request::new_with_unknowns(
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
None,
),
ast::EntityUIDEntry::unknown(),
ast::EntityUIDEntry::known(
ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
None,
),
None,
Some(&schema()),
Extensions::all_available(),
),
Ok(_)
);
}
#[test]
fn action_not_declared() {
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
ast::Context::empty(),
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"request's action `Action::"destroy"` is not declared in the schema"#).build());
}
);
}
#[test]
fn principal_type_not_declared() {
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
ast::Context::empty(),
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("principal type `Foo` is not declared in the schema").build());
}
);
}
#[test]
fn resource_type_not_declared() {
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(), None),
ast::Context::empty(),
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("resource type `Foo` is not declared in the schema").build());
}
);
}
#[test]
fn principal_type_invalid() {
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
ast::Context::empty(),
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"principal type `Album` is not valid for `Action::"view_photo"`"#)
.help(r#"valid principal types for `Action::"view_photo"`: `Group`, `User`"#)
.build(),
);
}
);
}
#[test]
fn resource_type_invalid() {
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(), None),
ast::Context::empty(),
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"resource type `Group` is not valid for `Action::"view_photo"`"#)
.help(r#"valid resource types for `Action::"view_photo"`: `Photo`"#)
.build(),
);
}
);
}
#[test]
fn context_missing_attribute() {
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
ast::Context::empty(),
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{}` is not valid for `Action::"edit_photo"`"#).build());
}
);
}
#[test]
fn context_extra_attribute() {
let context_with_extra_attr = ast::Context::from_pairs(
[
("admin_approval".into(), ast::RestrictedExpr::val(true)),
("extra".into(), ast::RestrictedExpr::val(42)),
],
Extensions::all_available(),
)
.unwrap();
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
context_with_extra_attr,
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, extra: 42}` is not valid for `Action::"edit_photo"`"#).build());
}
);
}
#[test]
fn context_attribute_wrong_type() {
let context_with_wrong_type_attr = ast::Context::from_pairs(
[(
"admin_approval".into(),
ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
)],
Extensions::all_available(),
)
.unwrap();
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
context_with_wrong_type_attr,
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true]}` is not valid for `Action::"edit_photo"`"#).build());
}
);
}
#[test]
fn context_attribute_heterogeneous_set() {
let context_with_heterogeneous_set = ast::Context::from_pairs(
[(
"admin_approval".into(),
ast::RestrictedExpr::set([
ast::RestrictedExpr::val(true),
ast::RestrictedExpr::val(-1001),
]),
)],
Extensions::all_available(),
)
.unwrap();
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
context_with_heterogeneous_set,
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true, -1001]}` is not valid for `Action::"edit_photo"`"#).build());
}
);
}
#[test]
fn context_large() {
let large_context_with_extra_attributes = ast::Context::from_pairs(
[
("admin_approval".into(), ast::RestrictedExpr::val(true)),
("extra1".into(), ast::RestrictedExpr::val(false)),
("also extra".into(), ast::RestrictedExpr::val("spam")),
(
"extra2".into(),
ast::RestrictedExpr::set([ast::RestrictedExpr::val(-100)]),
),
(
"extra3".into(),
ast::RestrictedExpr::val(
ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
),
),
("extra4".into(), ast::RestrictedExpr::val("foobar")),
],
Extensions::all_available(),
)
.unwrap();
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
large_context_with_extra_attributes,
Some(&schema()),
Extensions::all_available(),
),
Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, "also extra": "spam", extra1: false, extra2: [-100], extra3: User::"alice", .. }` is not valid for `Action::"edit_photo"`"#).build(),
);
}
);
}
#[test]
fn enumerated_entity_type() {
assert_matches!(
ast::Request::new(
(
ast::EntityUID::with_eid_and_type("People", "😋").unwrap(),
None
),
(
ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(),
None
),
(
ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(),
None
),
Context::empty(),
Some(&schema_with_enums()),
Extensions::none(),
),
Ok(_)
);
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Fruit", "🥝").unwrap(), None),
Context::empty(),
Some(&schema_with_enums()),
Extensions::none(),
),
Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥝"` is of an enumerated entity type, but `"🥝"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
.build(),
);
}
);
assert_matches!(
ast::Request::new(
(ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
(ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(), None),
Context::from_pairs([("fruit".into(), (Value::from(ast::EntityUID::with_eid_and_type("Fruit", "🥭").unwrap())).into())], Extensions::none()).expect("should be a valid context"),
Some(&schema_with_enums()),
Extensions::none(),
),
Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥭"` is of an enumerated entity type, but `"🥭"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
.build(),
);
}
);
}
}