use crate::ast::*;
use crate::extensions::Extensions;
use crate::transitive_closure::{compute_tc, enforce_tc_and_dag};
use std::collections::{hash_map, HashMap};
use std::sync::Arc;
use serde::Serialize;
use serde_with::serde_as;
pub mod conformance;
pub mod err;
pub mod json;
use json::err::JsonSerializationError;
pub use json::{
AllEntitiesNoAttrsSchema, AttributeType, CedarValueJson, ContextJsonParser, ContextSchema,
EntityJson, EntityJsonParser, EntityTypeDescription, EntityUidJson, FnAndArg, NoEntitiesSchema,
NoStaticContext, Schema, SchemaType, TypeAndId,
};
use conformance::EntitySchemaConformanceChecker;
use err::*;
#[serde_as]
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Entities {
#[serde_as(as = "Vec<(_, _)>")]
entities: HashMap<EntityUID, Arc<Entity>>,
#[serde(default)]
#[serde(skip_deserializing)]
#[serde(skip_serializing)]
mode: Mode,
}
impl Entities {
pub fn new() -> Self {
Self {
entities: HashMap::new(),
mode: Mode::default(),
}
}
#[cfg(feature = "partial-eval")]
pub fn partial(self) -> Self {
Self {
entities: self.entities,
mode: Mode::Partial,
}
}
pub fn entity(&self, uid: &EntityUID) -> Dereference<'_, Entity> {
match self.entities.get(uid) {
Some(e) => Dereference::Data(e),
None => match self.mode {
Mode::Concrete => Dereference::NoSuchEntity,
#[cfg(feature = "partial-eval")]
Mode::Partial => Dereference::Residual(Expr::unknown(Unknown::new_with_type(
format!("{uid}"),
Type::Entity {
ty: uid.entity_type().clone(),
},
))),
},
}
}
pub fn iter(&self) -> impl Iterator<Item = &Entity> {
self.entities.values().map(|e| e.as_ref())
}
pub fn add_entities(
mut self,
collection: impl IntoIterator<Item = Arc<Entity>>,
schema: Option<&impl Schema>,
tc_computation: TCComputation,
extensions: &Extensions<'_>,
) -> Result<Self> {
let checker = schema.map(|schema| EntitySchemaConformanceChecker::new(schema, extensions));
for entity in collection.into_iter() {
if let Some(checker) = checker.as_ref() {
checker.validate_entity(&entity)?;
}
match self.entities.entry(entity.uid().clone()) {
hash_map::Entry::Occupied(_) => {
return Err(EntitiesError::duplicate(entity.uid().clone()))
}
hash_map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(entity);
}
}
}
match tc_computation {
TCComputation::AssumeAlreadyComputed => (),
TCComputation::EnforceAlreadyComputed => enforce_tc_and_dag(&self.entities)?,
TCComputation::ComputeNow => compute_tc(&mut self.entities, true)?,
};
Ok(self)
}
pub fn from_entities(
entities: impl IntoIterator<Item = Entity>,
schema: Option<&impl Schema>,
tc_computation: TCComputation,
extensions: &Extensions<'_>,
) -> Result<Self> {
let mut entity_map = create_entity_map(entities.into_iter().map(Arc::new))?;
if let Some(schema) = schema {
let checker = EntitySchemaConformanceChecker::new(schema, extensions);
for entity in entity_map.values() {
if !entity.uid().entity_type().is_action() {
checker.validate_entity(entity)?;
}
}
}
match tc_computation {
TCComputation::AssumeAlreadyComputed => {}
TCComputation::EnforceAlreadyComputed => {
enforce_tc_and_dag(&entity_map)?;
}
TCComputation::ComputeNow => {
compute_tc(&mut entity_map, true)?;
}
}
if let Some(schema) = schema {
let checker = EntitySchemaConformanceChecker::new(schema, extensions);
for entity in entity_map.values() {
if entity.uid().entity_type().is_action() {
checker.validate_entity(entity)?;
}
}
entity_map.extend(
schema
.action_entities()
.into_iter()
.map(|e: Arc<Entity>| (e.uid().clone(), e)),
);
}
Ok(Self {
entities: entity_map,
mode: Mode::default(),
})
}
pub fn to_json_value(&self) -> Result<serde_json::Value> {
let ejsons: Vec<EntityJson> = self.to_ejsons()?;
serde_json::to_value(ejsons)
.map_err(JsonSerializationError::from)
.map_err(Into::into)
}
pub fn write_to_json(&self, f: impl std::io::Write) -> Result<()> {
let ejsons: Vec<EntityJson> = self.to_ejsons()?;
serde_json::to_writer_pretty(f, &ejsons).map_err(JsonSerializationError::from)?;
Ok(())
}
fn to_ejsons(&self) -> Result<Vec<EntityJson>> {
self.entities
.values()
.map(Arc::as_ref)
.map(EntityJson::from_entity)
.collect::<std::result::Result<_, JsonSerializationError>>()
.map_err(Into::into)
}
fn get_entities_by_entity_type(&self) -> HashMap<EntityType, Vec<&Entity>> {
let mut entities_by_type: HashMap<EntityType, Vec<&Entity>> = HashMap::new();
for entity in self.iter() {
let euid = entity.uid();
let entity_type = euid.entity_type();
if let Some(entities) = entities_by_type.get_mut(entity_type) {
entities.push(entity);
} else {
entities_by_type.insert(entity_type.clone(), Vec::from([entity]));
}
}
entities_by_type
}
pub fn to_dot_str(&self) -> String {
let mut dot_str = String::new();
dot_str.push_str("strict digraph {\n\tordering=\"out\"\n\tnode[shape=box]\n");
fn to_dot_id(v: &impl std::fmt::Display) -> String {
format!("\"{}\"", v.to_string().escape_debug())
}
let entities_by_type = self.get_entities_by_entity_type();
for (et, entities) in entities_by_type {
dot_str.push_str(&format!(
"\tsubgraph \"cluster_{et}\" {{\n\t\tlabel={}\n",
to_dot_id(&et)
));
for entity in entities {
let euid = to_dot_id(&entity.uid());
let label = format!(r#"[label={}]"#, to_dot_id(&entity.uid().eid().escaped()));
dot_str.push_str(&format!("\t\t{euid} {label}\n"));
}
dot_str.push_str("\t}\n");
}
for entity in self.iter() {
for ancestor in entity.ancestors() {
dot_str.push_str(&format!(
"\t{} -> {}\n",
to_dot_id(&entity.uid()),
to_dot_id(&ancestor)
));
}
}
dot_str.push_str("}\n");
dot_str
}
}
fn create_entity_map(
es: impl Iterator<Item = Arc<Entity>>,
) -> Result<HashMap<EntityUID, Arc<Entity>>> {
let mut map = HashMap::new();
for e in es {
match map.entry(e.uid().clone()) {
hash_map::Entry::Occupied(_) => return Err(EntitiesError::duplicate(e.uid().clone())),
hash_map::Entry::Vacant(v) => {
v.insert(e);
}
};
}
Ok(map)
}
impl IntoIterator for Entities {
type Item = Entity;
type IntoIter = std::iter::Map<
std::collections::hash_map::IntoValues<EntityUID, Arc<Entity>>,
fn(Arc<Entity>) -> Entity,
>;
fn into_iter(self) -> Self::IntoIter {
self.entities.into_values().map(Arc::unwrap_or_clone)
}
}
impl std::fmt::Display for Entities {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.entities.is_empty() {
write!(f, "<empty Entities>")
} else {
for e in self.entities.values() {
writeln!(f, "{e}")?;
}
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub enum Dereference<'a, T> {
NoSuchEntity,
Residual(Expr),
Data(&'a T),
}
impl<'a, T> Dereference<'a, T>
where
T: std::fmt::Debug,
{
#[allow(clippy::panic)]
pub fn unwrap(self) -> &'a T {
match self {
Self::Data(e) => e,
e => panic!("unwrap() called on {:?}", e),
}
}
#[allow(clippy::panic)]
#[track_caller] pub fn expect(self, msg: &str) -> &'a T {
match self {
Self::Data(e) => e,
e => panic!("expect() called on {:?}, msg: {msg}", e),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Concrete,
#[cfg(feature = "partial-eval")]
Partial,
}
impl Default for Mode {
fn default() -> Self {
Self::Concrete
}
}
#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TCComputation {
AssumeAlreadyComputed,
EnforceAlreadyComputed,
ComputeNow,
}
#[allow(clippy::panic)]
#[cfg(test)]
#[allow(clippy::panic)]
mod json_parsing_tests {
use super::*;
use crate::{extensions::Extensions, test_utils::*, transitive_closure::TcError};
use cool_asserts::assert_matches;
#[test]
fn simple_json_parse1() {
let v = serde_json::json!(
[
{
"uid" : { "type" : "A", "id" : "b"},
"attrs" : {},
"parents" : [ { "type" : "A", "id" : "c" }]
}
]
);
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
parser
.from_json_value(v)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
}
#[test]
fn enforces_tc_fail_cycle_almost() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{
"uid" : {
"type" : "Test",
"id" : "george"
},
"attrs" : { "foo" : 3},
"parents" : [
{
"type" : "Test",
"id" : "george"
},
{
"type" : "Test",
"id" : "janet"
}
]
}
]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let err = simple_entities(&parser).add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::EnforceAlreadyComputed,
Extensions::none(),
);
let expected = TcError::missing_tc_edge(
r#"Test::"janet""#.parse().unwrap(),
r#"Test::"george""#.parse().unwrap(),
r#"Test::"janet""#.parse().unwrap(),
);
assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
assert_eq!(&expected, e.inner());
});
}
#[test]
fn enforces_tc_fail_connecting() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{
"uid" : {
"type" : "Test",
"id" : "george"
},
"attrs" : { "foo" : 3 },
"parents" : [
{
"type" : "Test",
"id" : "henry"
}
]
}
]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let err = simple_entities(&parser).add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::EnforceAlreadyComputed,
Extensions::all_available(),
);
let expected = TcError::missing_tc_edge(
r#"Test::"janet""#.parse().unwrap(),
r#"Test::"george""#.parse().unwrap(),
r#"Test::"henry""#.parse().unwrap(),
);
assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
assert_eq!(&expected, e.inner());
});
}
#[test]
fn enforces_tc_fail_missing_edge() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{
"uid" : {
"type" : "Test",
"id" : "jeff",
},
"attrs" : { "foo" : 3 },
"parents" : [
{
"type" : "Test",
"id" : "alice"
}
]
}
]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let err = simple_entities(&parser).add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::EnforceAlreadyComputed,
Extensions::all_available(),
);
let expected = TcError::missing_tc_edge(
r#"Test::"jeff""#.parse().unwrap(),
r#"Test::"alice""#.parse().unwrap(),
r#"Test::"bob""#.parse().unwrap(),
);
assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
assert_eq!(&expected, e.inner());
});
}
#[test]
fn enforces_tc_success() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{
"uid" : {
"type" : "Test",
"id" : "jeff"
},
"attrs" : { "foo" : 3 },
"parents" : [
{
"type" : "Test",
"id" : "alice"
},
{
"type" : "Test",
"id" : "bob"
}
]
}
]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let es = simple_entities(&parser)
.add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::EnforceAlreadyComputed,
Extensions::all_available(),
)
.unwrap();
let euid = r#"Test::"jeff""#.parse().unwrap();
let jeff = es.entity(&euid).unwrap();
assert!(jeff.is_descendant_of(&r#"Test::"alice""#.parse().unwrap()));
assert!(jeff.is_descendant_of(&r#"Test::"bob""#.parse().unwrap()));
assert!(!jeff.is_descendant_of(&r#"Test::"george""#.parse().unwrap()));
simple_entities_still_sane(&es);
}
#[test]
fn adds_extends_tc_connecting() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{
"uid" : {
"type" : "Test",
"id" : "george"
},
"attrs" : { "foo" : 3},
"parents" : [
{
"type" : "Test",
"id" : "henry"
}
]
}
]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let es = simple_entities(&parser)
.add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
)
.unwrap();
let euid = r#"Test::"george""#.parse().unwrap();
let jeff = es.entity(&euid).unwrap();
assert!(jeff.is_descendant_of(&r#"Test::"henry""#.parse().unwrap()));
let alice = es.entity(&r#"Test::"janet""#.parse().unwrap()).unwrap();
assert!(alice.is_descendant_of(&r#"Test::"henry""#.parse().unwrap()));
simple_entities_still_sane(&es);
}
#[test]
fn adds_extends_tc() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{
"uid" : {
"type" : "Test",
"id" : "jeff"
},
"attrs" : {
"foo" : 3
},
"parents" : [
{
"type" : "Test",
"id" : "alice"
}
]
}
]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let es = simple_entities(&parser)
.add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
)
.unwrap();
let euid = r#"Test::"jeff""#.parse().unwrap();
let jeff = es.entity(&euid).unwrap();
assert!(jeff.is_descendant_of(&r#"Test::"alice""#.parse().unwrap()));
assert!(jeff.is_descendant_of(&r#"Test::"bob""#.parse().unwrap()));
simple_entities_still_sane(&es);
}
#[test]
fn adds_works() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{
"uid" : {
"type" : "Test",
"id" : "jeff"
},
"attrs" : {
"foo" : 3
},
"parents" : [
{
"type" : "Test",
"id" : "susan"
}
]
}
]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let es = simple_entities(&parser)
.add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
)
.unwrap();
let euid = r#"Test::"jeff""#.parse().unwrap();
let jeff = es.entity(&euid).unwrap();
let value = jeff.get("foo").unwrap();
assert_eq!(value, &PartialValue::from(3));
assert!(jeff.is_descendant_of(&r#"Test::"susan""#.parse().unwrap()));
simple_entities_still_sane(&es);
}
#[test]
fn add_duplicates_fail2() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([
{"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []},
{"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []}]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let err = simple_entities(&parser)
.add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
)
.err()
.unwrap();
let expected = r#"Test::"jeff""#.parse().unwrap();
assert_matches!(err, EntitiesError::Duplicate(d) => assert_eq!(d.euid(), &expected));
}
#[test]
fn add_duplicates_fail1() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let new = serde_json::json!([{"uid":{ "type": "Test", "id": "alice" }, "attrs" : {}, "parents" : []}]);
let addl_entities = parser
.iter_from_json_value(new)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.map(Arc::new);
let err = simple_entities(&parser).add_entities(
addl_entities,
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
);
let expected = r#"Test::"alice""#.parse().unwrap();
assert_matches!(err, Err(EntitiesError::Duplicate(d)) => assert_eq!(d.euid(), &expected));
}
#[test]
fn simple_entities_correct() {
let parser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
simple_entities(&parser);
}
fn simple_entities(parser: &EntityJsonParser<'_, '_>) -> Entities {
let json = serde_json::json!(
[
{
"uid" : { "type" : "Test", "id": "alice" },
"attrs" : { "bar" : 2},
"parents" : [
{
"type" : "Test",
"id" : "bob"
}
]
},
{
"uid" : { "type" : "Test", "id" : "janet"},
"attrs" : { "bar" : 2},
"parents" : [
{
"type" : "Test",
"id" : "george"
}
]
},
{
"uid" : { "type" : "Test", "id" : "bob"},
"attrs" : {},
"parents" : []
},
{
"uid" : { "type" : "Test", "id" : "henry"},
"attrs" : {},
"parents" : []
},
]
);
parser
.from_json_value(json)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
}
fn simple_entities_still_sane(e: &Entities) {
let bob = r#"Test::"bob""#.parse().unwrap();
let alice = e.entity(&r#"Test::"alice""#.parse().unwrap()).unwrap();
let bar = alice.get("bar").unwrap();
assert_eq!(bar, &PartialValue::from(2));
assert!(alice.is_descendant_of(&bob));
let bob = e.entity(&bob).unwrap();
assert!(bob.ancestors().collect::<Vec<_>>().is_empty());
}
#[cfg(feature = "partial-eval")]
#[test]
fn basic_partial() {
let json = serde_json::json!(
[
{
"uid" : {
"type" : "test_entity_type",
"id" : "alice"
},
"attrs": {},
"parents": [
{
"type" : "test_entity_type",
"id" : "jane"
}
]
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "jane"
},
"attrs": {},
"parents": [
{
"type" : "test_entity_type",
"id" : "bob",
}
]
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "bob"
},
"attrs": {},
"parents": []
}
]
);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let es = eparser
.from_json_value(json)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
.partial();
let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
let janice = es.entity(&EntityUID::with_eid("janice"));
assert_matches!(janice, Dereference::Residual(_));
}
#[test]
fn basic() {
let json = serde_json::json!([
{
"uid" : {
"type" : "test_entity_type",
"id" : "alice"
},
"attrs": {},
"parents": [
{
"type" : "test_entity_type",
"id" : "jane"
}
]
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "jane"
},
"attrs": {},
"parents": [
{
"type" : "test_entity_type",
"id" : "bob"
}
]
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "bob"
},
"attrs": {},
"parents": []
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "josephine"
},
"attrs": {},
"parents": [],
"tags": {}
}
]
);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let es = eparser
.from_json_value(json)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
}
#[test]
fn no_expr_escapes1() {
let json = serde_json::json!(
[
{
"uid" : r#"test_entity_type::"Alice""#,
"attrs": {
"bacon": "eggs",
"pancakes": [1, 2, 3],
"waffles": { "key": "value" },
"toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
"12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
"a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
},
"parents": [
{ "__entity": { "type" : "test_entity_type", "id" : "bob"} },
{ "__entity": { "type": "test_entity_type", "id": "catherine" } }
]
},
]);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"test_entity_type::\"Alice\""`"#)
.help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
.build()
);
});
}
#[test]
fn no_expr_escapes2() {
let json = serde_json::json!(
[
{
"uid" : {
"__expr" :
r#"test_entity_type::"Alice""#
},
"attrs": {
"bacon": "eggs",
"pancakes": [1, 2, 3],
"waffles": { "key": "value" },
"toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
"12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
"a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
},
"parents": [
{ "__entity": { "type" : "test_entity_type", "id" : "bob"} },
{ "__entity": { "type": "test_entity_type", "id": "catherine" } }
]
}
]);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in uid field of <unknown entity>, the `__expr` escape is no longer supported"#)
.help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
.build()
);
});
}
#[test]
fn no_expr_escapes3() {
let json = serde_json::json!(
[
{
"uid" : {
"type" : "test_entity_type",
"id" : "Alice"
},
"attrs": {
"bacon": "eggs",
"pancakes": { "__expr" : "[1,2,3]" },
"waffles": { "key": "value" },
"toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
"12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
"a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
},
"parents": [
{ "__entity": { "type" : "test_entity_type", "id" : "bob"} },
{ "__entity": { "type": "test_entity_type", "id": "catherine" } }
]
}
]);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in attribute `pancakes` on `test_entity_type::"Alice"`, the `__expr` escape is no longer supported"#)
.help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
.build()
);
});
}
#[test]
fn no_expr_escapes4() {
let json = serde_json::json!(
[
{
"uid" : {
"type" : "test_entity_type",
"id" : "Alice"
},
"attrs": {
"bacon": "eggs",
"waffles": { "key": "value" },
"12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
"a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
},
"parents": [
{ "__expr": "test_entity_type::\"Alice\"" },
{ "__entity": { "type": "test_entity_type", "id": "catherine" } }
]
}
]);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in parents field of `test_entity_type::"Alice"`, the `__expr` escape is no longer supported"#)
.help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
.build()
);
});
}
#[test]
fn no_expr_escapes5() {
let json = serde_json::json!(
[
{
"uid" : {
"type" : "test_entity_type",
"id" : "Alice"
},
"attrs": {
"bacon": "eggs",
"waffles": { "key": "value" },
"12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
"a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
},
"parents": [
"test_entity_type::\"bob\"",
{ "__entity": { "type": "test_entity_type", "id": "catherine" } }
]
}
]);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in parents field of `test_entity_type::"Alice"`, expected a literal entity reference, but got `"test_entity_type::\"bob\""`"#)
.help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
.build()
);
});
}
#[cfg(feature = "ipaddr")]
#[test]
fn more_escapes() {
let json = serde_json::json!(
[
{
"uid" : {
"type" : "test_entity_type",
"id" : "alice"
},
"attrs": {
"bacon": "eggs",
"pancakes": [1, 2, 3],
"waffles": { "key": "value" },
"toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
"12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
"a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
},
"parents": [
{ "__entity": { "type" : "test_entity_type", "id" : "bob"} },
{ "__entity": { "type": "test_entity_type", "id": "catherine" } }
]
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "bob"
},
"attrs": {},
"parents": []
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "catherine"
},
"attrs": {},
"parents": []
}
]
);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let es = eparser
.from_json_value(json)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
assert_eq!(alice.get("bacon"), Some(&PartialValue::from("eggs")));
assert_eq!(
alice.get("pancakes"),
Some(&PartialValue::from(vec![
Value::from(1),
Value::from(2),
Value::from(3),
])),
);
assert_eq!(
alice.get("waffles"),
Some(&PartialValue::from(Value::record(
vec![("key", Value::from("value"),)],
None
))),
);
assert_eq!(
alice.get("toast").cloned().map(RestrictedExpr::try_from),
Some(Ok(RestrictedExpr::call_extension_fn(
"decimal".parse().expect("should be a valid Name"),
vec![RestrictedExpr::val("33.47")],
))),
);
assert_eq!(
alice.get("12345"),
Some(&PartialValue::from(EntityUID::with_eid("bob"))),
);
assert_eq!(
alice.get("a b c").cloned().map(RestrictedExpr::try_from),
Some(Ok(RestrictedExpr::call_extension_fn(
"ip".parse().expect("should be a valid Name"),
vec![RestrictedExpr::val("222.222.222.0/24")],
))),
);
assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
assert!(alice.is_descendant_of(&EntityUID::with_eid("catherine")));
}
#[test]
fn implicit_and_explicit_escapes() {
let json = serde_json::json!(
[
{
"uid": { "type" : "test_entity_type", "id" : "alice" },
"attrs": {},
"parents": [
{ "type" : "test_entity_type", "id" : "bob" },
{ "__entity": { "type": "test_entity_type", "id": "charles" } },
{ "type": "test_entity_type", "id": "elaine" }
]
},
{
"uid": { "__entity": { "type": "test_entity_type", "id": "bob" }},
"attrs": {},
"parents": []
},
{
"uid" : {
"type" : "test_entity_type",
"id" : "charles"
},
"attrs" : {},
"parents" : []
},
{
"uid": { "type": "test_entity_type", "id": "darwin" },
"attrs": {},
"parents": []
},
{
"uid": { "type": "test_entity_type", "id": "elaine" },
"attrs": {},
"parents" : [
{
"type" : "test_entity_type",
"id" : "darwin"
}
]
}
]
);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let es = eparser
.from_json_value(json)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
let bob = es.entity(&EntityUID::with_eid("bob")).unwrap();
let charles = es.entity(&EntityUID::with_eid("charles")).unwrap();
let darwin = es.entity(&EntityUID::with_eid("darwin")).unwrap();
let elaine = es.entity(&EntityUID::with_eid("elaine")).unwrap();
assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
assert!(alice.is_descendant_of(&EntityUID::with_eid("charles")));
assert!(alice.is_descendant_of(&EntityUID::with_eid("darwin")));
assert!(alice.is_descendant_of(&EntityUID::with_eid("elaine")));
assert_eq!(bob.ancestors().next(), None);
assert_eq!(charles.ancestors().next(), None);
assert_eq!(darwin.ancestors().next(), None);
assert!(elaine.is_descendant_of(&EntityUID::with_eid("darwin")));
assert!(!elaine.is_descendant_of(&EntityUID::with_eid("bob")));
}
#[test]
fn uid_failures() {
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let json = serde_json::json!(
[
{
"uid": "hello",
"attrs": {},
"parents": []
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"hello"`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": "\"hello\"",
"attrs": {},
"parents": []
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"\"hello\""`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "spam": "eggs" },
"attrs": {},
"parents": []
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"spam":"eggs","type":"foo"}`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": {},
"parents": "foo::\"help\""
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"invalid type: string "foo::\"help\"", expected a sequence"#
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": {},
"parents": [
"foo::\"help\"",
{ "__extn": { "fn": "ip", "arg": "222.222.222.0" } }
]
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `"foo::\"help\""`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
}
#[test]
fn null_failures() {
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let json = serde_json::json!(
[
{
"uid": null,
"attrs": {},
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
"in uid field of <unknown entity>, expected a literal entity reference, but got `null`",
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": null, "id": "bar" },
"attrs": {},
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":"bar","type":null}`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": null },
"attrs": {},
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": null,
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
"invalid type: null, expected a map"
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": null },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": { "subattr": null } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": [ 3, null ] },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": [ 3, { "subattr" : null } ] },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "__extn": { "fn": null, "args": [] } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "__extn": { "fn": "ip", "args": null } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "__extn": { "fn": "ip", "args": [ null ] } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": null,
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
"invalid type: null, expected a sequence"
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": [ null ],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": [ { "type": "foo", "id": null } ],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": [ { "type": "foo", "id": "parent" }, null ],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
).help(
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
).build());
});
}
fn roundtrip(entities: &Entities) -> Result<Entities> {
let mut buf = Vec::new();
entities.write_to_json(&mut buf)?;
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
eparser.from_json_str(&String::from_utf8(buf).expect("should be valid UTF-8"))
}
fn test_entities() -> (Entity, Entity, Entity, Entity) {
(
Entity::with_uid(EntityUID::with_eid("test_principal")),
Entity::with_uid(EntityUID::with_eid("test_action")),
Entity::with_uid(EntityUID::with_eid("test_resource")),
Entity::with_uid(EntityUID::with_eid("test")),
)
}
#[test]
fn json_roundtripping() {
let empty_entities = Entities::new();
assert_eq!(
empty_entities,
roundtrip(&empty_entities).expect("should roundtrip without errors")
);
let (e0, e1, e2, e3) = test_entities();
let entities = Entities::from_entities(
[e0, e1, e2, e3],
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::none(),
)
.expect("Failed to construct entities");
assert_eq!(
entities,
roundtrip(&entities).expect("should roundtrip without errors")
);
let complicated_entity = Entity::new(
EntityUID::with_eid("complicated"),
[
("foo".into(), RestrictedExpr::val(false)),
("bar".into(), RestrictedExpr::val(-234)),
("ham".into(), RestrictedExpr::val(r"a b c * / ? \")),
(
"123".into(),
RestrictedExpr::val(EntityUID::with_eid("mom")),
),
(
"set".into(),
RestrictedExpr::set([
RestrictedExpr::val(0),
RestrictedExpr::val(EntityUID::with_eid("pancakes")),
RestrictedExpr::val("mmm"),
]),
),
(
"rec".into(),
RestrictedExpr::record([
("nested".into(), RestrictedExpr::val("attr")),
(
"another".into(),
RestrictedExpr::val(EntityUID::with_eid("foo")),
),
])
.unwrap(),
),
(
"src_ip".into(),
RestrictedExpr::call_extension_fn(
"ip".parse().expect("should be a valid Name"),
vec![RestrictedExpr::val("222.222.222.222")],
),
),
],
[
EntityUID::with_eid("parent1"),
EntityUID::with_eid("parent2"),
]
.into_iter()
.collect(),
[
("foo".into(), RestrictedExpr::val(2345)),
("bar".into(), RestrictedExpr::val(-1)),
(
"pancakes".into(),
RestrictedExpr::val(EntityUID::with_eid("pancakes")),
),
],
Extensions::all_available(),
)
.unwrap();
let entities = Entities::from_entities(
[
complicated_entity,
Entity::with_uid(EntityUID::with_eid("parent1")),
Entity::with_uid(EntityUID::with_eid("parent2")),
],
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
)
.expect("Failed to construct entities");
assert_eq!(
entities,
roundtrip(&entities).expect("should roundtrip without errors")
);
let oops_entity = Entity::new(
EntityUID::with_eid("oops"),
[(
"oops".into(),
RestrictedExpr::record([("__entity".into(), RestrictedExpr::val("hi"))]).unwrap(),
)],
[
EntityUID::with_eid("parent1"),
EntityUID::with_eid("parent2"),
]
.into_iter()
.collect(),
[],
Extensions::all_available(),
)
.unwrap();
let entities = Entities::from_entities(
[
oops_entity,
Entity::with_uid(EntityUID::with_eid("parent1")),
Entity::with_uid(EntityUID::with_eid("parent2")),
],
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
)
.expect("Failed to construct entities");
assert_matches!(
roundtrip(&entities),
Err(EntitiesError::Serialization(JsonSerializationError::ReservedKey(reserved))) if reserved.key().as_ref() == "__entity"
);
}
#[test]
fn bad_action_parent() {
let json = serde_json::json!(
[
{
"uid": { "type": "XYZ::Action", "id": "view" },
"attrs": {},
"parents": [
{ "type": "User", "id": "alice" }
]
}
]
);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"action `XYZ::Action::"view"` has a non-action parent `User::"alice"`"#)
.help(r#"parents of actions need to have type `Action` themselves, perhaps namespaced"#)
.build()
);
});
}
#[test]
fn not_bad_action_parent() {
let json = serde_json::json!(
[
{
"uid": { "type": "User", "id": "alice" },
"attrs": {},
"parents": [
{ "type": "XYZ::Action", "id": "view" },
]
}
]
);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
eparser
.from_json_value(json)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
}
#[test]
fn duplicate_keys() {
let json = r#"
[
{
"uid": { "type": "User", "id": "alice "},
"attrs": {
"foo": {
"hello": "goodbye",
"bar": 2,
"spam": "eggs",
"bar": 3
}
},
"parents": []
}
]
"#;
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
assert_matches!(eparser.from_json_str(json), Err(e) => {
expect_err(
json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"the key `bar` occurs two or more times in the same JSON object at line 11 column 25"#)
.build()
);
});
}
}
#[allow(clippy::panic)]
#[cfg(test)]
#[allow(clippy::panic)]
mod entities_tests {
use super::*;
#[test]
fn empty_entities() {
let e = Entities::new();
let es = e.iter().collect::<Vec<_>>();
assert!(es.is_empty(), "This vec should be empty");
}
fn test_entities() -> (Entity, Entity, Entity, Entity) {
(
Entity::with_uid(EntityUID::with_eid("test_principal")),
Entity::with_uid(EntityUID::with_eid("test_action")),
Entity::with_uid(EntityUID::with_eid("test_resource")),
Entity::with_uid(EntityUID::with_eid("test")),
)
}
#[test]
fn test_iter() {
let (e0, e1, e2, e3) = test_entities();
let v = vec![e0.clone(), e1.clone(), e2.clone(), e3.clone()];
let es = Entities::from_entities(
v,
None::<&NoEntitiesSchema>,
TCComputation::ComputeNow,
Extensions::all_available(),
)
.expect("Failed to construct entities");
let es_v = es.iter().collect::<Vec<_>>();
assert!(es_v.len() == 4, "All entities should be in the vec");
assert!(es_v.contains(&&e0));
assert!(es_v.contains(&&e1));
assert!(es_v.contains(&&e2));
assert!(es_v.contains(&&e3));
}
#[test]
fn test_enforce_already_computed_fail() {
let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
let e3 = Entity::with_uid(EntityUID::with_eid("c"));
e1.add_ancestor(EntityUID::with_eid("b"));
e2.add_ancestor(EntityUID::with_eid("c"));
let es = Entities::from_entities(
vec![e1, e2, e3],
None::<&NoEntitiesSchema>,
TCComputation::EnforceAlreadyComputed,
Extensions::all_available(),
);
match es {
Ok(_) => panic!("Was not transitively closed!"),
Err(EntitiesError::TransitiveClosureError(_)) => (),
Err(_) => panic!("Wrong Error!"),
};
}
#[test]
fn test_enforce_already_computed_succeed() {
let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
let e3 = Entity::with_uid(EntityUID::with_eid("c"));
e1.add_ancestor(EntityUID::with_eid("b"));
e1.add_ancestor(EntityUID::with_eid("c"));
e2.add_ancestor(EntityUID::with_eid("c"));
Entities::from_entities(
vec![e1, e2, e3],
None::<&NoEntitiesSchema>,
TCComputation::EnforceAlreadyComputed,
Extensions::all_available(),
)
.expect("Should have succeeded");
}
}
#[allow(clippy::panic)]
#[cfg(test)]
mod schema_based_parsing_tests {
use super::json::NullEntityTypeDescription;
use super::*;
use crate::extensions::Extensions;
use crate::test_utils::*;
use cool_asserts::assert_matches;
use serde_json::json;
use smol_str::SmolStr;
use std::collections::HashSet;
use std::sync::Arc;
struct MockSchema;
impl Schema for MockSchema {
type EntityTypeDescription = MockEmployeeDescription;
type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
fn entity_type(&self, entity_type: &EntityType) -> Option<MockEmployeeDescription> {
match entity_type.to_string().as_str() {
"Employee" => Some(MockEmployeeDescription),
_ => None,
}
}
fn action(&self, action: &EntityUID) -> Option<Arc<Entity>> {
match action.to_string().as_str() {
r#"Action::"view""# => Some(Arc::new(Entity::new_with_attr_partial_value(
action.clone(),
[(SmolStr::from("foo"), PartialValue::from(34))],
[r#"Action::"readOnly""#.parse().expect("valid uid")]
.into_iter()
.collect(),
))),
r#"Action::"readOnly""# => Some(Arc::new(Entity::with_uid(
r#"Action::"readOnly""#.parse().expect("valid uid"),
))),
_ => None,
}
}
fn entity_types_with_basename<'a>(
&'a self,
basename: &'a UnreservedId,
) -> Box<dyn Iterator<Item = EntityType> + 'a> {
match basename.as_ref() {
"Employee" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
basename.clone(),
)))),
"Action" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
basename.clone(),
)))),
_ => Box::new(std::iter::empty()),
}
}
fn action_entities(&self) -> Self::ActionEntityIterator {
std::iter::empty()
}
}
struct MockSchemaNoTags;
impl Schema for MockSchemaNoTags {
type EntityTypeDescription = NullEntityTypeDescription;
type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
fn entity_type(&self, entity_type: &EntityType) -> Option<NullEntityTypeDescription> {
match entity_type.to_string().as_str() {
"Employee" => Some(NullEntityTypeDescription::new("Employee".parse().unwrap())),
_ => None,
}
}
fn action(&self, action: &EntityUID) -> Option<Arc<Entity>> {
match action.to_string().as_str() {
r#"Action::"view""# => Some(Arc::new(Entity::with_uid(
r#"Action::"view""#.parse().expect("valid uid"),
))),
_ => None,
}
}
fn entity_types_with_basename<'a>(
&'a self,
basename: &'a UnreservedId,
) -> Box<dyn Iterator<Item = EntityType> + 'a> {
match basename.as_ref() {
"Employee" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
basename.clone(),
)))),
"Action" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
basename.clone(),
)))),
_ => Box::new(std::iter::empty()),
}
}
fn action_entities(&self) -> Self::ActionEntityIterator {
std::iter::empty()
}
}
struct MockEmployeeDescription;
impl EntityTypeDescription for MockEmployeeDescription {
fn entity_type(&self) -> EntityType {
EntityType::from(Name::parse_unqualified_name("Employee").expect("valid"))
}
fn attr_type(&self, attr: &str) -> Option<SchemaType> {
let employee_ty = || SchemaType::Entity {
ty: self.entity_type(),
};
let hr_ty = || SchemaType::Entity {
ty: EntityType::from(Name::parse_unqualified_name("HR").expect("valid")),
};
match attr {
"isFullTime" => Some(SchemaType::Bool),
"numDirectReports" => Some(SchemaType::Long),
"department" => Some(SchemaType::String),
"manager" => Some(employee_ty()),
"hr_contacts" => Some(SchemaType::Set {
element_ty: Box::new(hr_ty()),
}),
"json_blob" => Some(SchemaType::Record {
attrs: [
("inner1".into(), AttributeType::required(SchemaType::Bool)),
("inner2".into(), AttributeType::required(SchemaType::String)),
(
"inner3".into(),
AttributeType::required(SchemaType::Record {
attrs: [(
"innerinner".into(),
AttributeType::required(employee_ty()),
)]
.into_iter()
.collect(),
open_attrs: false,
}),
),
]
.into_iter()
.collect(),
open_attrs: false,
}),
"home_ip" => Some(SchemaType::Extension {
name: Name::parse_unqualified_name("ipaddr").expect("valid"),
}),
"work_ip" => Some(SchemaType::Extension {
name: Name::parse_unqualified_name("ipaddr").expect("valid"),
}),
"trust_score" => Some(SchemaType::Extension {
name: Name::parse_unqualified_name("decimal").expect("valid"),
}),
"tricky" => Some(SchemaType::Record {
attrs: [
("type".into(), AttributeType::required(SchemaType::String)),
("id".into(), AttributeType::required(SchemaType::String)),
]
.into_iter()
.collect(),
open_attrs: false,
}),
_ => None,
}
}
fn tag_type(&self) -> Option<SchemaType> {
Some(SchemaType::Set {
element_ty: Box::new(SchemaType::String),
})
}
fn required_attrs(&self) -> Box<dyn Iterator<Item = SmolStr>> {
Box::new(
[
"isFullTime",
"numDirectReports",
"department",
"manager",
"hr_contacts",
"json_blob",
"home_ip",
"work_ip",
"trust_score",
]
.map(SmolStr::new)
.into_iter(),
)
}
fn allowed_parent_types(&self) -> Arc<HashSet<EntityType>> {
Arc::new(HashSet::new())
}
fn open_attributes(&self) -> bool {
false
}
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn with_and_without_schema() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": [],
"tags": {
"someTag": ["pancakes"],
},
}
]
);
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
let parsed = eparser
.from_json_value(entitiesjson.clone())
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
assert_eq!(parsed.iter().count(), 1);
let parsed = parsed
.entity(&r#"Employee::"12UA45""#.parse().unwrap())
.expect("that should be the employee id");
let home_ip = parsed.get("home_ip").expect("home_ip attr should exist");
assert_matches!(
home_ip,
&PartialValue::Value(Value {
value: ValueKind::Lit(Literal::String(_)),
..
}),
);
let trust_score = parsed
.get("trust_score")
.expect("trust_score attr should exist");
assert_matches!(
trust_score,
&PartialValue::Value(Value {
value: ValueKind::Lit(Literal::String(_)),
..
}),
);
let manager = parsed.get("manager").expect("manager attr should exist");
assert_matches!(
manager,
&PartialValue::Value(Value {
value: ValueKind::Record(_),
..
})
);
let work_ip = parsed.get("work_ip").expect("work_ip attr should exist");
assert_matches!(
work_ip,
&PartialValue::Value(Value {
value: ValueKind::Record(_),
..
})
);
let hr_contacts = parsed
.get("hr_contacts")
.expect("hr_contacts attr should exist");
assert_matches!(hr_contacts, PartialValue::Value(Value { value: ValueKind::Set(set), .. }) => {
let contact = set.iter().next().expect("should be at least one contact");
assert_matches!(contact, &Value { value: ValueKind::Record(_), .. });
});
let json_blob = parsed
.get("json_blob")
.expect("json_blob attr should exist");
assert_matches!(json_blob, PartialValue::Value(Value { value: ValueKind::Record(record), .. }) => {
let (_, inner1) = record
.iter()
.find(|(k, _)| *k == "inner1")
.expect("inner1 attr should exist");
assert_matches!(inner1, Value { value: ValueKind::Lit(Literal::Bool(_)), .. });
let (_, inner3) = record
.iter()
.find(|(k, _)| *k == "inner3")
.expect("inner3 attr should exist");
assert_matches!(inner3, Value { value: ValueKind::Record(innerrecord), .. } => {
let (_, innerinner) = innerrecord
.iter()
.find(|(k, _)| *k == "innerinner")
.expect("innerinner attr should exist");
assert_matches!(innerinner, Value { value: ValueKind::Record(_), .. });
});
});
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
let parsed = eparser
.from_json_value(entitiesjson)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
assert_eq!(parsed.iter().count(), 1);
let parsed = parsed
.entity(&r#"Employee::"12UA45""#.parse().unwrap())
.expect("that should be the employee id");
let is_full_time = parsed
.get("isFullTime")
.expect("isFullTime attr should exist");
assert_eq!(is_full_time, &PartialValue::Value(Value::from(true)),);
let some_tag = parsed
.get_tag("someTag")
.expect("someTag attr should exist");
assert_eq!(
some_tag,
&PartialValue::Value(Value::set(["pancakes".into()], None))
);
let num_direct_reports = parsed
.get("numDirectReports")
.expect("numDirectReports attr should exist");
assert_eq!(num_direct_reports, &PartialValue::Value(Value::from(3)),);
let department = parsed
.get("department")
.expect("department attr should exist");
assert_eq!(department, &PartialValue::Value(Value::from("Sales")),);
let manager = parsed.get("manager").expect("manager attr should exist");
assert_eq!(
manager,
&PartialValue::Value(Value::from(
"Employee::\"34FB87\"".parse::<EntityUID>().expect("valid")
)),
);
let hr_contacts = parsed
.get("hr_contacts")
.expect("hr_contacts attr should exist");
assert_matches!(hr_contacts, PartialValue::Value(Value { value: ValueKind::Set(set), .. }) => {
let contact = set.iter().next().expect("should be at least one contact");
assert_matches!(contact, &Value { value: ValueKind::Lit(Literal::EntityUID(_)), .. });
});
let json_blob = parsed
.get("json_blob")
.expect("json_blob attr should exist");
assert_matches!(json_blob, PartialValue::Value(Value { value: ValueKind::Record(record), .. }) => {
let (_, inner1) = record
.iter()
.find(|(k, _)| *k == "inner1")
.expect("inner1 attr should exist");
assert_matches!(inner1, Value { value: ValueKind::Lit(Literal::Bool(_)), .. });
let (_, inner3) = record
.iter()
.find(|(k, _)| *k == "inner3")
.expect("inner3 attr should exist");
assert_matches!(inner3, Value { value: ValueKind::Record(innerrecord), .. } => {
let (_, innerinner) = innerrecord
.iter()
.find(|(k, _)| *k == "innerinner")
.expect("innerinner attr should exist");
assert_matches!(innerinner, Value { value: ValueKind::Lit(Literal::EntityUID(_)), .. });
});
});
assert_eq!(
parsed.get("home_ip").cloned().map(RestrictedExpr::try_from),
Some(Ok(RestrictedExpr::call_extension_fn(
Name::parse_unqualified_name("ip").expect("valid"),
vec![RestrictedExpr::val("222.222.222.101")]
))),
);
assert_eq!(
parsed.get("work_ip").cloned().map(RestrictedExpr::try_from),
Some(Ok(RestrictedExpr::call_extension_fn(
Name::parse_unqualified_name("ip").expect("valid"),
vec![RestrictedExpr::val("2.2.2.0/24")]
))),
);
assert_eq!(
parsed
.get("trust_score")
.cloned()
.map(RestrictedExpr::try_from),
Some(Ok(RestrictedExpr::call_extension_fn(
Name::parse_unqualified_name("decimal").expect("valid"),
vec![RestrictedExpr::val("5.7")]
))),
);
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn type_mismatch_string_long() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": "3",
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"in attribute `numDirectReports` on `Employee::"12UA45"`, type mismatch: value was expected to have type long, but it actually has type string: `"3"`"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn type_mismatch_entity_record() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": "34FB87",
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in attribute `manager` on `Employee::"12UA45"`, expected a literal entity reference, but got `"34FB87"`"#)
.help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn type_mismatch_set_element() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": { "type": "HR", "id": "aaaaa" },
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in attribute `hr_contacts` on `Employee::"12UA45"`, type mismatch: value was expected to have type [`HR`], but it actually has type record: `{"id": "aaaaa", "type": "HR"}`"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn type_mismatch_entity_types() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "HR", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"in attribute `manager` on `Employee::"12UA45"`, type mismatch: value was expected to have type `Employee`, but it actually has type (entity of type `HR`): `HR::"34FB87"`"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn type_mismatch_extension_types() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": { "fn": "decimal", "arg": "3.33" },
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"in attribute `home_ip` on `Employee::"12UA45"`, type mismatch: value was expected to have type ipaddr, but it actually has type decimal: `decimal("3.33")`"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn missing_record_attr() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in attribute `json_blob` on `Employee::"12UA45"`, expected the record to have an attribute `inner2`, but it does not"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn type_mismatch_in_record_attr() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": 33,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error_starts_with("entity does not conform to the schema")
.source(r#"in attribute `json_blob` on `Employee::"12UA45"`, type mismatch: value was expected to have type bool, but it actually has type long: `33`"#)
.build()
);
});
let entitiesjson = json!(
[
{
"uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
"work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
"trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let _ = eparser
.from_json_value(entitiesjson)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
}
#[test]
fn type_mismatch_in_tag() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": [],
"tags": {
"someTag": "pancakes",
}
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
let expected_error_msg =
ExpectedErrorMessageBuilder::error_starts_with("error during entity deserialization")
.source(r#"in tag `someTag` on `Employee::"12UA45"`, type mismatch: value was expected to have type [string], but it actually has type string: `"pancakes"`"#)
.build();
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&expected_error_msg,
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn unexpected_record_attr() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
"inner4": "wat?"
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"in attribute `json_blob` on `Employee::"12UA45"`, record attribute `inner4` should not exist according to the schema"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn missing_required_attr() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"expected entity `Employee::"12UA45"` to have attribute `numDirectReports`, but it does not"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn unexpected_entity_attr() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" },
"wat": "???",
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"attribute `wat` on `Employee::"12UA45"` should not exist according to the schema"#)
.build()
);
});
}
#[test]
fn unexpected_entity_tag() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {},
"parents": [],
"tags": {
"someTag": 12,
}
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchemaNoTags),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"found a tag `someTag` on `Employee::"12UA45"`, but no tags should exist on `Employee::"12UA45"` according to the schema"#)
.build()
);
});
}
#[cfg(all(feature = "decimal", feature = "ipaddr"))]
#[test]
fn parents_wrong_type() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": [
{ "type": "Employee", "id": "34FB87" }
]
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"`Employee::"12UA45"` is not allowed to have an ancestor of type `Employee` according to the schema"#)
.build()
);
});
}
#[test]
fn undeclared_entity_type() {
let entitiesjson = json!(
[
{
"uid": { "type": "CEO", "id": "abcdef" },
"attrs": {},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"entity `CEO::"abcdef"` has type `CEO` which is not declared in the schema"#)
.build()
);
});
}
#[test]
fn undeclared_action() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "update" },
"attrs": {},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"found action entity `Action::"update"`, but it was not declared as an action in the schema"#)
.build()
);
});
}
#[test]
fn action_declared_both_places() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "view" },
"attrs": {
"foo": 34
},
"parents": [
{ "type": "Action", "id": "readOnly" }
]
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
let entities = eparser
.from_json_value(entitiesjson)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
assert_eq!(entities.iter().count(), 1);
let expected_uid = r#"Action::"view""#.parse().expect("valid uid");
let parsed_entity = match entities.entity(&expected_uid) {
Dereference::Data(e) => e,
_ => panic!("expected entity to exist and be concrete"),
};
assert_eq!(parsed_entity.uid(), &expected_uid);
}
#[test]
fn action_attr_wrong_val() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "view" },
"attrs": {
"foo": 6789
},
"parents": [
{ "type": "Action", "id": "readOnly" }
]
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
.help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
.build()
);
});
}
#[test]
fn action_attr_wrong_type() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "view" },
"attrs": {
"foo": "bar"
},
"parents": [
{ "type": "Action", "id": "readOnly" }
]
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
.help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
.build()
);
});
}
#[test]
fn action_attr_missing_in_json() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "view" },
"attrs": {},
"parents": [
{ "type": "Action", "id": "readOnly" }
]
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
.help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
.build()
);
});
}
#[test]
fn action_attr_missing_in_schema() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "view" },
"attrs": {
"foo": "bar",
"wow": false
},
"parents": [
{ "type": "Action", "id": "readOnly" }
]
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
.help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
.build()
);
});
}
#[test]
fn action_parent_missing_in_json() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "view" },
"attrs": {
"foo": 34
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
.help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
.build()
);
});
}
#[test]
fn action_parent_missing_in_schema() {
let entitiesjson = json!(
[
{
"uid": { "type": "Action", "id": "view" },
"attrs": {
"foo": 34
},
"parents": [
{ "type": "Action", "id": "readOnly" },
{ "type": "Action", "id": "coolActions" }
]
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
.help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
.build()
);
});
}
#[test]
fn namespaces() {
use std::str::FromStr;
struct MockSchema;
impl Schema for MockSchema {
type EntityTypeDescription = MockEmployeeDescription;
type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
fn entity_type(&self, entity_type: &EntityType) -> Option<MockEmployeeDescription> {
if &entity_type.to_string() == "XYZCorp::Employee" {
Some(MockEmployeeDescription)
} else {
None
}
}
fn action(&self, _action: &EntityUID) -> Option<Arc<Entity>> {
None
}
fn entity_types_with_basename<'a>(
&'a self,
basename: &'a UnreservedId,
) -> Box<dyn Iterator<Item = EntityType> + 'a> {
match basename.as_ref() {
"Employee" => Box::new(std::iter::once(EntityType::from(
Name::from_str("XYZCorp::Employee").expect("valid name"),
))),
_ => Box::new(std::iter::empty()),
}
}
fn action_entities(&self) -> Self::ActionEntityIterator {
std::iter::empty()
}
}
struct MockEmployeeDescription;
impl EntityTypeDescription for MockEmployeeDescription {
fn entity_type(&self) -> EntityType {
"XYZCorp::Employee".parse().expect("valid")
}
fn attr_type(&self, attr: &str) -> Option<SchemaType> {
match attr {
"isFullTime" => Some(SchemaType::Bool),
"department" => Some(SchemaType::String),
"manager" => Some(SchemaType::Entity {
ty: self.entity_type(),
}),
_ => None,
}
}
fn tag_type(&self) -> Option<SchemaType> {
None
}
fn required_attrs(&self) -> Box<dyn Iterator<Item = SmolStr>> {
Box::new(
["isFullTime", "department", "manager"]
.map(SmolStr::new)
.into_iter(),
)
}
fn allowed_parent_types(&self) -> Arc<HashSet<EntityType>> {
Arc::new(HashSet::new())
}
fn open_attributes(&self) -> bool {
false
}
}
let entitiesjson = json!(
[
{
"uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"department": "Sales",
"manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let eparser = EntityJsonParser::new(
Some(&MockSchema),
Extensions::all_available(),
TCComputation::ComputeNow,
);
let parsed = eparser
.from_json_value(entitiesjson)
.unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
assert_eq!(parsed.iter().count(), 1);
let parsed = parsed
.entity(&r#"XYZCorp::Employee::"12UA45""#.parse().unwrap())
.expect("that should be the employee type and id");
let is_full_time = parsed
.get("isFullTime")
.expect("isFullTime attr should exist");
assert_eq!(is_full_time, &PartialValue::from(true));
let department = parsed
.get("department")
.expect("department attr should exist");
assert_eq!(department, &PartialValue::from("Sales"),);
let manager = parsed.get("manager").expect("manager attr should exist");
assert_eq!(
manager,
&PartialValue::from(
"XYZCorp::Employee::\"34FB87\""
.parse::<EntityUID>()
.expect("valid")
),
);
let entitiesjson = json!(
[
{
"uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
.source(r#"in attribute `manager` on `XYZCorp::Employee::"12UA45"`, type mismatch: value was expected to have type `XYZCorp::Employee`, but it actually has type (entity of type `Employee`): `Employee::"34FB87"`"#)
.build()
);
});
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"department": "Sales",
"manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
},
"parents": []
}
]
);
assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
expect_err(
&entitiesjson,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("error during entity deserialization")
.source(r#"entity `Employee::"12UA45"` has type `Employee` which is not declared in the schema"#)
.help(r#"did you mean `XYZCorp::Employee`?"#)
.build()
);
});
}
}