1use crate::ast::*;
20use crate::extensions::Extensions;
21use crate::transitive_closure::{compute_tc, enforce_tc_and_dag};
22use std::collections::{hash_map, HashMap};
23use std::sync::Arc;
24
25pub mod conformance;
27pub mod err;
29pub mod json;
30use json::err::JsonSerializationError;
31
32pub use json::{
33 AllEntitiesNoAttrsSchema, AttributeType, CedarValueJson, ContextJsonParser, ContextSchema,
34 EntityJson, EntityJsonParser, EntityTypeDescription, EntityUidJson, FnAndArgs,
35 NoEntitiesSchema, NoStaticContext, Schema, SchemaType, TypeAndId,
36};
37
38use conformance::EntitySchemaConformanceChecker;
39use err::*;
40#[cfg(feature = "partial-eval")]
41use smol_str::ToSmolStr;
42
43#[derive(Clone, Debug, Default, PartialEq, Eq)]
51pub struct Entities {
52 entities: HashMap<EntityUID, Arc<Entity>>,
55
56 mode: Mode,
61}
62
63impl Entities {
64 pub fn new() -> Self {
66 Self {
67 entities: HashMap::new(),
68 mode: Mode::default(),
69 }
70 }
71
72 #[cfg(feature = "partial-eval")]
76 pub fn partial(self) -> Self {
77 Self {
78 entities: self.entities,
79 mode: Mode::Partial,
80 }
81 }
82
83 pub fn is_partial(&self) -> bool {
85 #[cfg(feature = "partial-eval")]
86 let ret = self.mode == Mode::Partial;
87 #[cfg(not(feature = "partial-eval"))]
88 let ret = false;
89
90 ret
91 }
92
93 pub fn entity(&self, uid: &EntityUID) -> Dereference<'_, Entity> {
95 match self.entities.get(uid) {
96 Some(e) => Dereference::Data(e),
97 None => match self.mode {
98 Mode::Concrete => Dereference::NoSuchEntity,
99 #[cfg(feature = "partial-eval")]
100 Mode::Partial => Dereference::Residual(Expr::unknown(Unknown::new_with_type(
101 uid.to_smolstr(),
102 Type::Entity {
103 ty: uid.entity_type().clone(),
104 },
105 ))),
106 },
107 }
108 }
109
110 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
112 self.entities.values().map(|e| e.as_ref())
113 }
114
115 pub fn deep_eq(&self, other: &Self) -> bool {
121 if self.mode != other.mode || self.entities.len() != other.entities.len() {
122 return false;
123 }
124
125 self.entities.iter().all(|(id, entity)| {
126 other
127 .entities
128 .get(id)
129 .is_some_and(|other_entity| entity.deep_eq(other_entity))
130 })
131 }
132
133 pub fn add_entities(
147 mut self,
148 collection: impl IntoIterator<Item = Arc<Entity>>,
149 schema: Option<&impl Schema>,
150 tc_computation: TCComputation,
151 extensions: &Extensions<'_>,
152 ) -> Result<Self> {
153 let checker = schema.map(|schema| EntitySchemaConformanceChecker::new(schema, extensions));
154 for entity in collection.into_iter() {
155 if let Some(checker) = checker.as_ref() {
156 checker.validate_entity(&entity)?;
157 }
158 update_entity_map(&mut self.entities, entity, false)?;
159 }
160 match tc_computation {
161 TCComputation::AssumeAlreadyComputed => (),
162 TCComputation::EnforceAlreadyComputed => enforce_tc_and_dag(&self.entities)?,
163 TCComputation::ComputeNow => compute_tc(&mut self.entities, true)?,
164 };
165 Ok(self)
166 }
167
168 pub fn remove_entities(
174 mut self,
175 collection: impl IntoIterator<Item = EntityUID>,
176 tc_computation: TCComputation,
177 ) -> Result<Self> {
178 for uid_to_remove in collection.into_iter() {
179 match self.entities.remove(&uid_to_remove) {
180 None => (),
181 Some(entity_to_remove) => {
182 for entity in self.entities.values_mut() {
183 if entity.is_descendant_of(&uid_to_remove) {
184 Arc::make_mut(entity).remove_indirect_ancestor(&uid_to_remove);
186 Arc::make_mut(entity).remove_parent(&uid_to_remove);
187 for ancestor_uid in entity_to_remove.ancestors() {
189 Arc::make_mut(entity).remove_indirect_ancestor(ancestor_uid);
190 }
191 }
192 }
193 }
194 }
195 }
196 match tc_computation {
197 TCComputation::AssumeAlreadyComputed => (),
198 TCComputation::EnforceAlreadyComputed => enforce_tc_and_dag(&self.entities)?,
199 TCComputation::ComputeNow => compute_tc(&mut self.entities, true)?,
200 }
201 Ok(self)
202 }
203
204 pub fn upsert_entities(
217 mut self,
218 collection: impl IntoIterator<Item = Arc<Entity>>,
219 schema: Option<&impl Schema>,
220 tc_computation: TCComputation,
221 extensions: &Extensions<'_>,
222 ) -> Result<Self> {
223 let checker = schema.map(|schema| EntitySchemaConformanceChecker::new(schema, extensions));
224 for entity in collection.into_iter() {
225 if let Some(checker) = checker.as_ref() {
226 checker.validate_entity(&entity)?;
227 }
228 update_entity_map(&mut self.entities, entity, true)?;
229 }
230 match tc_computation {
231 TCComputation::AssumeAlreadyComputed => (),
232 TCComputation::EnforceAlreadyComputed => enforce_tc_and_dag(&self.entities)?,
233 TCComputation::ComputeNow => compute_tc(&mut self.entities, true)?,
234 };
235 Ok(self)
236 }
237
238 pub fn from_entities(
257 entities: impl IntoIterator<Item = Entity>,
258 schema: Option<&impl Schema>,
259 tc_computation: TCComputation,
260 extensions: &Extensions<'_>,
261 ) -> Result<Self> {
262 let mut entity_map = create_entity_map(entities.into_iter().map(Arc::new))?;
263 if let Some(schema) = schema {
264 let checker = EntitySchemaConformanceChecker::new(schema, extensions);
269 for entity in entity_map.values() {
270 if !entity.uid().entity_type().is_action() {
271 checker.validate_entity(entity)?;
272 }
273 }
274 }
275 match tc_computation {
276 TCComputation::AssumeAlreadyComputed => {}
277 TCComputation::EnforceAlreadyComputed => {
278 enforce_tc_and_dag(&entity_map)?;
279 }
280 TCComputation::ComputeNow => {
281 compute_tc(&mut entity_map, true)?;
282 }
283 }
284 if let Some(schema) = schema {
290 let checker = EntitySchemaConformanceChecker::new(schema, extensions);
291 for entity in entity_map.values() {
292 if entity.uid().entity_type().is_action() {
293 checker.validate_entity(entity)?;
294 }
295 }
296 entity_map.extend(
298 schema
299 .action_entities()
300 .into_iter()
301 .map(|e: Arc<Entity>| (e.uid().clone(), e)),
302 );
303 }
304 Ok(Self {
305 entities: entity_map,
306 mode: Mode::default(),
307 })
308 }
309
310 pub fn len(&self) -> usize {
312 self.entities.len()
313 }
314
315 pub fn is_empty(&self) -> bool {
317 self.entities.is_empty()
318 }
319
320 pub fn to_json_value(&self) -> Result<serde_json::Value> {
327 let ejsons: Vec<EntityJson> = self.to_ejsons()?;
328 serde_json::to_value(ejsons)
329 .map_err(JsonSerializationError::from)
330 .map_err(Into::into)
331 }
332
333 pub fn write_to_json(&self, f: impl std::io::Write) -> Result<()> {
341 let ejsons: Vec<EntityJson> = self.to_ejsons()?;
342 serde_json::to_writer_pretty(f, &ejsons).map_err(JsonSerializationError::from)?;
343 Ok(())
344 }
345
346 fn to_ejsons(&self) -> Result<Vec<EntityJson>> {
348 self.entities
349 .values()
350 .map(Arc::as_ref)
351 .map(EntityJson::from_entity)
352 .collect::<std::result::Result<_, JsonSerializationError>>()
353 .map_err(Into::into)
354 }
355
356 fn get_entities_by_entity_type(&self) -> HashMap<EntityType, Vec<&Entity>> {
357 let mut entities_by_type: HashMap<EntityType, Vec<&Entity>> = HashMap::new();
358 for entity in self.iter() {
359 let euid = entity.uid();
360 let entity_type = euid.entity_type();
361 if let Some(entities) = entities_by_type.get_mut(entity_type) {
362 entities.push(entity);
363 } else {
364 entities_by_type.insert(entity_type.clone(), Vec::from([entity]));
365 }
366 }
367 entities_by_type
368 }
369
370 pub fn to_dot_str(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
374 write!(
376 f,
377 "strict digraph {{\n\tordering=\"out\"\n\tnode[shape=box]\n"
378 )?;
379
380 fn to_dot_id(f: &mut impl std::fmt::Write, v: &impl std::fmt::Display) -> std::fmt::Result {
389 write!(f, "\"{}\"", v.to_string().escape_debug())
390 }
391
392 let entities_by_type = self.get_entities_by_entity_type();
394
395 for (et, entities) in entities_by_type {
396 write!(f, "\tsubgraph \"cluster_{et}\" {{\n\t\tlabel=",)?;
397 to_dot_id(f, &et)?;
398 writeln!(f)?;
399 for entity in entities {
400 write!(f, "\t\t")?;
401 to_dot_id(f, &entity.uid())?;
402 write!(f, " [label=")?;
403 to_dot_id(f, &entity.uid().eid().escaped())?;
404 writeln!(f, "]")?;
405 }
406 writeln!(f, "\t}}")?;
407 }
408
409 for entity in self.iter() {
411 for ancestor in entity.ancestors() {
412 write!(f, "\t")?;
413 to_dot_id(f, &entity.uid())?;
414 write!(f, " -> ")?;
415 to_dot_id(f, &ancestor)?;
416 writeln!(f)?;
417 }
418 }
419 writeln!(f, "}}")?;
420 Ok(())
421 }
422}
423
424fn create_entity_map(
427 es: impl Iterator<Item = Arc<Entity>>,
428) -> Result<HashMap<EntityUID, Arc<Entity>>> {
429 let mut map: HashMap<EntityUID, Arc<Entity>> = HashMap::new();
430 for e in es {
431 update_entity_map(&mut map, e, false)?;
432 }
433 Ok(map)
434}
435
436fn update_entity_map(
442 map: &mut HashMap<EntityUID, Arc<Entity>>,
443 entity: Arc<Entity>,
444 allow_override: bool,
445) -> Result<()> {
446 match map.entry(entity.uid().clone()) {
447 hash_map::Entry::Occupied(mut occupied_entry) => {
448 if allow_override {
449 occupied_entry.insert(entity);
450 } else {
451 if !entity.deep_eq(occupied_entry.get()) {
454 let entry = occupied_entry.remove_entry();
455 return Err(EntitiesError::duplicate(entry.0));
456 }
457 }
458 }
459 hash_map::Entry::Vacant(v) => {
460 v.insert(entity);
461 }
462 }
463 Ok(())
464}
465
466impl IntoIterator for Entities {
467 type Item = Entity;
468
469 type IntoIter = std::iter::Map<
470 std::collections::hash_map::IntoValues<EntityUID, Arc<Entity>>,
471 fn(Arc<Entity>) -> Entity,
472 >;
473
474 fn into_iter(self) -> Self::IntoIter {
475 self.entities.into_values().map(Arc::unwrap_or_clone)
476 }
477}
478
479impl std::fmt::Display for Entities {
480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481 if self.entities.is_empty() {
482 write!(f, "<empty Entities>")
483 } else {
484 for e in self.entities.values() {
485 writeln!(f, "{e}")?;
486 }
487 Ok(())
488 }
489 }
490}
491
492#[derive(Debug, Clone)]
494pub enum Dereference<'a, T> {
495 NoSuchEntity,
497 Residual(Expr),
499 Data(&'a T),
501}
502
503impl<'a, T> Dereference<'a, T>
504where
505 T: std::fmt::Debug,
506{
507 #[allow(clippy::panic)]
518 pub fn unwrap(self) -> &'a T {
519 match self {
520 Self::Data(e) => e,
521 e => panic!("unwrap() called on {e:?}"),
522 }
523 }
524
525 #[allow(clippy::panic)]
536 #[track_caller] pub fn expect(self, msg: &str) -> &'a T {
538 match self {
539 Self::Data(e) => e,
540 e => panic!("expect() called on {e:?}, msg: {msg}"),
541 }
542 }
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546enum Mode {
547 Concrete,
548 #[cfg(feature = "partial-eval")]
549 Partial,
550}
551
552impl Default for Mode {
553 fn default() -> Self {
554 Self::Concrete
555 }
556}
557
558#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
562pub enum TCComputation {
563 AssumeAlreadyComputed,
566 EnforceAlreadyComputed,
570 ComputeNow,
574}
575
576#[allow(clippy::panic)]
578#[cfg(test)]
579#[allow(clippy::panic)]
581#[allow(clippy::cognitive_complexity)]
582mod json_parsing_tests {
583 use super::*;
584 use crate::{
585 assert_deep_eq, extensions::Extensions, test_utils::*, transitive_closure::TcError,
586 };
587 use cool_asserts::assert_matches;
588 use std::collections::HashSet;
589
590 #[test]
591 fn simple_json_parse1() {
592 let v = serde_json::json!(
593 [
594 {
595 "uid" : { "type" : "A", "id" : "b"},
596 "attrs" : {},
597 "parents" : [ { "type" : "A", "id" : "c" }]
598 }
599 ]
600 );
601 let parser: EntityJsonParser<'_, '_> =
602 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
603 parser
604 .from_json_value(v)
605 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
606 }
607
608 #[test]
609 fn enforces_tc_fail_cycle_almost() {
610 let parser: EntityJsonParser<'_, '_> =
611 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
612 let new = serde_json::json!([
613 {
614 "uid" : {
615 "type" : "Test",
616 "id" : "george"
617 },
618 "attrs" : { "foo" : 3},
619 "parents" : [
620 {
621 "type" : "Test",
622 "id" : "george"
623 },
624 {
625 "type" : "Test",
626 "id" : "janet"
627 }
628 ]
629 }
630 ]);
631
632 let addl_entities = parser
633 .iter_from_json_value(new)
634 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
635 .map(Arc::new);
636 let err = simple_entities(&parser).add_entities(
637 addl_entities,
638 None::<&NoEntitiesSchema>,
639 TCComputation::EnforceAlreadyComputed,
640 Extensions::none(),
641 );
642 let expected = TcError::missing_tc_edge(
644 r#"Test::"janet""#.parse().unwrap(),
645 r#"Test::"george""#.parse().unwrap(),
646 r#"Test::"janet""#.parse().unwrap(),
647 );
648 assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
649 assert_eq!(&expected, e.inner());
650 });
651 }
652
653 #[test]
654 fn enforces_tc_fail_connecting() {
655 let parser: EntityJsonParser<'_, '_> =
656 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
657 let new = serde_json::json!([
658 {
659 "uid" : {
660 "type" : "Test",
661 "id" : "george"
662 },
663 "attrs" : { "foo" : 3 },
664 "parents" : [
665 {
666 "type" : "Test",
667 "id" : "henry"
668 }
669 ]
670 }
671 ]);
672
673 let addl_entities = parser
674 .iter_from_json_value(new)
675 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
676 .map(Arc::new);
677 let err = simple_entities(&parser).add_entities(
678 addl_entities,
679 None::<&NoEntitiesSchema>,
680 TCComputation::EnforceAlreadyComputed,
681 Extensions::all_available(),
682 );
683 let expected = TcError::missing_tc_edge(
684 r#"Test::"janet""#.parse().unwrap(),
685 r#"Test::"george""#.parse().unwrap(),
686 r#"Test::"henry""#.parse().unwrap(),
687 );
688 assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
689 assert_eq!(&expected, e.inner());
690 });
691 }
692
693 #[test]
694 fn enforces_tc_fail_missing_edge() {
695 let parser: EntityJsonParser<'_, '_> =
696 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
697 let new = serde_json::json!([
698 {
699 "uid" : {
700 "type" : "Test",
701 "id" : "jeff",
702 },
703 "attrs" : { "foo" : 3 },
704 "parents" : [
705 {
706 "type" : "Test",
707 "id" : "alice"
708 }
709 ]
710 }
711 ]);
712
713 let addl_entities = parser
714 .iter_from_json_value(new)
715 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
716 .map(Arc::new);
717 let err = simple_entities(&parser).add_entities(
718 addl_entities,
719 None::<&NoEntitiesSchema>,
720 TCComputation::EnforceAlreadyComputed,
721 Extensions::all_available(),
722 );
723 let expected = TcError::missing_tc_edge(
724 r#"Test::"jeff""#.parse().unwrap(),
725 r#"Test::"alice""#.parse().unwrap(),
726 r#"Test::"bob""#.parse().unwrap(),
727 );
728 assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
729 assert_eq!(&expected, e.inner());
730 });
731 }
732
733 #[test]
734 fn enforces_tc_success() {
735 let parser: EntityJsonParser<'_, '_> =
736 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
737 let new = serde_json::json!([
738 {
739 "uid" : {
740 "type" : "Test",
741 "id" : "jeff"
742 },
743 "attrs" : { "foo" : 3 },
744 "parents" : [
745 {
746 "type" : "Test",
747 "id" : "alice"
748 },
749 {
750 "type" : "Test",
751 "id" : "bob"
752 }
753 ]
754 }
755 ]);
756
757 let addl_entities = parser
758 .iter_from_json_value(new)
759 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
760 .map(Arc::new);
761 let es = simple_entities(&parser)
762 .add_entities(
763 addl_entities,
764 None::<&NoEntitiesSchema>,
765 TCComputation::EnforceAlreadyComputed,
766 Extensions::all_available(),
767 )
768 .unwrap();
769 let euid = r#"Test::"jeff""#.parse().unwrap();
770 let jeff = es.entity(&euid).unwrap();
771 assert!(jeff.is_descendant_of(&r#"Test::"alice""#.parse().unwrap()));
772 assert!(jeff.is_descendant_of(&r#"Test::"bob""#.parse().unwrap()));
773 assert!(!jeff.is_descendant_of(&r#"Test::"george""#.parse().unwrap()));
774 simple_entities_still_sane(&es);
775 }
776
777 #[test]
778 fn adds_extends_tc_connecting() {
779 let parser: EntityJsonParser<'_, '_> =
780 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
781 let new = serde_json::json!([
782 {
783 "uid" : {
784 "type" : "Test",
785 "id" : "george"
786 },
787 "attrs" : { "foo" : 3},
788 "parents" : [
789 {
790 "type" : "Test",
791 "id" : "henry"
792 }
793 ]
794 }
795 ]);
796
797 let addl_entities = parser
798 .iter_from_json_value(new)
799 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
800 .map(Arc::new);
801 let es = simple_entities(&parser)
802 .add_entities(
803 addl_entities,
804 None::<&NoEntitiesSchema>,
805 TCComputation::ComputeNow,
806 Extensions::all_available(),
807 )
808 .unwrap();
809 let euid = r#"Test::"george""#.parse().unwrap();
810 let jeff = es.entity(&euid).unwrap();
811 assert!(jeff.is_descendant_of(&r#"Test::"henry""#.parse().unwrap()));
812 let alice = es.entity(&r#"Test::"janet""#.parse().unwrap()).unwrap();
813 assert!(alice.is_descendant_of(&r#"Test::"henry""#.parse().unwrap()));
814 simple_entities_still_sane(&es);
815 }
816
817 #[test]
818 fn adds_extends_tc() {
819 let parser: EntityJsonParser<'_, '_> =
820 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
821 let new = serde_json::json!([
822 {
823 "uid" : {
824 "type" : "Test",
825 "id" : "jeff"
826 },
827 "attrs" : {
828 "foo" : 3
829 },
830 "parents" : [
831 {
832 "type" : "Test",
833 "id" : "alice"
834 }
835 ]
836 }
837 ]);
838
839 let addl_entities = parser
840 .iter_from_json_value(new)
841 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
842 .map(Arc::new);
843 let es = simple_entities(&parser)
844 .add_entities(
845 addl_entities,
846 None::<&NoEntitiesSchema>,
847 TCComputation::ComputeNow,
848 Extensions::all_available(),
849 )
850 .unwrap();
851 let euid = r#"Test::"jeff""#.parse().unwrap();
852 let jeff = es.entity(&euid).unwrap();
853 assert!(jeff.is_descendant_of(&r#"Test::"alice""#.parse().unwrap()));
854 assert!(jeff.is_descendant_of(&r#"Test::"bob""#.parse().unwrap()));
855 simple_entities_still_sane(&es);
856 }
857
858 #[test]
859 fn adds_works() {
860 let parser: EntityJsonParser<'_, '_> =
861 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
862 let new = serde_json::json!([
863 {
864 "uid" : {
865 "type" : "Test",
866 "id" : "jeff"
867 },
868 "attrs" : {
869 "foo" : 3
870 },
871 "parents" : [
872 {
873 "type" : "Test",
874 "id" : "susan"
875 }
876 ]
877 }
878 ]);
879
880 let addl_entities = parser
881 .iter_from_json_value(new)
882 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
883 .map(Arc::new);
884 let es = simple_entities(&parser)
885 .add_entities(
886 addl_entities,
887 None::<&NoEntitiesSchema>,
888 TCComputation::ComputeNow,
889 Extensions::all_available(),
890 )
891 .unwrap();
892 let euid = r#"Test::"jeff""#.parse().unwrap();
893 let jeff = es.entity(&euid).unwrap();
894 let value = jeff.get("foo").unwrap();
895 assert_eq!(value, &PartialValue::from(3));
896 assert!(jeff.is_descendant_of(&r#"Test::"susan""#.parse().unwrap()));
897 simple_entities_still_sane(&es);
898 }
899
900 #[test]
901 fn add_consistent_duplicates_in_iterator() {
902 let parser: EntityJsonParser<'_, '_> =
903 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
904 let new = serde_json::json!([
906 {"uid":{ "type" : "Test", "id" : "ruby" }, "attrs" : {}, "parents" : []},
907 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []},
908 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []}]);
909 let addl_entities = parser
910 .iter_from_json_value(new)
911 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
912 .map(Arc::new);
913 let original = simple_entities(&parser);
915 let original_size = original.entities.len();
916 let es = original
918 .add_entities(
919 addl_entities,
920 None::<&NoEntitiesSchema>,
921 TCComputation::ComputeNow,
922 Extensions::all_available(),
923 )
924 .unwrap();
925 simple_entities_still_sane(&es);
927 es.entity(&r#"Test::"jeff""#.parse().unwrap()).unwrap();
929 es.entity(&r#"Test::"ruby""#.parse().unwrap()).unwrap();
931 assert_eq!(es.entities.len(), 2 + original_size);
933 }
934
935 #[test]
936 fn add_inconsistent_duplicates_in_iterator() {
937 let parser: EntityJsonParser<'_, '_> =
938 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
939 let new = serde_json::json!([
941 {"uid":{ "type" : "Test", "id" : "ruby" }, "attrs" : {"location": "France"}, "parents" : []},
942 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {"location": "France"}, "parents" : []},
943 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []}]);
944
945 let addl_entities = parser
946 .iter_from_json_value(new)
947 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
948 .map(Arc::new);
949 let original = simple_entities(&parser);
951 let err = original
953 .add_entities(
954 addl_entities,
955 None::<&NoEntitiesSchema>,
956 TCComputation::ComputeNow,
957 Extensions::all_available(),
958 )
959 .err()
960 .unwrap();
961 let expected = r#"Test::"jeff""#.parse().unwrap();
963 assert_matches!(err, EntitiesError::Duplicate(d) => assert_eq!(d.euid(), &expected));
964 }
965
966 #[test]
967 fn add_consistent_duplicate() {
968 let parser: EntityJsonParser<'_, '_> =
969 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
970 let new = serde_json::json!([
972 {"uid":{ "type" : "Test", "id" : "ruby" }, "attrs" : {}, "parents" : []},
973 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []}]);
974 let addl_entities = parser
975 .iter_from_json_value(new)
976 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
977 .map(Arc::new);
978 let json = serde_json::json!([
980 {"uid":{ "type" : "Test", "id" : "amy" }, "attrs" : {}, "parents" : []},
981 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []}]);
982 let original = parser
983 .from_json_value(json)
984 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
985 let original_size = original.entities.len();
986 let es = original
988 .add_entities(
989 addl_entities,
990 None::<&NoEntitiesSchema>,
991 TCComputation::ComputeNow,
992 Extensions::all_available(),
993 )
994 .unwrap();
995 es.entity(&r#"Test::"jeff""#.parse().unwrap()).unwrap();
997 es.entity(&r#"Test::"amy""#.parse().unwrap()).unwrap();
999 es.entity(&r#"Test::"ruby""#.parse().unwrap()).unwrap();
1001 assert_eq!(es.entities.len(), 1 + original_size);
1003 }
1004
1005 #[test]
1006 fn add_inconsistent_duplicate() {
1007 let parser: EntityJsonParser<'_, '_> =
1008 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1009 let new = serde_json::json!([
1011 {"uid":{ "type" : "Test", "id" : "ruby" }, "attrs" : {}, "parents" : []},
1012 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {"location": "England"}, "parents" : []}]);
1013 let addl_entities = parser
1014 .iter_from_json_value(new)
1015 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
1016 .map(Arc::new);
1017 let json = serde_json::json!([
1019 {"uid":{ "type" : "Test", "id" : "amy" }, "attrs" : {}, "parents" : []},
1020 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {"location": "London"}, "parents" : []}]);
1021 let original = parser
1022 .from_json_value(json)
1023 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
1024 let err = original
1025 .add_entities(
1026 addl_entities,
1027 None::<&NoEntitiesSchema>,
1028 TCComputation::ComputeNow,
1029 Extensions::all_available(),
1030 )
1031 .err()
1032 .unwrap();
1033 let expected = r#"Test::"jeff""#.parse().unwrap();
1035 assert_matches!(err, EntitiesError::Duplicate(d) => assert_eq!(d.euid(), &expected));
1036 }
1037
1038 #[test]
1039 fn add_inconsistent_duplicate_tags() {
1040 let parser: EntityJsonParser<'_, '_> =
1041 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1042
1043 let initial = parser.single_from_json_value(serde_json::json!({"uid":{ "type" : "Test", "id" : "jeff" }, "attrs": {}, "tags" : {"t": 1}, "parents" : []})).unwrap();
1044 let initial_entities = Entities::from_entities(
1045 [initial],
1046 None::<&NoEntitiesSchema>,
1047 TCComputation::ComputeNow,
1048 Extensions::all_available(),
1049 )
1050 .unwrap();
1051
1052 let dup = parser.single_from_json_value(serde_json::json!({"uid":{ "type" : "Test", "id" : "jeff" }, "attrs": {}, "tags" : {}, "parents" : []})).unwrap();
1053 let err = initial_entities
1054 .add_entities(
1055 [Arc::new(dup)],
1056 None::<&NoEntitiesSchema>,
1057 TCComputation::ComputeNow,
1058 Extensions::all_available(),
1059 )
1060 .err()
1061 .unwrap();
1062
1063 assert_matches!(err, EntitiesError::Duplicate(d) => assert_eq!(d.euid(), &r#"Test::"jeff""#.parse().unwrap()));
1064 }
1065
1066 #[test]
1067 fn simple_entities_correct() {
1068 let parser: EntityJsonParser<'_, '_> =
1069 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1070 simple_entities(&parser);
1071 }
1072
1073 fn simple_entities(parser: &EntityJsonParser<'_, '_>) -> Entities {
1074 let json = serde_json::json!(
1075 [
1076 {
1077 "uid" : { "type" : "Test", "id": "alice" },
1078 "attrs" : { "bar" : 2},
1079 "parents" : [
1080 {
1081 "type" : "Test",
1082 "id" : "bob"
1083 }
1084 ]
1085 },
1086 {
1087 "uid" : { "type" : "Test", "id" : "janet"},
1088 "attrs" : { "bar" : 2},
1089 "parents" : [
1090 {
1091 "type" : "Test",
1092 "id" : "george"
1093 }
1094 ]
1095 },
1096 {
1097 "uid" : { "type" : "Test", "id" : "bob"},
1098 "attrs" : {},
1099 "parents" : []
1100 },
1101 {
1102 "uid" : { "type" : "Test", "id" : "henry"},
1103 "attrs" : {},
1104 "parents" : []
1105 },
1106 ]
1107 );
1108 parser
1109 .from_json_value(json)
1110 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
1111 }
1112
1113 fn simple_entities_still_sane(e: &Entities) {
1115 let bob = r#"Test::"bob""#.parse().unwrap();
1116 let alice = e.entity(&r#"Test::"alice""#.parse().unwrap()).unwrap();
1117 let bar = alice.get("bar").unwrap();
1118 assert_eq!(bar, &PartialValue::from(2));
1119 assert!(alice.is_descendant_of(&bob));
1120 let bob = e.entity(&bob).unwrap();
1121 assert!(bob.ancestors().next().is_none());
1122 }
1123
1124 #[cfg(feature = "partial-eval")]
1125 #[test]
1126 fn basic_partial() {
1127 let json = serde_json::json!(
1129 [
1130 {
1131 "uid" : {
1132 "type" : "test_entity_type",
1133 "id" : "alice"
1134 },
1135 "attrs": {},
1136 "parents": [
1137 {
1138 "type" : "test_entity_type",
1139 "id" : "jane"
1140 }
1141 ]
1142 },
1143 {
1144 "uid" : {
1145 "type" : "test_entity_type",
1146 "id" : "jane"
1147 },
1148 "attrs": {},
1149 "parents": [
1150 {
1151 "type" : "test_entity_type",
1152 "id" : "bob",
1153 }
1154 ]
1155 },
1156 {
1157 "uid" : {
1158 "type" : "test_entity_type",
1159 "id" : "bob"
1160 },
1161 "attrs": {},
1162 "parents": []
1163 }
1164 ]
1165 );
1166
1167 let eparser: EntityJsonParser<'_, '_> =
1168 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1169 let es = eparser
1170 .from_json_value(json)
1171 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)))
1172 .partial();
1173
1174 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
1175 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
1177
1178 let janice = es.entity(&EntityUID::with_eid("janice"));
1179
1180 assert_matches!(janice, Dereference::Residual(_));
1181 }
1182
1183 #[test]
1184 fn basic() {
1185 let json = serde_json::json!([
1187 {
1188 "uid" : {
1189 "type" : "test_entity_type",
1190 "id" : "alice"
1191 },
1192 "attrs": {},
1193 "parents": [
1194 {
1195 "type" : "test_entity_type",
1196 "id" : "jane"
1197 }
1198 ]
1199 },
1200 {
1201 "uid" : {
1202 "type" : "test_entity_type",
1203 "id" : "jane"
1204 },
1205 "attrs": {},
1206 "parents": [
1207 {
1208 "type" : "test_entity_type",
1209 "id" : "bob"
1210 }
1211 ]
1212 },
1213 {
1214 "uid" : {
1215 "type" : "test_entity_type",
1216 "id" : "bob"
1217 },
1218 "attrs": {},
1219 "parents": []
1220 },
1221 {
1222 "uid" : {
1223 "type" : "test_entity_type",
1224 "id" : "josephine"
1225 },
1226 "attrs": {},
1227 "parents": [],
1228 "tags": {}
1229 }
1230 ]
1231 );
1232
1233 let eparser: EntityJsonParser<'_, '_> =
1234 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1235 let es = eparser
1236 .from_json_value(json)
1237 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
1238
1239 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
1240 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
1242 }
1243
1244 #[test]
1245 fn no_expr_escapes1() {
1246 let json = serde_json::json!(
1247 [
1248 {
1249 "uid" : r#"test_entity_type::"Alice""#,
1250 "attrs": {
1251 "bacon": "eggs",
1252 "pancakes": [1, 2, 3],
1253 "waffles": { "key": "value" },
1254 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
1255 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1256 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1257 },
1258 "parents": [
1259 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
1260 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1261 ]
1262 },
1263 ]);
1264 let eparser: EntityJsonParser<'_, '_> =
1265 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1266 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1267 expect_err(
1268 &json,
1269 &miette::Report::new(e),
1270 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1271 .source(r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"test_entity_type::\"Alice\""`"#)
1272 .help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
1273 .build()
1274 );
1275 });
1276 }
1277
1278 #[test]
1279 fn no_expr_escapes2() {
1280 let json = serde_json::json!(
1281 [
1282 {
1283 "uid" : {
1284 "__expr" :
1285 r#"test_entity_type::"Alice""#
1286 },
1287 "attrs": {
1288 "bacon": "eggs",
1289 "pancakes": [1, 2, 3],
1290 "waffles": { "key": "value" },
1291 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
1292 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1293 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1294 },
1295 "parents": [
1296 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
1297 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1298 ]
1299 }
1300 ]);
1301 let eparser: EntityJsonParser<'_, '_> =
1302 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1303 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1304 expect_err(
1305 &json,
1306 &miette::Report::new(e),
1307 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1308 .source(r#"in uid field of <unknown entity>, the `__expr` escape is no longer supported"#)
1309 .help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
1310 .build()
1311 );
1312 });
1313 }
1314
1315 #[test]
1316 fn no_expr_escapes3() {
1317 let json = serde_json::json!(
1318 [
1319 {
1320 "uid" : {
1321 "type" : "test_entity_type",
1322 "id" : "Alice"
1323 },
1324 "attrs": {
1325 "bacon": "eggs",
1326 "pancakes": { "__expr" : "[1,2,3]" },
1327 "waffles": { "key": "value" },
1328 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
1329 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1330 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1331 },
1332 "parents": [
1333 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
1334 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1335 ]
1336 }
1337 ]);
1338 let eparser: EntityJsonParser<'_, '_> =
1339 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1340 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1341 expect_err(
1342 &json,
1343 &miette::Report::new(e),
1344 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1345 .source(r#"in attribute `pancakes` on `test_entity_type::"Alice"`, the `__expr` escape is no longer supported"#)
1346 .help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
1347 .build()
1348 );
1349 });
1350 }
1351
1352 #[test]
1353 fn no_expr_escapes4() {
1354 let json = serde_json::json!(
1355 [
1356 {
1357 "uid" : {
1358 "type" : "test_entity_type",
1359 "id" : "Alice"
1360 },
1361 "attrs": {
1362 "bacon": "eggs",
1363 "waffles": { "key": "value" },
1364 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1365 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1366 },
1367 "parents": [
1368 { "__expr": "test_entity_type::\"Alice\"" },
1369 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1370 ]
1371 }
1372 ]);
1373 let eparser: EntityJsonParser<'_, '_> =
1374 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1375 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1376 expect_err(
1377 &json,
1378 &miette::Report::new(e),
1379 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1380 .source(r#"in parents field of `test_entity_type::"Alice"`, the `__expr` escape is no longer supported"#)
1381 .help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
1382 .build()
1383 );
1384 });
1385 }
1386
1387 #[test]
1388 fn no_expr_escapes5() {
1389 let json = serde_json::json!(
1390 [
1391 {
1392 "uid" : {
1393 "type" : "test_entity_type",
1394 "id" : "Alice"
1395 },
1396 "attrs": {
1397 "bacon": "eggs",
1398 "waffles": { "key": "value" },
1399 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1400 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1401 },
1402 "parents": [
1403 "test_entity_type::\"bob\"",
1404 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1405 ]
1406 }
1407 ]);
1408 let eparser: EntityJsonParser<'_, '_> =
1409 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1410 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1411 expect_err(
1412 &json,
1413 &miette::Report::new(e),
1414 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1415 .source(r#"in parents field of `test_entity_type::"Alice"`, expected a literal entity reference, but got `"test_entity_type::\"bob\""`"#)
1416 .help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
1417 .build()
1418 );
1419 });
1420 }
1421
1422 #[cfg(feature = "ipaddr")]
1423 #[test]
1425 fn more_escapes() {
1426 let json = serde_json::json!(
1427 [
1428 {
1429 "uid" : {
1430 "type" : "test_entity_type",
1431 "id" : "alice"
1432 },
1433 "attrs": {
1434 "bacon": "eggs",
1435 "pancakes": [1, 2, 3],
1436 "waffles": { "key": "value" },
1437 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
1438 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1439 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1440 },
1441 "parents": [
1442 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
1443 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1444 ]
1445 },
1446 {
1447 "uid" : {
1448 "type" : "test_entity_type",
1449 "id" : "bob"
1450 },
1451 "attrs": {},
1452 "parents": []
1453 },
1454 {
1455 "uid" : {
1456 "type" : "test_entity_type",
1457 "id" : "catherine"
1458 },
1459 "attrs": {},
1460 "parents": []
1461 }
1462 ]
1463 );
1464
1465 let eparser: EntityJsonParser<'_, '_> =
1466 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1467 let es = eparser
1468 .from_json_value(json)
1469 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
1470
1471 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
1472 assert_eq!(alice.get("bacon"), Some(&PartialValue::from("eggs")));
1473 assert_eq!(
1474 alice.get("pancakes"),
1475 Some(&PartialValue::from(vec![
1476 Value::from(1),
1477 Value::from(2),
1478 Value::from(3),
1479 ])),
1480 );
1481 assert_eq!(
1482 alice.get("waffles"),
1483 Some(&PartialValue::from(Value::record(
1484 vec![("key", Value::from("value"),)],
1485 None
1486 ))),
1487 );
1488 assert_eq!(
1489 alice.get("toast").cloned().map(RestrictedExpr::try_from),
1490 Some(Ok(RestrictedExpr::call_extension_fn(
1491 "decimal".parse().expect("should be a valid Name"),
1492 vec![RestrictedExpr::val("33.47")],
1493 ))),
1494 );
1495 assert_eq!(
1496 alice.get("12345"),
1497 Some(&PartialValue::from(EntityUID::with_eid("bob"))),
1498 );
1499 assert_eq!(
1500 alice.get("a b c").cloned().map(RestrictedExpr::try_from),
1501 Some(Ok(RestrictedExpr::call_extension_fn(
1502 "ip".parse().expect("should be a valid Name"),
1503 vec![RestrictedExpr::val("222.222.222.0/24")],
1504 ))),
1505 );
1506 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
1507 assert!(alice.is_descendant_of(&EntityUID::with_eid("catherine")));
1508 }
1509
1510 #[test]
1511 fn implicit_and_explicit_escapes() {
1512 let json = serde_json::json!(
1515 [
1516 {
1517 "uid": { "type" : "test_entity_type", "id" : "alice" },
1518 "attrs": {},
1519 "parents": [
1520 { "type" : "test_entity_type", "id" : "bob" },
1521 { "__entity": { "type": "test_entity_type", "id": "charles" } },
1522 { "type": "test_entity_type", "id": "elaine" }
1523 ]
1524 },
1525 {
1526 "uid": { "__entity": { "type": "test_entity_type", "id": "bob" }},
1527 "attrs": {},
1528 "parents": []
1529 },
1530 {
1531 "uid" : {
1532 "type" : "test_entity_type",
1533 "id" : "charles"
1534 },
1535 "attrs" : {},
1536 "parents" : []
1537 },
1538 {
1539 "uid": { "type": "test_entity_type", "id": "darwin" },
1540 "attrs": {},
1541 "parents": []
1542 },
1543 {
1544 "uid": { "type": "test_entity_type", "id": "elaine" },
1545 "attrs": {},
1546 "parents" : [
1547 {
1548 "type" : "test_entity_type",
1549 "id" : "darwin"
1550 }
1551 ]
1552 }
1553 ]
1554 );
1555
1556 let eparser: EntityJsonParser<'_, '_> =
1557 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1558 let es = eparser
1559 .from_json_value(json)
1560 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
1561
1562 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
1564 let bob = es.entity(&EntityUID::with_eid("bob")).unwrap();
1565 let charles = es.entity(&EntityUID::with_eid("charles")).unwrap();
1566 let darwin = es.entity(&EntityUID::with_eid("darwin")).unwrap();
1567 let elaine = es.entity(&EntityUID::with_eid("elaine")).unwrap();
1568
1569 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
1571 assert!(alice.is_descendant_of(&EntityUID::with_eid("charles")));
1572 assert!(alice.is_descendant_of(&EntityUID::with_eid("darwin")));
1573 assert!(alice.is_descendant_of(&EntityUID::with_eid("elaine")));
1574 assert_eq!(bob.ancestors().next(), None);
1575 assert_eq!(charles.ancestors().next(), None);
1576 assert_eq!(darwin.ancestors().next(), None);
1577 assert!(elaine.is_descendant_of(&EntityUID::with_eid("darwin")));
1578 assert!(!elaine.is_descendant_of(&EntityUID::with_eid("bob")));
1579 }
1580
1581 #[test]
1582 fn uid_failures() {
1583 let eparser: EntityJsonParser<'_, '_> =
1585 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1586
1587 let json = serde_json::json!(
1588 [
1589 {
1590 "uid": "hello",
1591 "attrs": {},
1592 "parents": []
1593 }
1594 ]
1595 );
1596 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1597 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1598 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"hello"`"#,
1599 ).help(
1600 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1601 ).build());
1602 });
1603
1604 let json = serde_json::json!(
1605 [
1606 {
1607 "uid": "\"hello\"",
1608 "attrs": {},
1609 "parents": []
1610 }
1611 ]
1612 );
1613 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1614 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1615 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"\"hello\""`"#,
1616 ).help(
1617 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1618 ).build());
1619 });
1620
1621 let json = serde_json::json!(
1622 [
1623 {
1624 "uid": { "type": "foo", "spam": "eggs" },
1625 "attrs": {},
1626 "parents": []
1627 }
1628 ]
1629 );
1630 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1631 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1632 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"spam":"eggs","type":"foo"}`"#,
1633 ).help(
1634 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1635 ).build());
1636 });
1637
1638 let json = serde_json::json!(
1639 [
1640 {
1641 "uid": { "type": "foo", "id": "bar" },
1642 "attrs": {},
1643 "parents": "foo::\"help\""
1644 }
1645 ]
1646 );
1647 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1648 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1649 r#"invalid type: string "foo::\"help\"", expected a sequence"#
1650 ).build());
1651 });
1652
1653 let json = serde_json::json!(
1654 [
1655 {
1656 "uid": { "type": "foo", "id": "bar" },
1657 "attrs": {},
1658 "parents": [
1659 "foo::\"help\"",
1660 { "__extn": { "fn": "ip", "arg": "222.222.222.0" } }
1661 ]
1662 }
1663 ]
1664 );
1665 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1666 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1667 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `"foo::\"help\""`"#,
1668 ).help(
1669 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1670 ).build());
1671 });
1672 }
1673
1674 #[test]
1677 fn null_failures() {
1678 let eparser: EntityJsonParser<'_, '_> =
1679 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1680
1681 let json = serde_json::json!(
1682 [
1683 {
1684 "uid": null,
1685 "attrs": {},
1686 "parents": [],
1687 }
1688 ]
1689 );
1690 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1691 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1692 "in uid field of <unknown entity>, expected a literal entity reference, but got `null`",
1693 ).help(
1694 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1695 ).build());
1696 });
1697
1698 let json = serde_json::json!(
1699 [
1700 {
1701 "uid": { "type": null, "id": "bar" },
1702 "attrs": {},
1703 "parents": [],
1704 }
1705 ]
1706 );
1707 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1708 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1709 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":"bar","type":null}`"#,
1710 ).help(
1711 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1712 ).build());
1713 });
1714
1715 let json = serde_json::json!(
1716 [
1717 {
1718 "uid": { "type": "foo", "id": null },
1719 "attrs": {},
1720 "parents": [],
1721 }
1722 ]
1723 );
1724 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1725 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1726 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
1727 ).help(
1728 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1729 ).build());
1730 });
1731
1732 let json = serde_json::json!(
1733 [
1734 {
1735 "uid": { "type": "foo", "id": "bar" },
1736 "attrs": null,
1737 "parents": [],
1738 }
1739 ]
1740 );
1741 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1742 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1743 "invalid type: null, expected a map"
1744 ).build());
1745 });
1746
1747 let json = serde_json::json!(
1748 [
1749 {
1750 "uid": { "type": "foo", "id": "bar" },
1751 "attrs": { "attr": null },
1752 "parents": [],
1753 }
1754 ]
1755 );
1756 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1757 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1758 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1759 ).build());
1760 });
1761
1762 let json = serde_json::json!(
1763 [
1764 {
1765 "uid": { "type": "foo", "id": "bar" },
1766 "attrs": { "attr": { "subattr": null } },
1767 "parents": [],
1768 }
1769 ]
1770 );
1771 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1772 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1773 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1774 ).build());
1775 });
1776
1777 let json = serde_json::json!(
1778 [
1779 {
1780 "uid": { "type": "foo", "id": "bar" },
1781 "attrs": { "attr": [ 3, null ] },
1782 "parents": [],
1783 }
1784 ]
1785 );
1786 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1787 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1788 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1789 ).build());
1790 });
1791
1792 let json = serde_json::json!(
1793 [
1794 {
1795 "uid": { "type": "foo", "id": "bar" },
1796 "attrs": { "attr": [ 3, { "subattr" : null } ] },
1797 "parents": [],
1798 }
1799 ]
1800 );
1801 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1802 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1803 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1804 ).build());
1805 });
1806
1807 let json = serde_json::json!(
1808 [
1809 {
1810 "uid": { "type": "foo", "id": "bar" },
1811 "attrs": { "__extn": { "fn": null, "args": [] } },
1812 "parents": [],
1813 }
1814 ]
1815 );
1816 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1817 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1818 r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1819 ).build());
1820 });
1821
1822 let json = serde_json::json!(
1823 [
1824 {
1825 "uid": { "type": "foo", "id": "bar" },
1826 "attrs": { "__extn": { "fn": "ip", "args": null } },
1827 "parents": [],
1828 }
1829 ]
1830 );
1831 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1832 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1833 r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1834 ).build());
1835 });
1836
1837 let json = serde_json::json!(
1838 [
1839 {
1840 "uid": { "type": "foo", "id": "bar" },
1841 "attrs": { "__extn": { "fn": "ip", "args": [ null ] } },
1842 "parents": [],
1843 }
1844 ]
1845 );
1846 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1847 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1848 r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1849 ).build());
1850 });
1851
1852 let json = serde_json::json!(
1853 [
1854 {
1855 "uid": { "type": "foo", "id": "bar" },
1856 "attrs": { "attr": 2 },
1857 "parents": null,
1858 }
1859 ]
1860 );
1861 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1862 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1863 "invalid type: null, expected a sequence"
1864 ).build());
1865 });
1866
1867 let json = serde_json::json!(
1868 [
1869 {
1870 "uid": { "type": "foo", "id": "bar" },
1871 "attrs": { "attr": 2 },
1872 "parents": [ null ],
1873 }
1874 ]
1875 );
1876 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1877 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1878 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
1879 ).help(
1880 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1881 ).build());
1882 });
1883
1884 let json = serde_json::json!(
1885 [
1886 {
1887 "uid": { "type": "foo", "id": "bar" },
1888 "attrs": { "attr": 2 },
1889 "parents": [ { "type": "foo", "id": null } ],
1890 }
1891 ]
1892 );
1893 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1894 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1895 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
1896 ).help(
1897 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1898 ).build());
1899 });
1900
1901 let json = serde_json::json!(
1902 [
1903 {
1904 "uid": { "type": "foo", "id": "bar" },
1905 "attrs": { "attr": 2 },
1906 "parents": [ { "type": "foo", "id": "parent" }, null ],
1907 }
1908 ]
1909 );
1910 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1911 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1912 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
1913 ).help(
1914 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1915 ).build());
1916 });
1917 }
1918
1919 fn roundtrip(entities: &Entities) -> Result<Entities> {
1921 let mut buf = Vec::new();
1922 entities.write_to_json(&mut buf)?;
1923 let eparser: EntityJsonParser<'_, '_> =
1924 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1925 eparser.from_json_str(&String::from_utf8(buf).expect("should be valid UTF-8"))
1926 }
1927
1928 fn test_entities() -> [Entity; 4] {
1930 [
1931 Entity::with_uid(EntityUID::with_eid("test_principal")),
1932 Entity::with_uid(EntityUID::with_eid("test_action")),
1933 Entity::with_uid(EntityUID::with_eid("test_resource")),
1934 Entity::with_uid(EntityUID::with_eid("test")),
1935 ]
1936 }
1937
1938 #[test]
1941 fn json_roundtripping() {
1942 let empty_entities = Entities::new();
1943 assert_deep_eq!(
1944 empty_entities,
1945 roundtrip(&empty_entities).expect("should roundtrip without errors")
1946 );
1947
1948 let entities = Entities::from_entities(
1949 test_entities(),
1950 None::<&NoEntitiesSchema>,
1951 TCComputation::ComputeNow,
1952 Extensions::none(),
1953 )
1954 .expect("Failed to construct entities");
1955 assert_deep_eq!(
1956 entities,
1957 roundtrip(&entities).expect("should roundtrip without errors")
1958 );
1959
1960 let complicated_entity = Entity::new(
1961 EntityUID::with_eid("complicated"),
1962 [
1963 ("foo".into(), RestrictedExpr::val(false)),
1964 ("bar".into(), RestrictedExpr::val(-234)),
1965 ("ham".into(), RestrictedExpr::val(r"a b c * / ? \")),
1966 (
1967 "123".into(),
1968 RestrictedExpr::val(EntityUID::with_eid("mom")),
1969 ),
1970 (
1971 "set".into(),
1972 RestrictedExpr::set([
1973 RestrictedExpr::val(0),
1974 RestrictedExpr::val(EntityUID::with_eid("pancakes")),
1975 RestrictedExpr::val("mmm"),
1976 ]),
1977 ),
1978 (
1979 "rec".into(),
1980 RestrictedExpr::record([
1981 ("nested".into(), RestrictedExpr::val("attr")),
1982 (
1983 "another".into(),
1984 RestrictedExpr::val(EntityUID::with_eid("foo")),
1985 ),
1986 ])
1987 .unwrap(),
1988 ),
1989 (
1990 "src_ip".into(),
1991 RestrictedExpr::call_extension_fn(
1992 "ip".parse().expect("should be a valid Name"),
1993 vec![RestrictedExpr::val("222.222.222.222")],
1994 ),
1995 ),
1996 ],
1997 HashSet::new(),
1998 [
1999 EntityUID::with_eid("parent1"),
2000 EntityUID::with_eid("parent2"),
2001 ]
2002 .into_iter()
2003 .collect(),
2004 [
2005 ("foo".into(), RestrictedExpr::val(2345)),
2007 ("bar".into(), RestrictedExpr::val(-1)),
2009 (
2012 "pancakes".into(),
2013 RestrictedExpr::val(EntityUID::with_eid("pancakes")),
2014 ),
2015 ],
2016 Extensions::all_available(),
2017 )
2018 .unwrap();
2019 let entities = Entities::from_entities(
2020 [
2021 complicated_entity,
2022 Entity::with_uid(EntityUID::with_eid("parent1")),
2023 Entity::with_uid(EntityUID::with_eid("parent2")),
2024 ],
2025 None::<&NoEntitiesSchema>,
2026 TCComputation::ComputeNow,
2027 Extensions::all_available(),
2028 )
2029 .expect("Failed to construct entities");
2030 assert_deep_eq!(
2031 entities,
2032 roundtrip(&entities).expect("should roundtrip without errors")
2033 );
2034
2035 let oops_entity = Entity::new(
2036 EntityUID::with_eid("oops"),
2037 [(
2038 "oops".into(),
2040 RestrictedExpr::record([("__entity".into(), RestrictedExpr::val("hi"))]).unwrap(),
2041 )],
2042 HashSet::new(),
2043 [
2044 EntityUID::with_eid("parent1"),
2045 EntityUID::with_eid("parent2"),
2046 ]
2047 .into_iter()
2048 .collect(),
2049 [],
2050 Extensions::all_available(),
2051 )
2052 .unwrap();
2053 let entities = Entities::from_entities(
2054 [
2055 oops_entity,
2056 Entity::with_uid(EntityUID::with_eid("parent1")),
2057 Entity::with_uid(EntityUID::with_eid("parent2")),
2058 ],
2059 None::<&NoEntitiesSchema>,
2060 TCComputation::ComputeNow,
2061 Extensions::all_available(),
2062 )
2063 .expect("Failed to construct entities");
2064 assert_matches!(
2065 roundtrip(&entities),
2066 Err(EntitiesError::Serialization(JsonSerializationError::ReservedKey(reserved))) if reserved.key().as_ref() == "__entity"
2067 );
2068 }
2069
2070 #[test]
2072 fn bad_action_parent() {
2073 let json = serde_json::json!(
2074 [
2075 {
2076 "uid": { "type": "XYZ::Action", "id": "view" },
2077 "attrs": {},
2078 "parents": [
2079 { "type": "User", "id": "alice" }
2080 ]
2081 }
2082 ]
2083 );
2084 let eparser: EntityJsonParser<'_, '_> =
2085 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
2086 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
2087 expect_err(
2088 &json,
2089 &miette::Report::new(e),
2090 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2091 .source(r#"action `XYZ::Action::"view"` has a non-action parent `User::"alice"`"#)
2092 .help(r#"parents of actions need to have type `Action` themselves, perhaps namespaced"#)
2093 .build()
2094 );
2095 });
2096 }
2097
2098 #[test]
2102 fn not_bad_action_parent() {
2103 let json = serde_json::json!(
2104 [
2105 {
2106 "uid": { "type": "User", "id": "alice" },
2107 "attrs": {},
2108 "parents": [
2109 { "type": "XYZ::Action", "id": "view" },
2110 ]
2111 }
2112 ]
2113 );
2114 let eparser: EntityJsonParser<'_, '_> =
2115 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
2116 eparser
2117 .from_json_value(json)
2118 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
2119 }
2120
2121 #[test]
2123 fn duplicate_keys() {
2124 let json = r#"
2127 [
2128 {
2129 "uid": { "type": "User", "id": "alice "},
2130 "attrs": {
2131 "foo": {
2132 "hello": "goodbye",
2133 "bar": 2,
2134 "spam": "eggs",
2135 "bar": 3
2136 }
2137 },
2138 "parents": []
2139 }
2140 ]
2141 "#;
2142 let eparser: EntityJsonParser<'_, '_> =
2143 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
2144 assert_matches!(eparser.from_json_str(json), Err(e) => {
2145 expect_err(
2147 json,
2148 &miette::Report::new(e),
2149 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2150 .source(r#"the key `bar` occurs two or more times in the same JSON object at line 11 column 25"#)
2151 .build()
2152 );
2153 });
2154 }
2155
2156 #[test]
2157 fn multi_arg_ext_func_calls() {
2158 let eparser: EntityJsonParser<'_, '_> =
2159 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
2160
2161 let json = serde_json::json!(
2162 {
2163 "uid": { "type": "User", "id": "alice "},
2164 "attrs": {
2165 "time": { "__extn": { "fn": "offset", "args": [{ "__extn": { "fn": "datetime", "arg": "1970-01-01" }}, { "__extn": { "fn": "duration", "arg": "1h" } }]}}
2166 },
2167 "parents": []
2168 }
2169 );
2170
2171 assert_matches!(eparser.single_from_json_value(json), Ok(entity) => {
2172 assert_matches!(entity.get("time"), Some(PartialValue::Value(Value { value: ValueKind::ExtensionValue(v), .. })) => {
2173 assert_eq!(v.func, "offset".parse().unwrap());
2174 assert_eq!(v.args[0].to_string(), r#"datetime("1970-01-01")"#);
2175 assert_eq!(v.args[1].to_string(), r#"duration("3600000ms")"#);
2176 });
2177 });
2178
2179 let json = serde_json::json!(
2182 {
2183 "uid": { "type": "User", "id": "alice "},
2184 "attrs": {
2185 "time": { "__extn": { "fn": "offset", "args": [{ "__extn": { "fn": "datetime", "arg": "1970-01-01" }}, { "__extn": { "fn": "duration", "arg": "1h" } }], "aaargs": 42}}
2186 },
2187 "parents": []
2188 }
2189 );
2190
2191 assert_matches!(eparser.single_from_json_value(json), Ok(entity) => {
2192 assert_matches!(entity.get("time"), Some(PartialValue::Value(Value { value: ValueKind::ExtensionValue(v), .. })) => {
2193 assert_eq!(v.func, "offset".parse().unwrap());
2194 assert_eq!(v.args[0].to_string(), r#"datetime("1970-01-01")"#);
2195 assert_eq!(v.args[1].to_string(), r#"duration("3600000ms")"#);
2196 });
2197 });
2198 }
2199
2200 #[test]
2201 fn serialize_unknown_no_error() {
2202 let test = serde_json::json!([{
2203 "uid" : { "type" : "A", "id" : "b" },
2204 "attrs": {
2205 "age": {
2206 "__extn": {
2207 "fn": "unknown",
2208 "arg": "890.9"
2209 }
2210 }
2211 },
2212 "parents": []
2213 }]);
2214 let eparser: EntityJsonParser<'_, '_, NoEntitiesSchema> =
2215 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
2216 let x = eparser.from_json_value(test);
2217 let y = x.unwrap().to_json_value();
2218 y.unwrap();
2220 }
2221}
2222
2223#[allow(clippy::panic)]
2225#[allow(clippy::cognitive_complexity)]
2226#[cfg(test)]
2227mod entities_tests {
2228 use super::*;
2229 use cool_asserts::assert_matches;
2230
2231 #[test]
2232 fn empty_entities() {
2233 let e = Entities::new();
2234 assert!(
2235 e.iter().next().is_none(),
2236 "The entity store should be empty"
2237 );
2238 }
2239
2240 fn test_entities() -> (Entity, Entity, Entity, Entity) {
2242 (
2243 Entity::with_uid(EntityUID::with_eid("test_principal")),
2244 Entity::with_uid(EntityUID::with_eid("test_action")),
2245 Entity::with_uid(EntityUID::with_eid("test_resource")),
2246 Entity::with_uid(EntityUID::with_eid("test")),
2247 )
2248 }
2249
2250 #[test]
2251 fn test_len() {
2252 let (e0, e1, e2, e3) = test_entities();
2253 let v = vec![e0, e1, e2, e3];
2254 let es = Entities::from_entities(
2255 v,
2256 None::<&NoEntitiesSchema>,
2257 TCComputation::ComputeNow,
2258 Extensions::all_available(),
2259 )
2260 .expect("Failed to construct entities");
2261 assert_eq!(es.len(), 4);
2262 assert!(!es.is_empty());
2263 }
2264
2265 #[test]
2266 fn test_is_empty() {
2267 let es = Entities::from_entities(
2268 vec![],
2269 None::<&NoEntitiesSchema>,
2270 TCComputation::ComputeNow,
2271 Extensions::all_available(),
2272 )
2273 .expect("Failed to construct entities");
2274 assert_eq!(es.len(), 0);
2275 assert!(es.is_empty());
2276 }
2277
2278 #[test]
2279 fn test_iter() {
2280 let (e0, e1, e2, e3) = test_entities();
2281 let v = vec![e0.clone(), e1.clone(), e2.clone(), e3.clone()];
2282 let es = Entities::from_entities(
2283 v,
2284 None::<&NoEntitiesSchema>,
2285 TCComputation::ComputeNow,
2286 Extensions::all_available(),
2287 )
2288 .expect("Failed to construct entities");
2289 let es_v = es.iter().collect::<Vec<_>>();
2290 assert!(es_v.len() == 4, "All entities should be in the vec");
2291 assert!(es_v.contains(&&e0));
2292 assert!(es_v.contains(&&e1));
2293 assert!(es_v.contains(&&e2));
2294 assert!(es_v.contains(&&e3));
2295 }
2296
2297 #[test]
2298 fn test_enforce_already_computed_fail() {
2299 let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
2303 let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
2304 let e3 = Entity::with_uid(EntityUID::with_eid("c"));
2305 e1.add_parent(EntityUID::with_eid("b"));
2306 e2.add_parent(EntityUID::with_eid("c"));
2307
2308 let es = Entities::from_entities(
2309 vec![e1, e2, e3],
2310 None::<&NoEntitiesSchema>,
2311 TCComputation::EnforceAlreadyComputed,
2312 Extensions::all_available(),
2313 );
2314 match es {
2315 Ok(_) => panic!("Was not transitively closed!"),
2316 Err(EntitiesError::TransitiveClosureError(_)) => (),
2317 Err(_) => panic!("Wrong Error!"),
2318 };
2319 }
2320
2321 #[test]
2322 fn test_enforce_already_computed_succeed() {
2323 let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
2328 let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
2329 let e3 = Entity::with_uid(EntityUID::with_eid("c"));
2330 e1.add_parent(EntityUID::with_eid("b"));
2331 e1.add_indirect_ancestor(EntityUID::with_eid("c"));
2332 e2.add_parent(EntityUID::with_eid("c"));
2333
2334 Entities::from_entities(
2335 vec![e1, e2, e3],
2336 None::<&NoEntitiesSchema>,
2337 TCComputation::EnforceAlreadyComputed,
2338 Extensions::all_available(),
2339 )
2340 .expect("Should have succeeded");
2341 }
2342
2343 #[test]
2344 fn test_remove_entities() {
2345 let aid = EntityUID::with_eid("A");
2350 let a = Entity::with_uid(aid.clone());
2351 let bid = EntityUID::with_eid("B");
2352 let b = Entity::with_uid(bid.clone());
2353 let cid = EntityUID::with_eid("C");
2354 let c = Entity::with_uid(cid.clone());
2355 let did = EntityUID::with_eid("D");
2356 let mut d = Entity::with_uid(did.clone());
2357 let eid = EntityUID::with_eid("E");
2358 let mut e = Entity::with_uid(eid.clone());
2359 let fid = EntityUID::with_eid("F");
2360 let mut f = Entity::with_uid(fid.clone());
2361 f.add_parent(aid.clone());
2362 f.add_parent(did.clone());
2363 f.add_parent(eid.clone());
2364 d.add_parent(aid.clone());
2365 d.add_parent(bid.clone());
2366 d.add_parent(cid.clone());
2367 e.add_parent(cid.clone());
2368
2369 let entities = Entities::from_entities(
2371 vec![a, b, c, d, e, f],
2372 None::<&NoEntitiesSchema>,
2373 TCComputation::ComputeNow,
2374 Extensions::all_available(),
2375 )
2376 .expect("Failed to construct entities")
2377 .remove_entities(vec![EntityUID::with_eid("D")], TCComputation::ComputeNow)
2379 .expect("Failed to remove entities");
2380 assert_matches!(entities.entity(&did), Dereference::NoSuchEntity);
2386
2387 let e = entities.entity(&eid).unwrap();
2388 let f = entities.entity(&fid).unwrap();
2389
2390 assert!(f.is_descendant_of(&aid));
2392 assert!(f.is_descendant_of(&eid));
2393 assert!(f.is_descendant_of(&cid));
2394 assert!(e.is_descendant_of(&cid));
2395
2396 assert!(!f.is_descendant_of(&bid));
2399 }
2400
2401 #[test]
2402 fn test_upsert_entities() {
2403 let aid = EntityUID::with_eid("A");
2408 let a = Entity::with_uid(aid.clone());
2409 let bid = EntityUID::with_eid("B");
2410 let b = Entity::with_uid(bid.clone());
2411 let cid = EntityUID::with_eid("C");
2412 let c = Entity::with_uid(cid.clone());
2413 let did = EntityUID::with_eid("D");
2414 let mut d = Entity::with_uid(did.clone());
2415 let eid = EntityUID::with_eid("E");
2416 let mut e = Entity::with_uid(eid.clone());
2417 let fid = EntityUID::with_eid("F");
2418 let mut f = Entity::with_uid(fid.clone());
2419 f.add_parent(aid.clone());
2420 f.add_parent(did);
2421 f.add_parent(eid.clone());
2422 d.add_parent(aid);
2423 d.add_parent(bid);
2424 d.add_parent(cid.clone());
2425 e.add_parent(cid.clone());
2426
2427 let mut f_updated = Entity::with_uid(fid.clone());
2428 f_updated.add_parent(cid.clone());
2429
2430 let gid = EntityUID::with_eid("G");
2431 let mut g = Entity::with_uid(gid.clone());
2432 g.add_parent(fid.clone());
2433
2434 let updates = vec![f_updated, g]
2435 .into_iter()
2436 .map(Arc::new)
2437 .collect::<Vec<_>>();
2438 let entities = Entities::from_entities(
2440 vec![a, b, c, d, e, f],
2441 None::<&NoEntitiesSchema>,
2442 TCComputation::ComputeNow,
2443 Extensions::all_available(),
2444 )
2445 .expect("Failed to construct entities")
2446 .upsert_entities(
2448 updates,
2449 None::<&NoEntitiesSchema>,
2450 TCComputation::ComputeNow,
2451 Extensions::all_available(),
2452 )
2453 .expect("Failed to remove entities");
2454 let g = entities.entity(&gid).unwrap();
2460 let f = entities.entity(&fid).unwrap();
2461
2462 assert!(f.is_descendant_of(&cid));
2464 assert!(g.is_descendant_of(&cid));
2465 assert!(g.is_descendant_of(&fid));
2466
2467 assert!(!f.is_descendant_of(&eid));
2469 }
2470}
2471
2472#[allow(clippy::panic)]
2474#[allow(clippy::cognitive_complexity)]
2475#[cfg(test)]
2476mod schema_based_parsing_tests {
2477 use super::json::NullEntityTypeDescription;
2478 use super::*;
2479 use crate::extensions::Extensions;
2480 use crate::test_utils::*;
2481 use cool_asserts::assert_matches;
2482 use nonempty::NonEmpty;
2483 use serde_json::json;
2484 use smol_str::SmolStr;
2485 use std::collections::{BTreeMap, HashSet};
2486 use std::sync::Arc;
2487
2488 struct MockSchema;
2490 impl Schema for MockSchema {
2491 type EntityTypeDescription = MockEmployeeDescription;
2492 type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
2493 fn entity_type(&self, entity_type: &EntityType) -> Option<MockEmployeeDescription> {
2494 match entity_type.to_string().as_str() {
2495 "Employee" => Some(MockEmployeeDescription),
2496 _ => None,
2497 }
2498 }
2499 fn action(&self, action: &EntityUID) -> Option<Arc<Entity>> {
2500 match action.to_string().as_str() {
2501 r#"Action::"view""# => Some(Arc::new(Entity::new_with_attr_partial_value(
2502 action.clone(),
2503 [(SmolStr::from("foo"), PartialValue::from(34))],
2504 HashSet::new(),
2505 HashSet::from([r#"Action::"readOnly""#.parse().expect("valid uid")]),
2506 [],
2507 ))),
2508 r#"Action::"readOnly""# => Some(Arc::new(Entity::with_uid(action.clone()))),
2509 _ => None,
2510 }
2511 }
2512 fn entity_types_with_basename<'a>(
2513 &'a self,
2514 basename: &'a UnreservedId,
2515 ) -> Box<dyn Iterator<Item = EntityType> + 'a> {
2516 match basename.as_ref() {
2517 "Employee" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
2518 basename.clone(),
2519 )))),
2520 "Action" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
2521 basename.clone(),
2522 )))),
2523 _ => Box::new(std::iter::empty()),
2524 }
2525 }
2526 fn action_entities(&self) -> Self::ActionEntityIterator {
2527 std::iter::empty()
2528 }
2529 }
2530
2531 struct MockSchemaNoTags;
2533 impl Schema for MockSchemaNoTags {
2534 type EntityTypeDescription = NullEntityTypeDescription;
2535 type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
2536 fn entity_type(&self, entity_type: &EntityType) -> Option<NullEntityTypeDescription> {
2537 match entity_type.to_string().as_str() {
2538 "Employee" => Some(NullEntityTypeDescription::new("Employee".parse().unwrap())),
2539 _ => None,
2540 }
2541 }
2542 fn action(&self, action: &EntityUID) -> Option<Arc<Entity>> {
2543 match action.to_string().as_str() {
2544 r#"Action::"view""# => Some(Arc::new(Entity::with_uid(
2545 r#"Action::"view""#.parse().expect("valid uid"),
2546 ))),
2547 _ => None,
2548 }
2549 }
2550 fn entity_types_with_basename<'a>(
2551 &'a self,
2552 basename: &'a UnreservedId,
2553 ) -> Box<dyn Iterator<Item = EntityType> + 'a> {
2554 match basename.as_ref() {
2555 "Employee" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
2556 basename.clone(),
2557 )))),
2558 "Action" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
2559 basename.clone(),
2560 )))),
2561 _ => Box::new(std::iter::empty()),
2562 }
2563 }
2564 fn action_entities(&self) -> Self::ActionEntityIterator {
2565 std::iter::empty()
2566 }
2567 }
2568
2569 struct MockEmployeeDescription;
2571 impl EntityTypeDescription for MockEmployeeDescription {
2572 fn enum_entity_eids(&self) -> Option<NonEmpty<Eid>> {
2573 None
2574 }
2575 fn entity_type(&self) -> EntityType {
2576 EntityType::from(Name::parse_unqualified_name("Employee").expect("valid"))
2577 }
2578
2579 fn attr_type(&self, attr: &str) -> Option<SchemaType> {
2580 let employee_ty = || SchemaType::Entity {
2581 ty: self.entity_type(),
2582 };
2583 let hr_ty = || SchemaType::Entity {
2584 ty: EntityType::from(Name::parse_unqualified_name("HR").expect("valid")),
2585 };
2586 match attr {
2587 "isFullTime" => Some(SchemaType::Bool),
2588 "numDirectReports" => Some(SchemaType::Long),
2589 "department" => Some(SchemaType::String),
2590 "manager" => Some(employee_ty()),
2591 "hr_contacts" => Some(SchemaType::Set {
2592 element_ty: Box::new(hr_ty()),
2593 }),
2594 "json_blob" => Some(SchemaType::Record {
2595 attrs: [
2596 ("inner1".into(), AttributeType::required(SchemaType::Bool)),
2597 ("inner2".into(), AttributeType::required(SchemaType::String)),
2598 (
2599 "inner3".into(),
2600 AttributeType::required(SchemaType::Record {
2601 attrs: BTreeMap::from([(
2602 "innerinner".into(),
2603 AttributeType::required(employee_ty()),
2604 )]),
2605 open_attrs: false,
2606 }),
2607 ),
2608 ]
2609 .into_iter()
2610 .collect(),
2611 open_attrs: false,
2612 }),
2613 "home_ip" => Some(SchemaType::Extension {
2614 name: Name::parse_unqualified_name("ipaddr").expect("valid"),
2615 }),
2616 "work_ip" => Some(SchemaType::Extension {
2617 name: Name::parse_unqualified_name("ipaddr").expect("valid"),
2618 }),
2619 "trust_score" => Some(SchemaType::Extension {
2620 name: Name::parse_unqualified_name("decimal").expect("valid"),
2621 }),
2622 "tricky" => Some(SchemaType::Record {
2623 attrs: [
2624 ("type".into(), AttributeType::required(SchemaType::String)),
2625 ("id".into(), AttributeType::required(SchemaType::String)),
2626 ]
2627 .into_iter()
2628 .collect(),
2629 open_attrs: false,
2630 }),
2631 "start_date" => Some(SchemaType::Extension {
2632 name: Name::parse_unqualified_name("datetime").expect("valid"),
2633 }),
2634 _ => None,
2635 }
2636 }
2637
2638 fn tag_type(&self) -> Option<SchemaType> {
2639 Some(SchemaType::Set {
2640 element_ty: Box::new(SchemaType::String),
2641 })
2642 }
2643
2644 fn required_attrs(&self) -> Box<dyn Iterator<Item = SmolStr>> {
2645 Box::new(
2646 [
2647 "isFullTime",
2648 "numDirectReports",
2649 "department",
2650 "manager",
2651 "hr_contacts",
2652 "json_blob",
2653 "home_ip",
2654 "work_ip",
2655 "trust_score",
2656 ]
2657 .map(SmolStr::new_static)
2658 .into_iter(),
2659 )
2660 }
2661
2662 fn allowed_parent_types(&self) -> Arc<HashSet<EntityType>> {
2663 Arc::new(HashSet::new())
2664 }
2665
2666 fn open_attributes(&self) -> bool {
2667 false
2668 }
2669 }
2670
2671 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2672 #[test]
2674 fn with_and_without_schema() {
2675 let entitiesjson = json!(
2676 [
2677 {
2678 "uid": { "type": "Employee", "id": "12UA45" },
2679 "attrs": {
2680 "isFullTime": true,
2681 "numDirectReports": 3,
2682 "department": "Sales",
2683 "manager": { "type": "Employee", "id": "34FB87" },
2684 "hr_contacts": [
2685 { "type": "HR", "id": "aaaaa" },
2686 { "type": "HR", "id": "bbbbb" }
2687 ],
2688 "json_blob": {
2689 "inner1": false,
2690 "inner2": "-*/",
2691 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2692 },
2693 "home_ip": "222.222.222.101",
2694 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2695 "trust_score": "5.7",
2696 "tricky": { "type": "Employee", "id": "34FB87" },
2697 "start_date": { "fn": "offset", "args": [
2698 {"fn": "datetime", "arg": "1970-01-01"},
2699 {"fn": "duration", "arg": "1h"}
2700 ]}
2701 },
2702 "parents": [],
2703 "tags": {
2704 "someTag": ["pancakes"],
2705 },
2706 }
2707 ]
2708 );
2709 let eparser: EntityJsonParser<'_, '_> =
2713 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
2714 let parsed = eparser
2715 .from_json_value(entitiesjson.clone())
2716 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
2717 assert_eq!(parsed.iter().count(), 1);
2718 let parsed = parsed
2719 .entity(&r#"Employee::"12UA45""#.parse().unwrap())
2720 .expect("that should be the employee id");
2721 let home_ip = parsed.get("home_ip").expect("home_ip attr should exist");
2722 assert_matches!(
2723 home_ip,
2724 &PartialValue::Value(Value {
2725 value: ValueKind::Lit(Literal::String(_)),
2726 ..
2727 }),
2728 );
2729 let trust_score = parsed
2730 .get("trust_score")
2731 .expect("trust_score attr should exist");
2732 assert_matches!(
2733 trust_score,
2734 &PartialValue::Value(Value {
2735 value: ValueKind::Lit(Literal::String(_)),
2736 ..
2737 }),
2738 );
2739 let manager = parsed.get("manager").expect("manager attr should exist");
2740 assert_matches!(
2741 manager,
2742 &PartialValue::Value(Value {
2743 value: ValueKind::Record(_),
2744 ..
2745 })
2746 );
2747 let work_ip = parsed.get("work_ip").expect("work_ip attr should exist");
2748 assert_matches!(
2749 work_ip,
2750 &PartialValue::Value(Value {
2751 value: ValueKind::Record(_),
2752 ..
2753 })
2754 );
2755 let hr_contacts = parsed
2756 .get("hr_contacts")
2757 .expect("hr_contacts attr should exist");
2758 assert_matches!(hr_contacts, PartialValue::Value(Value { value: ValueKind::Set(set), .. }) => {
2759 let contact = set.iter().next().expect("should be at least one contact");
2760 assert_matches!(contact, &Value { value: ValueKind::Record(_), .. });
2761 });
2762 let json_blob = parsed
2763 .get("json_blob")
2764 .expect("json_blob attr should exist");
2765 assert_matches!(json_blob, PartialValue::Value(Value { value: ValueKind::Record(record), .. }) => {
2766 let (_, inner1) = record
2767 .iter()
2768 .find(|(k, _)| *k == "inner1")
2769 .expect("inner1 attr should exist");
2770 assert_matches!(inner1, Value { value: ValueKind::Lit(Literal::Bool(_)), .. });
2771 let (_, inner3) = record
2772 .iter()
2773 .find(|(k, _)| *k == "inner3")
2774 .expect("inner3 attr should exist");
2775 assert_matches!(inner3, Value { value: ValueKind::Record(innerrecord), .. } => {
2776 let (_, innerinner) = innerrecord
2777 .iter()
2778 .find(|(k, _)| *k == "innerinner")
2779 .expect("innerinner attr should exist");
2780 assert_matches!(innerinner, Value { value: ValueKind::Record(_), .. });
2781 });
2782 });
2783 let eparser = EntityJsonParser::new(
2785 Some(&MockSchema),
2786 Extensions::all_available(),
2787 TCComputation::ComputeNow,
2788 );
2789 let parsed = eparser
2790 .from_json_value(entitiesjson)
2791 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
2792 assert_eq!(parsed.iter().count(), 1);
2793 let parsed = parsed
2794 .entity(&r#"Employee::"12UA45""#.parse().unwrap())
2795 .expect("that should be the employee id");
2796 let is_full_time = parsed
2797 .get("isFullTime")
2798 .expect("isFullTime attr should exist");
2799 assert_eq!(is_full_time, &PartialValue::Value(Value::from(true)),);
2800 let some_tag = parsed
2801 .get_tag("someTag")
2802 .expect("someTag attr should exist");
2803 assert_eq!(
2804 some_tag,
2805 &PartialValue::Value(Value::set(["pancakes".into()], None))
2806 );
2807 let num_direct_reports = parsed
2808 .get("numDirectReports")
2809 .expect("numDirectReports attr should exist");
2810 assert_eq!(num_direct_reports, &PartialValue::Value(Value::from(3)),);
2811 let department = parsed
2812 .get("department")
2813 .expect("department attr should exist");
2814 assert_eq!(department, &PartialValue::Value(Value::from("Sales")),);
2815 let manager = parsed.get("manager").expect("manager attr should exist");
2816 assert_eq!(
2817 manager,
2818 &PartialValue::Value(Value::from(
2819 "Employee::\"34FB87\"".parse::<EntityUID>().expect("valid")
2820 )),
2821 );
2822 let hr_contacts = parsed
2823 .get("hr_contacts")
2824 .expect("hr_contacts attr should exist");
2825 assert_matches!(hr_contacts, PartialValue::Value(Value { value: ValueKind::Set(set), .. }) => {
2826 let contact = set.iter().next().expect("should be at least one contact");
2827 assert_matches!(contact, &Value { value: ValueKind::Lit(Literal::EntityUID(_)), .. });
2828 });
2829 let json_blob = parsed
2830 .get("json_blob")
2831 .expect("json_blob attr should exist");
2832 assert_matches!(json_blob, PartialValue::Value(Value { value: ValueKind::Record(record), .. }) => {
2833 let (_, inner1) = record
2834 .iter()
2835 .find(|(k, _)| *k == "inner1")
2836 .expect("inner1 attr should exist");
2837 assert_matches!(inner1, Value { value: ValueKind::Lit(Literal::Bool(_)), .. });
2838 let (_, inner3) = record
2839 .iter()
2840 .find(|(k, _)| *k == "inner3")
2841 .expect("inner3 attr should exist");
2842 assert_matches!(inner3, Value { value: ValueKind::Record(innerrecord), .. } => {
2843 let (_, innerinner) = innerrecord
2844 .iter()
2845 .find(|(k, _)| *k == "innerinner")
2846 .expect("innerinner attr should exist");
2847 assert_matches!(innerinner, Value { value: ValueKind::Lit(Literal::EntityUID(_)), .. });
2848 });
2849 });
2850 assert_eq!(
2851 parsed.get("home_ip").cloned().map(RestrictedExpr::try_from),
2852 Some(Ok(RestrictedExpr::call_extension_fn(
2853 Name::parse_unqualified_name("ip").expect("valid"),
2854 vec![RestrictedExpr::val("222.222.222.101")]
2855 ))),
2856 );
2857 assert_eq!(
2858 parsed.get("work_ip").cloned().map(RestrictedExpr::try_from),
2859 Some(Ok(RestrictedExpr::call_extension_fn(
2860 Name::parse_unqualified_name("ip").expect("valid"),
2861 vec![RestrictedExpr::val("2.2.2.0/24")]
2862 ))),
2863 );
2864 assert_eq!(
2865 parsed
2866 .get("trust_score")
2867 .cloned()
2868 .map(RestrictedExpr::try_from),
2869 Some(Ok(RestrictedExpr::call_extension_fn(
2870 Name::parse_unqualified_name("decimal").expect("valid"),
2871 vec![RestrictedExpr::val("5.7")]
2872 ))),
2873 );
2874 }
2875
2876 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2877 #[test]
2879 fn type_mismatch_string_long() {
2880 let entitiesjson = json!(
2881 [
2882 {
2883 "uid": { "type": "Employee", "id": "12UA45" },
2884 "attrs": {
2885 "isFullTime": true,
2886 "numDirectReports": "3",
2887 "department": "Sales",
2888 "manager": { "type": "Employee", "id": "34FB87" },
2889 "hr_contacts": [
2890 { "type": "HR", "id": "aaaaa" },
2891 { "type": "HR", "id": "bbbbb" }
2892 ],
2893 "json_blob": {
2894 "inner1": false,
2895 "inner2": "-*/",
2896 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2897 },
2898 "home_ip": "222.222.222.101",
2899 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2900 "trust_score": "5.7",
2901 "tricky": { "type": "Employee", "id": "34FB87" }
2902 },
2903 "parents": []
2904 }
2905 ]
2906 );
2907 let eparser = EntityJsonParser::new(
2908 Some(&MockSchema),
2909 Extensions::all_available(),
2910 TCComputation::ComputeNow,
2911 );
2912 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2913 expect_err(
2914 &entitiesjson,
2915 &miette::Report::new(e),
2916 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2917 .source(r#"in attribute `numDirectReports` on `Employee::"12UA45"`, type mismatch: value was expected to have type long, but it actually has type string: `"3"`"#)
2918 .build()
2919 );
2920 });
2921 }
2922
2923 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2924 #[test]
2926 fn type_mismatch_entity_record() {
2927 let entitiesjson = json!(
2928 [
2929 {
2930 "uid": { "type": "Employee", "id": "12UA45" },
2931 "attrs": {
2932 "isFullTime": true,
2933 "numDirectReports": 3,
2934 "department": "Sales",
2935 "manager": "34FB87",
2936 "hr_contacts": [
2937 { "type": "HR", "id": "aaaaa" },
2938 { "type": "HR", "id": "bbbbb" }
2939 ],
2940 "json_blob": {
2941 "inner1": false,
2942 "inner2": "-*/",
2943 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2944 },
2945 "home_ip": "222.222.222.101",
2946 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2947 "trust_score": "5.7",
2948 "tricky": { "type": "Employee", "id": "34FB87" }
2949 },
2950 "parents": []
2951 }
2952 ]
2953 );
2954 let eparser = EntityJsonParser::new(
2955 Some(&MockSchema),
2956 Extensions::all_available(),
2957 TCComputation::ComputeNow,
2958 );
2959 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2960 expect_err(
2961 &entitiesjson,
2962 &miette::Report::new(e),
2963 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2964 .source(r#"in attribute `manager` on `Employee::"12UA45"`, expected a literal entity reference, but got `"34FB87"`"#)
2965 .help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
2966 .build()
2967 );
2968 });
2969 }
2970
2971 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2972 #[test]
2974 fn type_mismatch_set_element() {
2975 let entitiesjson = json!(
2976 [
2977 {
2978 "uid": { "type": "Employee", "id": "12UA45" },
2979 "attrs": {
2980 "isFullTime": true,
2981 "numDirectReports": 3,
2982 "department": "Sales",
2983 "manager": { "type": "Employee", "id": "34FB87" },
2984 "hr_contacts": { "type": "HR", "id": "aaaaa" },
2985 "json_blob": {
2986 "inner1": false,
2987 "inner2": "-*/",
2988 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2989 },
2990 "home_ip": "222.222.222.101",
2991 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2992 "trust_score": "5.7",
2993 "tricky": { "type": "Employee", "id": "34FB87" }
2994 },
2995 "parents": []
2996 }
2997 ]
2998 );
2999 let eparser = EntityJsonParser::new(
3000 Some(&MockSchema),
3001 Extensions::all_available(),
3002 TCComputation::ComputeNow,
3003 );
3004 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3005 expect_err(
3006 &entitiesjson,
3007 &miette::Report::new(e),
3008 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3009 .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"}`"#)
3010 .build()
3011 );
3012 });
3013 }
3014
3015 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3016 #[test]
3018 fn type_mismatch_entity_types() {
3019 let entitiesjson = json!(
3020 [
3021 {
3022 "uid": { "type": "Employee", "id": "12UA45" },
3023 "attrs": {
3024 "isFullTime": true,
3025 "numDirectReports": 3,
3026 "department": "Sales",
3027 "manager": { "type": "HR", "id": "34FB87" },
3028 "hr_contacts": [
3029 { "type": "HR", "id": "aaaaa" },
3030 { "type": "HR", "id": "bbbbb" }
3031 ],
3032 "json_blob": {
3033 "inner1": false,
3034 "inner2": "-*/",
3035 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3036 },
3037 "home_ip": "222.222.222.101",
3038 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3039 "trust_score": "5.7",
3040 "tricky": { "type": "Employee", "id": "34FB87" }
3041 },
3042 "parents": []
3043 }
3044 ]
3045 );
3046 let eparser = EntityJsonParser::new(
3047 Some(&MockSchema),
3048 Extensions::all_available(),
3049 TCComputation::ComputeNow,
3050 );
3051 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3052 expect_err(
3053 &entitiesjson,
3054 &miette::Report::new(e),
3055 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3056 .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"`"#)
3057 .build()
3058 );
3059 });
3060 }
3061
3062 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3063 #[test]
3066 fn type_mismatch_extension_types() {
3067 let entitiesjson = json!(
3068 [
3069 {
3070 "uid": { "type": "Employee", "id": "12UA45" },
3071 "attrs": {
3072 "isFullTime": true,
3073 "numDirectReports": 3,
3074 "department": "Sales",
3075 "manager": { "type": "Employee", "id": "34FB87" },
3076 "hr_contacts": [
3077 { "type": "HR", "id": "aaaaa" },
3078 { "type": "HR", "id": "bbbbb" }
3079 ],
3080 "json_blob": {
3081 "inner1": false,
3082 "inner2": "-*/",
3083 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3084 },
3085 "home_ip": { "fn": "decimal", "arg": "3.33" },
3086 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3087 "trust_score": "5.7",
3088 "tricky": { "type": "Employee", "id": "34FB87" }
3089 },
3090 "parents": []
3091 }
3092 ]
3093 );
3094 let eparser = EntityJsonParser::new(
3095 Some(&MockSchema),
3096 Extensions::all_available(),
3097 TCComputation::ComputeNow,
3098 );
3099 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3100 expect_err(
3101 &entitiesjson,
3102 &miette::Report::new(e),
3103 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3104 .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")`"#)
3105 .build()
3106 );
3107 });
3108 }
3109
3110 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3111 #[test]
3112 fn missing_record_attr() {
3113 let entitiesjson = json!(
3115 [
3116 {
3117 "uid": { "type": "Employee", "id": "12UA45" },
3118 "attrs": {
3119 "isFullTime": true,
3120 "numDirectReports": 3,
3121 "department": "Sales",
3122 "manager": { "type": "Employee", "id": "34FB87" },
3123 "hr_contacts": [
3124 { "type": "HR", "id": "aaaaa" },
3125 { "type": "HR", "id": "bbbbb" }
3126 ],
3127 "json_blob": {
3128 "inner1": false,
3129 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3130 },
3131 "home_ip": "222.222.222.101",
3132 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3133 "trust_score": "5.7",
3134 "tricky": { "type": "Employee", "id": "34FB87" }
3135 },
3136 "parents": []
3137 }
3138 ]
3139 );
3140 let eparser = EntityJsonParser::new(
3141 Some(&MockSchema),
3142 Extensions::all_available(),
3143 TCComputation::ComputeNow,
3144 );
3145 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3146 expect_err(
3147 &entitiesjson,
3148 &miette::Report::new(e),
3149 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3150 .source(r#"in attribute `json_blob` on `Employee::"12UA45"`, expected the record to have an attribute `inner2`, but it does not"#)
3151 .build()
3152 );
3153 });
3154 }
3155
3156 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3157 #[test]
3159 fn type_mismatch_in_record_attr() {
3160 let entitiesjson = json!(
3161 [
3162 {
3163 "uid": { "type": "Employee", "id": "12UA45" },
3164 "attrs": {
3165 "isFullTime": true,
3166 "numDirectReports": 3,
3167 "department": "Sales",
3168 "manager": { "type": "Employee", "id": "34FB87" },
3169 "hr_contacts": [
3170 { "type": "HR", "id": "aaaaa" },
3171 { "type": "HR", "id": "bbbbb" }
3172 ],
3173 "json_blob": {
3174 "inner1": 33,
3175 "inner2": "-*/",
3176 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3177 },
3178 "home_ip": "222.222.222.101",
3179 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3180 "trust_score": "5.7",
3181 "tricky": { "type": "Employee", "id": "34FB87" }
3182 },
3183 "parents": []
3184 }
3185 ]
3186 );
3187 let eparser = EntityJsonParser::new(
3188 Some(&MockSchema),
3189 Extensions::all_available(),
3190 TCComputation::ComputeNow,
3191 );
3192 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3193 expect_err(
3194 &entitiesjson,
3195 &miette::Report::new(e),
3196 &ExpectedErrorMessageBuilder::error_starts_with("entity does not conform to the schema")
3197 .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`"#)
3198 .build()
3199 );
3200 });
3201
3202 let entitiesjson = json!(
3204 [
3205 {
3206 "uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
3207 "attrs": {
3208 "isFullTime": true,
3209 "numDirectReports": 3,
3210 "department": "Sales",
3211 "manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
3212 "hr_contacts": [
3213 { "type": "HR", "id": "aaaaa" },
3214 { "type": "HR", "id": "bbbbb" }
3215 ],
3216 "json_blob": {
3217 "inner1": false,
3218 "inner2": "-*/",
3219 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3220 },
3221 "home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
3222 "work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
3223 "trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
3224 "tricky": { "type": "Employee", "id": "34FB87" }
3225 },
3226 "parents": []
3227 }
3228 ]
3229 );
3230 let _ = eparser
3231 .from_json_value(entitiesjson)
3232 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
3233 }
3234
3235 #[test]
3237 fn type_mismatch_in_tag() {
3238 let entitiesjson = json!(
3239 [
3240 {
3241 "uid": { "type": "Employee", "id": "12UA45" },
3242 "attrs": {
3243 "isFullTime": true,
3244 "numDirectReports": 3,
3245 "department": "Sales",
3246 "manager": { "type": "Employee", "id": "34FB87" },
3247 "hr_contacts": [
3248 { "type": "HR", "id": "aaaaa" },
3249 { "type": "HR", "id": "bbbbb" }
3250 ],
3251 "json_blob": {
3252 "inner1": false,
3253 "inner2": "-*/",
3254 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3255 },
3256 "home_ip": "222.222.222.101",
3257 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3258 "trust_score": "5.7",
3259 "tricky": { "type": "Employee", "id": "34FB87" }
3260 },
3261 "parents": [],
3262 "tags": {
3263 "someTag": "pancakes",
3264 }
3265 }
3266 ]
3267 );
3268 let eparser = EntityJsonParser::new(
3269 Some(&MockSchema),
3270 Extensions::all_available(),
3271 TCComputation::ComputeNow,
3272 );
3273 let expected_error_msg =
3274 ExpectedErrorMessageBuilder::error_starts_with("error during entity deserialization")
3275 .source(r#"in tag `someTag` on `Employee::"12UA45"`, type mismatch: value was expected to have type [string], but it actually has type string: `"pancakes"`"#)
3276 .build();
3277 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3278 expect_err(
3279 &entitiesjson,
3280 &miette::Report::new(e),
3281 &expected_error_msg,
3282 );
3283 });
3284 }
3285
3286 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3287 #[test]
3289 fn unexpected_record_attr() {
3290 let entitiesjson = json!(
3291 [
3292 {
3293 "uid": { "type": "Employee", "id": "12UA45" },
3294 "attrs": {
3295 "isFullTime": true,
3296 "numDirectReports": 3,
3297 "department": "Sales",
3298 "manager": { "type": "Employee", "id": "34FB87" },
3299 "hr_contacts": [
3300 { "type": "HR", "id": "aaaaa" },
3301 { "type": "HR", "id": "bbbbb" }
3302 ],
3303 "json_blob": {
3304 "inner1": false,
3305 "inner2": "-*/",
3306 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3307 "inner4": "wat?"
3308 },
3309 "home_ip": "222.222.222.101",
3310 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3311 "trust_score": "5.7",
3312 "tricky": { "type": "Employee", "id": "34FB87" }
3313 },
3314 "parents": []
3315 }
3316 ]
3317 );
3318 let eparser = EntityJsonParser::new(
3319 Some(&MockSchema),
3320 Extensions::all_available(),
3321 TCComputation::ComputeNow,
3322 );
3323 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3324 expect_err(
3325 &entitiesjson,
3326 &miette::Report::new(e),
3327 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3328 .source(r#"in attribute `json_blob` on `Employee::"12UA45"`, record attribute `inner4` should not exist according to the schema"#)
3329 .build()
3330 );
3331 });
3332 }
3333
3334 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3335 #[test]
3337 fn missing_required_attr() {
3338 let entitiesjson = json!(
3339 [
3340 {
3341 "uid": { "type": "Employee", "id": "12UA45" },
3342 "attrs": {
3343 "isFullTime": true,
3344 "department": "Sales",
3345 "manager": { "type": "Employee", "id": "34FB87" },
3346 "hr_contacts": [
3347 { "type": "HR", "id": "aaaaa" },
3348 { "type": "HR", "id": "bbbbb" }
3349 ],
3350 "json_blob": {
3351 "inner1": false,
3352 "inner2": "-*/",
3353 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3354 },
3355 "home_ip": "222.222.222.101",
3356 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3357 "trust_score": "5.7",
3358 "tricky": { "type": "Employee", "id": "34FB87" }
3359 },
3360 "parents": []
3361 }
3362 ]
3363 );
3364 let eparser = EntityJsonParser::new(
3365 Some(&MockSchema),
3366 Extensions::all_available(),
3367 TCComputation::ComputeNow,
3368 );
3369 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3370 expect_err(
3371 &entitiesjson,
3372 &miette::Report::new(e),
3373 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3374 .source(r#"expected entity `Employee::"12UA45"` to have attribute `numDirectReports`, but it does not"#)
3375 .build()
3376 );
3377 });
3378 }
3379
3380 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3381 #[test]
3383 fn unexpected_entity_attr() {
3384 let entitiesjson = json!(
3385 [
3386 {
3387 "uid": { "type": "Employee", "id": "12UA45" },
3388 "attrs": {
3389 "isFullTime": true,
3390 "numDirectReports": 3,
3391 "department": "Sales",
3392 "manager": { "type": "Employee", "id": "34FB87" },
3393 "hr_contacts": [
3394 { "type": "HR", "id": "aaaaa" },
3395 { "type": "HR", "id": "bbbbb" }
3396 ],
3397 "json_blob": {
3398 "inner1": false,
3399 "inner2": "-*/",
3400 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3401 },
3402 "home_ip": "222.222.222.101",
3403 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3404 "trust_score": "5.7",
3405 "tricky": { "type": "Employee", "id": "34FB87" },
3406 "wat": "???",
3407 },
3408 "parents": []
3409 }
3410 ]
3411 );
3412 let eparser = EntityJsonParser::new(
3413 Some(&MockSchema),
3414 Extensions::all_available(),
3415 TCComputation::ComputeNow,
3416 );
3417 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3418 expect_err(
3419 &entitiesjson,
3420 &miette::Report::new(e),
3421 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3422 .source(r#"attribute `wat` on `Employee::"12UA45"` should not exist according to the schema"#)
3423 .build()
3424 );
3425 });
3426 }
3427
3428 #[test]
3430 fn unexpected_entity_tag() {
3431 let entitiesjson = json!(
3432 [
3433 {
3434 "uid": { "type": "Employee", "id": "12UA45" },
3435 "attrs": {},
3436 "parents": [],
3437 "tags": {
3438 "someTag": 12,
3439 }
3440 }
3441 ]
3442 );
3443 let eparser = EntityJsonParser::new(
3444 Some(&MockSchemaNoTags),
3445 Extensions::all_available(),
3446 TCComputation::ComputeNow,
3447 );
3448 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3449 expect_err(
3450 &entitiesjson,
3451 &miette::Report::new(e),
3452 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3453 .source(r#"found a tag `someTag` on `Employee::"12UA45"`, but no tags should exist on `Employee::"12UA45"` according to the schema"#)
3454 .build()
3455 );
3456 });
3457 }
3458
3459 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
3460 #[test]
3462 fn parents_wrong_type() {
3463 let entitiesjson = json!(
3464 [
3465 {
3466 "uid": { "type": "Employee", "id": "12UA45" },
3467 "attrs": {
3468 "isFullTime": true,
3469 "numDirectReports": 3,
3470 "department": "Sales",
3471 "manager": { "type": "Employee", "id": "34FB87" },
3472 "hr_contacts": [
3473 { "type": "HR", "id": "aaaaa" },
3474 { "type": "HR", "id": "bbbbb" }
3475 ],
3476 "json_blob": {
3477 "inner1": false,
3478 "inner2": "-*/",
3479 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3480 },
3481 "home_ip": "222.222.222.101",
3482 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3483 "trust_score": "5.7",
3484 "tricky": { "type": "Employee", "id": "34FB87" }
3485 },
3486 "parents": [
3487 { "type": "Employee", "id": "34FB87" }
3488 ]
3489 }
3490 ]
3491 );
3492 let eparser = EntityJsonParser::new(
3493 Some(&MockSchema),
3494 Extensions::all_available(),
3495 TCComputation::ComputeNow,
3496 );
3497 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3498 expect_err(
3499 &entitiesjson,
3500 &miette::Report::new(e),
3501 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3502 .source(r#"`Employee::"12UA45"` is not allowed to have an ancestor of type `Employee` according to the schema"#)
3503 .build()
3504 );
3505 });
3506 }
3507
3508 #[test]
3510 fn undeclared_entity_type() {
3511 let entitiesjson = json!(
3512 [
3513 {
3514 "uid": { "type": "CEO", "id": "abcdef" },
3515 "attrs": {},
3516 "parents": []
3517 }
3518 ]
3519 );
3520 let eparser = EntityJsonParser::new(
3521 Some(&MockSchema),
3522 Extensions::all_available(),
3523 TCComputation::ComputeNow,
3524 );
3525 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3526 expect_err(
3527 &entitiesjson,
3528 &miette::Report::new(e),
3529 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3530 .source(r#"entity `CEO::"abcdef"` has type `CEO` which is not declared in the schema"#)
3531 .build()
3532 );
3533 });
3534 }
3535
3536 #[test]
3538 fn undeclared_action() {
3539 let entitiesjson = json!(
3540 [
3541 {
3542 "uid": { "type": "Action", "id": "update" },
3543 "attrs": {},
3544 "parents": []
3545 }
3546 ]
3547 );
3548 let eparser = EntityJsonParser::new(
3549 Some(&MockSchema),
3550 Extensions::all_available(),
3551 TCComputation::ComputeNow,
3552 );
3553 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3554 expect_err(
3555 &entitiesjson,
3556 &miette::Report::new(e),
3557 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3558 .source(r#"found action entity `Action::"update"`, but it was not declared as an action in the schema"#)
3559 .build()
3560 );
3561 });
3562 }
3563
3564 #[test]
3566 fn action_declared_both_places() {
3567 let entitiesjson = json!(
3568 [
3569 {
3570 "uid": { "type": "Action", "id": "view" },
3571 "attrs": {
3572 "foo": 34
3573 },
3574 "parents": [
3575 { "type": "Action", "id": "readOnly" }
3576 ]
3577 }
3578 ]
3579 );
3580 let eparser = EntityJsonParser::new(
3581 Some(&MockSchema),
3582 Extensions::all_available(),
3583 TCComputation::ComputeNow,
3584 );
3585 let entities = eparser
3586 .from_json_value(entitiesjson)
3587 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
3588 assert_eq!(entities.iter().count(), 1);
3589 let expected_uid = r#"Action::"view""#.parse().expect("valid uid");
3590 let parsed_entity = match entities.entity(&expected_uid) {
3591 Dereference::Data(e) => e,
3592 _ => panic!("expected entity to exist and be concrete"),
3593 };
3594 assert_eq!(parsed_entity.uid(), &expected_uid);
3595 }
3596
3597 #[test]
3599 fn action_attr_wrong_val() {
3600 let entitiesjson = json!(
3601 [
3602 {
3603 "uid": { "type": "Action", "id": "view" },
3604 "attrs": {
3605 "foo": 6789
3606 },
3607 "parents": [
3608 { "type": "Action", "id": "readOnly" }
3609 ]
3610 }
3611 ]
3612 );
3613 let eparser = EntityJsonParser::new(
3614 Some(&MockSchema),
3615 Extensions::all_available(),
3616 TCComputation::ComputeNow,
3617 );
3618 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3619 expect_err(
3620 &entitiesjson,
3621 &miette::Report::new(e),
3622 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3623 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3624 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3625 .build()
3626 );
3627 });
3628 }
3629
3630 #[test]
3632 fn action_attr_wrong_type() {
3633 let entitiesjson = json!(
3634 [
3635 {
3636 "uid": { "type": "Action", "id": "view" },
3637 "attrs": {
3638 "foo": "bar"
3639 },
3640 "parents": [
3641 { "type": "Action", "id": "readOnly" }
3642 ]
3643 }
3644 ]
3645 );
3646 let eparser = EntityJsonParser::new(
3647 Some(&MockSchema),
3648 Extensions::all_available(),
3649 TCComputation::ComputeNow,
3650 );
3651 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3652 expect_err(
3653 &entitiesjson,
3654 &miette::Report::new(e),
3655 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3656 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3657 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3658 .build()
3659 );
3660 });
3661 }
3662
3663 #[test]
3665 fn action_attr_missing_in_json() {
3666 let entitiesjson = json!(
3667 [
3668 {
3669 "uid": { "type": "Action", "id": "view" },
3670 "attrs": {},
3671 "parents": [
3672 { "type": "Action", "id": "readOnly" }
3673 ]
3674 }
3675 ]
3676 );
3677 let eparser = EntityJsonParser::new(
3678 Some(&MockSchema),
3679 Extensions::all_available(),
3680 TCComputation::ComputeNow,
3681 );
3682 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3683 expect_err(
3684 &entitiesjson,
3685 &miette::Report::new(e),
3686 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3687 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3688 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3689 .build()
3690 );
3691 });
3692 }
3693
3694 #[test]
3696 fn action_attr_missing_in_schema() {
3697 let entitiesjson = json!(
3698 [
3699 {
3700 "uid": { "type": "Action", "id": "view" },
3701 "attrs": {
3702 "foo": "bar",
3703 "wow": false
3704 },
3705 "parents": [
3706 { "type": "Action", "id": "readOnly" }
3707 ]
3708 }
3709 ]
3710 );
3711 let eparser = EntityJsonParser::new(
3712 Some(&MockSchema),
3713 Extensions::all_available(),
3714 TCComputation::ComputeNow,
3715 );
3716 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3717 expect_err(
3718 &entitiesjson,
3719 &miette::Report::new(e),
3720 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3721 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3722 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3723 .build()
3724 );
3725 });
3726 }
3727
3728 #[test]
3730 fn action_parent_missing_in_json() {
3731 let entitiesjson = json!(
3732 [
3733 {
3734 "uid": { "type": "Action", "id": "view" },
3735 "attrs": {
3736 "foo": 34
3737 },
3738 "parents": []
3739 }
3740 ]
3741 );
3742 let eparser = EntityJsonParser::new(
3743 Some(&MockSchema),
3744 Extensions::all_available(),
3745 TCComputation::ComputeNow,
3746 );
3747 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3748 expect_err(
3749 &entitiesjson,
3750 &miette::Report::new(e),
3751 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3752 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3753 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3754 .build()
3755 );
3756 });
3757 }
3758
3759 #[test]
3761 fn action_parent_missing_in_schema() {
3762 let entitiesjson = json!(
3763 [
3764 {
3765 "uid": { "type": "Action", "id": "view" },
3766 "attrs": {
3767 "foo": 34
3768 },
3769 "parents": [
3770 { "type": "Action", "id": "readOnly" },
3771 { "type": "Action", "id": "coolActions" }
3772 ]
3773 }
3774 ]
3775 );
3776 let eparser = EntityJsonParser::new(
3777 Some(&MockSchema),
3778 Extensions::all_available(),
3779 TCComputation::ComputeNow,
3780 );
3781 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3782 expect_err(
3783 &entitiesjson,
3784 &miette::Report::new(e),
3785 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3786 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3787 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3788 .build()
3789 );
3790 });
3791 }
3792
3793 #[test]
3795 fn namespaces() {
3796 use std::str::FromStr;
3797
3798 struct MockSchema;
3799 impl Schema for MockSchema {
3800 type EntityTypeDescription = MockEmployeeDescription;
3801 type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
3802 fn entity_type(&self, entity_type: &EntityType) -> Option<MockEmployeeDescription> {
3803 if &entity_type.to_string() == "XYZCorp::Employee" {
3804 Some(MockEmployeeDescription)
3805 } else {
3806 None
3807 }
3808 }
3809 fn action(&self, _action: &EntityUID) -> Option<Arc<Entity>> {
3810 None
3811 }
3812 fn entity_types_with_basename<'a>(
3813 &'a self,
3814 basename: &'a UnreservedId,
3815 ) -> Box<dyn Iterator<Item = EntityType> + 'a> {
3816 match basename.as_ref() {
3817 "Employee" => Box::new(std::iter::once(EntityType::from(
3818 Name::from_str("XYZCorp::Employee").expect("valid name"),
3819 ))),
3820 _ => Box::new(std::iter::empty()),
3821 }
3822 }
3823 fn action_entities(&self) -> Self::ActionEntityIterator {
3824 std::iter::empty()
3825 }
3826 }
3827
3828 struct MockEmployeeDescription;
3829 impl EntityTypeDescription for MockEmployeeDescription {
3830 fn enum_entity_eids(&self) -> Option<NonEmpty<Eid>> {
3831 None
3832 }
3833 fn entity_type(&self) -> EntityType {
3834 "XYZCorp::Employee".parse().expect("valid")
3835 }
3836
3837 fn attr_type(&self, attr: &str) -> Option<SchemaType> {
3838 match attr {
3839 "isFullTime" => Some(SchemaType::Bool),
3840 "department" => Some(SchemaType::String),
3841 "manager" => Some(SchemaType::Entity {
3842 ty: self.entity_type(),
3843 }),
3844 _ => None,
3845 }
3846 }
3847
3848 fn tag_type(&self) -> Option<SchemaType> {
3849 None
3850 }
3851
3852 fn required_attrs(&self) -> Box<dyn Iterator<Item = SmolStr>> {
3853 Box::new(
3854 ["isFullTime", "department", "manager"]
3855 .map(SmolStr::new_static)
3856 .into_iter(),
3857 )
3858 }
3859
3860 fn allowed_parent_types(&self) -> Arc<HashSet<EntityType>> {
3861 Arc::new(HashSet::new())
3862 }
3863
3864 fn open_attributes(&self) -> bool {
3865 false
3866 }
3867 }
3868
3869 let entitiesjson = json!(
3870 [
3871 {
3872 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3873 "attrs": {
3874 "isFullTime": true,
3875 "department": "Sales",
3876 "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
3877 },
3878 "parents": []
3879 }
3880 ]
3881 );
3882 let eparser = EntityJsonParser::new(
3883 Some(&MockSchema),
3884 Extensions::all_available(),
3885 TCComputation::ComputeNow,
3886 );
3887 let parsed = eparser
3888 .from_json_value(entitiesjson)
3889 .unwrap_or_else(|e| panic!("{:?}", &miette::Report::new(e)));
3890 assert_eq!(parsed.iter().count(), 1);
3891 let parsed = parsed
3892 .entity(&r#"XYZCorp::Employee::"12UA45""#.parse().unwrap())
3893 .expect("that should be the employee type and id");
3894 let is_full_time = parsed
3895 .get("isFullTime")
3896 .expect("isFullTime attr should exist");
3897 assert_eq!(is_full_time, &PartialValue::from(true));
3898 let department = parsed
3899 .get("department")
3900 .expect("department attr should exist");
3901 assert_eq!(department, &PartialValue::from("Sales"),);
3902 let manager = parsed.get("manager").expect("manager attr should exist");
3903 assert_eq!(
3904 manager,
3905 &PartialValue::from(
3906 "XYZCorp::Employee::\"34FB87\""
3907 .parse::<EntityUID>()
3908 .expect("valid")
3909 ),
3910 );
3911
3912 let entitiesjson = json!(
3913 [
3914 {
3915 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3916 "attrs": {
3917 "isFullTime": true,
3918 "department": "Sales",
3919 "manager": { "type": "Employee", "id": "34FB87" }
3920 },
3921 "parents": []
3922 }
3923 ]
3924 );
3925
3926 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3927 expect_err(
3928 &entitiesjson,
3929 &miette::Report::new(e),
3930 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3931 .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"`"#)
3932 .build()
3933 );
3934 });
3935
3936 let entitiesjson = json!(
3937 [
3938 {
3939 "uid": { "type": "Employee", "id": "12UA45" },
3940 "attrs": {
3941 "isFullTime": true,
3942 "department": "Sales",
3943 "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
3944 },
3945 "parents": []
3946 }
3947 ]
3948 );
3949
3950 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3951 expect_err(
3952 &entitiesjson,
3953 &miette::Report::new(e),
3954 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3955 .source(r#"entity `Employee::"12UA45"` has type `Employee` which is not declared in the schema"#)
3956 .help(r#"did you mean `XYZCorp::Employee`?"#)
3957 .build()
3958 );
3959 });
3960 }
3961
3962 #[test]
3963 fn enumerated_entities() {
3964 struct MockSchema;
3965 struct StarTypeDescription;
3966 impl EntityTypeDescription for StarTypeDescription {
3967 fn entity_type(&self) -> EntityType {
3968 "Star".parse().unwrap()
3969 }
3970
3971 fn attr_type(&self, _attr: &str) -> Option<SchemaType> {
3972 None
3973 }
3974
3975 fn tag_type(&self) -> Option<SchemaType> {
3976 None
3977 }
3978
3979 fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
3980 Box::new(std::iter::empty())
3981 }
3982
3983 fn allowed_parent_types(&self) -> Arc<HashSet<EntityType>> {
3984 Arc::new(HashSet::new())
3985 }
3986
3987 fn open_attributes(&self) -> bool {
3988 false
3989 }
3990
3991 fn enum_entity_eids(&self) -> Option<NonEmpty<Eid>> {
3992 Some(nonempty::nonempty![Eid::new("🌎"), Eid::new("🌕"),])
3993 }
3994 }
3995 impl Schema for MockSchema {
3996 type EntityTypeDescription = StarTypeDescription;
3997
3998 type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
3999
4000 fn entity_type(&self, entity_type: &EntityType) -> Option<Self::EntityTypeDescription> {
4001 if entity_type == &"Star".parse::<EntityType>().unwrap() {
4002 Some(StarTypeDescription)
4003 } else {
4004 None
4005 }
4006 }
4007
4008 fn action(&self, _action: &EntityUID) -> Option<Arc<Entity>> {
4009 None
4010 }
4011
4012 fn entity_types_with_basename<'a>(
4013 &'a self,
4014 basename: &'a UnreservedId,
4015 ) -> Box<dyn Iterator<Item = EntityType> + 'a> {
4016 if basename == &"Star".parse::<UnreservedId>().unwrap() {
4017 Box::new(std::iter::once("Star".parse::<EntityType>().unwrap()))
4018 } else {
4019 Box::new(std::iter::empty())
4020 }
4021 }
4022
4023 fn action_entities(&self) -> Self::ActionEntityIterator {
4024 std::iter::empty()
4025 }
4026 }
4027
4028 let eparser = EntityJsonParser::new(
4029 Some(&MockSchema),
4030 Extensions::none(),
4031 TCComputation::ComputeNow,
4032 );
4033
4034 assert_matches!(
4035 eparser.from_json_value(serde_json::json!([
4036 {
4037 "uid": { "type": "Star", "id": "🌎" },
4038 "attrs": {},
4039 "parents": [],
4040 }
4041 ])),
4042 Ok(_)
4043 );
4044
4045 let entitiesjson = serde_json::json!([
4046 {
4047 "uid": { "type": "Star", "id": "🪐" },
4048 "attrs": {},
4049 "parents": [],
4050 }
4051 ]);
4052 assert_matches!(eparser.from_json_value(entitiesjson.clone()),
4053 Err(e) => {
4054 expect_err(
4055 &entitiesjson,
4056 &miette::Report::new(e),
4057 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
4058 .source(r#"entity `Star::"🪐"` is of an enumerated entity type, but `"🪐"` is not declared as a valid eid"#)
4059 .help(r#"valid entity eids: "🌎", "🌕""#)
4060 .build()
4061 );
4062 });
4063 }
4064}