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
25use serde::Serialize;
26use serde_with::serde_as;
27
28pub mod conformance;
30pub mod err;
32pub mod json;
33use json::err::JsonSerializationError;
34
35pub use json::{
36 AllEntitiesNoAttrsSchema, AttributeType, CedarValueJson, ContextJsonParser, ContextSchema,
37 EntityJson, EntityJsonParser, EntityTypeDescription, EntityUidJson, FnAndArg, NoEntitiesSchema,
38 NoStaticContext, Schema, SchemaType, TypeAndId,
39};
40
41use conformance::EntitySchemaConformanceChecker;
42use err::*;
43
44#[serde_as]
51#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
52pub struct Entities {
53 #[serde_as(as = "Vec<(_, _)>")]
61 entities: HashMap<EntityUID, Entity>,
62
63 #[serde(default)]
68 #[serde(skip_deserializing)]
69 #[serde(skip_serializing)]
70 mode: Mode,
71}
72
73impl Entities {
74 pub fn new() -> Self {
76 Self {
77 entities: HashMap::new(),
78 mode: Mode::default(),
79 }
80 }
81
82 #[cfg(feature = "partial-eval")]
86 pub fn partial(self) -> Self {
87 Self {
88 entities: self.entities,
89 mode: Mode::Partial,
90 }
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 format!("{uid}"),
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()
113 }
114
115 pub fn add_entities(
127 mut self,
128 collection: impl IntoIterator<Item = Entity>,
129 schema: Option<&impl Schema>,
130 tc_computation: TCComputation,
131 extensions: &Extensions<'_>,
132 ) -> Result<Self> {
133 let checker = schema.map(|schema| EntitySchemaConformanceChecker::new(schema, extensions));
134 for entity in collection.into_iter() {
135 if let Some(checker) = checker.as_ref() {
136 checker.validate_entity(&entity)?;
137 }
138 match self.entities.entry(entity.uid().clone()) {
139 hash_map::Entry::Occupied(_) => {
140 return Err(EntitiesError::duplicate(entity.uid().clone()))
141 }
142 hash_map::Entry::Vacant(vacant_entry) => {
143 vacant_entry.insert(entity);
144 }
145 }
146 }
147 match tc_computation {
148 TCComputation::AssumeAlreadyComputed => (),
149 TCComputation::EnforceAlreadyComputed => enforce_tc_and_dag(&self.entities)?,
150 TCComputation::ComputeNow => compute_tc(&mut self.entities, true)?,
151 };
152 Ok(self)
153 }
154
155 pub fn from_entities(
172 entities: impl IntoIterator<Item = Entity>,
173 schema: Option<&impl Schema>,
174 tc_computation: TCComputation,
175 extensions: &Extensions<'_>,
176 ) -> Result<Self> {
177 let mut entity_map = create_entity_map(entities.into_iter())?;
178 if let Some(schema) = schema {
179 let checker = EntitySchemaConformanceChecker::new(schema, extensions);
184 for entity in entity_map.values() {
185 if !entity.uid().entity_type().is_action() {
186 checker.validate_entity(entity)?;
187 }
188 }
189 }
190 match tc_computation {
191 TCComputation::AssumeAlreadyComputed => {}
192 TCComputation::EnforceAlreadyComputed => {
193 enforce_tc_and_dag(&entity_map)?;
194 }
195 TCComputation::ComputeNow => {
196 compute_tc(&mut entity_map, true)?;
197 }
198 }
199 if let Some(schema) = schema {
205 let checker = EntitySchemaConformanceChecker::new(schema, extensions);
206 for entity in entity_map.values() {
207 if entity.uid().entity_type().is_action() {
208 checker.validate_entity(entity)?;
209 }
210 }
211 entity_map.extend(
213 schema
214 .action_entities()
215 .into_iter()
216 .map(|e| (e.uid().clone(), Arc::unwrap_or_clone(e))),
217 );
218 }
219 Ok(Self {
220 entities: entity_map,
221 mode: Mode::default(),
222 })
223 }
224
225 pub fn to_json_value(&self) -> Result<serde_json::Value> {
232 let ejsons: Vec<EntityJson> = self.to_ejsons()?;
233 serde_json::to_value(ejsons)
234 .map_err(JsonSerializationError::from)
235 .map_err(Into::into)
236 }
237
238 pub fn write_to_json(&self, f: impl std::io::Write) -> Result<()> {
246 let ejsons: Vec<EntityJson> = self.to_ejsons()?;
247 serde_json::to_writer_pretty(f, &ejsons).map_err(JsonSerializationError::from)?;
248 Ok(())
249 }
250
251 fn to_ejsons(&self) -> Result<Vec<EntityJson>> {
253 self.entities
254 .values()
255 .map(EntityJson::from_entity)
256 .collect::<std::result::Result<_, JsonSerializationError>>()
257 .map_err(Into::into)
258 }
259
260 fn get_entities_by_entity_type(&self) -> HashMap<EntityType, Vec<&Entity>> {
261 let mut entities_by_type: HashMap<EntityType, Vec<&Entity>> = HashMap::new();
262 for entity in self.iter() {
263 let euid = entity.uid();
264 let entity_type = euid.entity_type();
265 if let Some(entities) = entities_by_type.get_mut(entity_type) {
266 entities.push(entity);
267 } else {
268 entities_by_type.insert(entity_type.clone(), Vec::from([entity]));
269 }
270 }
271 entities_by_type
272 }
273
274 pub fn to_dot_str(&self) -> String {
276 let mut dot_str = String::new();
277 dot_str.push_str("strict digraph {\n\tordering=\"out\"\n\tnode[shape=box]\n");
279
280 fn to_dot_id(v: &impl std::fmt::Display) -> String {
289 format!("\"{}\"", v.to_string().escape_debug())
290 }
291
292 let entities_by_type = self.get_entities_by_entity_type();
294
295 for (et, entities) in entities_by_type {
296 dot_str.push_str(&format!(
297 "\tsubgraph \"cluster_{et}\" {{\n\t\tlabel={}\n",
298 to_dot_id(&et)
299 ));
300 for entity in entities {
301 let euid = to_dot_id(&entity.uid());
302 let label = format!(r#"[label={}]"#, to_dot_id(&entity.uid().eid().escaped()));
303 dot_str.push_str(&format!("\t\t{euid} {label}\n"));
304 }
305 dot_str.push_str("\t}\n");
306 }
307
308 for entity in self.iter() {
310 for ancestor in entity.ancestors() {
311 dot_str.push_str(&format!(
312 "\t{} -> {}\n",
313 to_dot_id(&entity.uid()),
314 to_dot_id(&ancestor)
315 ));
316 }
317 }
318
319 dot_str.push_str("}\n");
320 dot_str
321 }
322}
323
324fn create_entity_map(es: impl Iterator<Item = Entity>) -> Result<HashMap<EntityUID, Entity>> {
326 let mut map = HashMap::new();
327 for e in es {
328 match map.entry(e.uid().clone()) {
329 hash_map::Entry::Occupied(_) => return Err(EntitiesError::duplicate(e.uid().clone())),
330 hash_map::Entry::Vacant(v) => {
331 v.insert(e);
332 }
333 };
334 }
335 Ok(map)
336}
337
338impl IntoIterator for Entities {
339 type Item = Entity;
340
341 type IntoIter = hash_map::IntoValues<EntityUID, Entity>;
342
343 fn into_iter(self) -> Self::IntoIter {
344 self.entities.into_values()
345 }
346}
347
348impl std::fmt::Display for Entities {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 if self.entities.is_empty() {
351 write!(f, "<empty Entities>")
352 } else {
353 for e in self.entities.values() {
354 writeln!(f, "{e}")?;
355 }
356 Ok(())
357 }
358 }
359}
360
361#[derive(Debug, Clone)]
363pub enum Dereference<'a, T> {
364 NoSuchEntity,
366 Residual(Expr),
368 Data(&'a T),
370}
371
372impl<'a, T> Dereference<'a, T>
373where
374 T: std::fmt::Debug,
375{
376 #[allow(clippy::panic)]
387 pub fn unwrap(self) -> &'a T {
388 match self {
389 Self::Data(e) => e,
390 e => panic!("unwrap() called on {:?}", e),
391 }
392 }
393
394 #[allow(clippy::panic)]
405 #[track_caller] pub fn expect(self, msg: &str) -> &'a T {
407 match self {
408 Self::Data(e) => e,
409 e => panic!("expect() called on {:?}, msg: {msg}", e),
410 }
411 }
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415enum Mode {
416 Concrete,
417 #[cfg(feature = "partial-eval")]
418 Partial,
419}
420
421impl Default for Mode {
422 fn default() -> Self {
423 Self::Concrete
424 }
425}
426
427#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
431pub enum TCComputation {
432 AssumeAlreadyComputed,
435 EnforceAlreadyComputed,
439 ComputeNow,
443}
444
445#[allow(clippy::panic)]
447#[cfg(test)]
448#[allow(clippy::panic)]
450mod json_parsing_tests {
451
452 use super::*;
453 use crate::{extensions::Extensions, test_utils::*, transitive_closure::TcError};
454 use cool_asserts::assert_matches;
455
456 #[test]
457 fn simple_json_parse1() {
458 let v = serde_json::json!(
459 [
460 {
461 "uid" : { "type" : "A", "id" : "b"},
462 "attrs" : {},
463 "parents" : [ { "type" : "A", "id" : "c" }]
464 }
465 ]
466 );
467 let parser: EntityJsonParser<'_, '_> =
468 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
469 parser.from_json_value(v).unwrap();
470 }
471
472 #[test]
473 fn enforces_tc_fail_cycle_almost() {
474 let parser: EntityJsonParser<'_, '_> =
475 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
476 let new = serde_json::json!([
477 {
478 "uid" : {
479 "type" : "Test",
480 "id" : "george"
481 },
482 "attrs" : { "foo" : 3},
483 "parents" : [
484 {
485 "type" : "Test",
486 "id" : "george"
487 },
488 {
489 "type" : "Test",
490 "id" : "janet"
491 }
492 ]
493 }
494 ]);
495
496 let addl_entities = parser.iter_from_json_value(new).unwrap();
497 let err = simple_entities(&parser).add_entities(
498 addl_entities,
499 None::<&NoEntitiesSchema>,
500 TCComputation::EnforceAlreadyComputed,
501 Extensions::none(),
502 );
503 let expected = TcError::missing_tc_edge(
505 r#"Test::"janet""#.parse().unwrap(),
506 r#"Test::"george""#.parse().unwrap(),
507 r#"Test::"janet""#.parse().unwrap(),
508 );
509 assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
510 assert_eq!(&expected, e.inner());
511 });
512 }
513
514 #[test]
515 fn enforces_tc_fail_connecting() {
516 let parser: EntityJsonParser<'_, '_> =
517 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
518 let new = serde_json::json!([
519 {
520 "uid" : {
521 "type" : "Test",
522 "id" : "george"
523 },
524 "attrs" : { "foo" : 3 },
525 "parents" : [
526 {
527 "type" : "Test",
528 "id" : "henry"
529 }
530 ]
531 }
532 ]);
533
534 let addl_entities = parser.iter_from_json_value(new).unwrap();
535 let err = simple_entities(&parser).add_entities(
536 addl_entities,
537 None::<&NoEntitiesSchema>,
538 TCComputation::EnforceAlreadyComputed,
539 Extensions::all_available(),
540 );
541 let expected = TcError::missing_tc_edge(
542 r#"Test::"janet""#.parse().unwrap(),
543 r#"Test::"george""#.parse().unwrap(),
544 r#"Test::"henry""#.parse().unwrap(),
545 );
546 assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
547 assert_eq!(&expected, e.inner());
548 });
549 }
550
551 #[test]
552 fn enforces_tc_fail_missing_edge() {
553 let parser: EntityJsonParser<'_, '_> =
554 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
555 let new = serde_json::json!([
556 {
557 "uid" : {
558 "type" : "Test",
559 "id" : "jeff",
560 },
561 "attrs" : { "foo" : 3 },
562 "parents" : [
563 {
564 "type" : "Test",
565 "id" : "alice"
566 }
567 ]
568 }
569 ]);
570
571 let addl_entities = parser.iter_from_json_value(new).unwrap();
572 let err = simple_entities(&parser).add_entities(
573 addl_entities,
574 None::<&NoEntitiesSchema>,
575 TCComputation::EnforceAlreadyComputed,
576 Extensions::all_available(),
577 );
578 let expected = TcError::missing_tc_edge(
579 r#"Test::"jeff""#.parse().unwrap(),
580 r#"Test::"alice""#.parse().unwrap(),
581 r#"Test::"bob""#.parse().unwrap(),
582 );
583 assert_matches!(err, Err(EntitiesError::TransitiveClosureError(e)) => {
584 assert_eq!(&expected, e.inner());
585 });
586 }
587
588 #[test]
589 fn enforces_tc_success() {
590 let parser: EntityJsonParser<'_, '_> =
591 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
592 let new = serde_json::json!([
593 {
594 "uid" : {
595 "type" : "Test",
596 "id" : "jeff"
597 },
598 "attrs" : { "foo" : 3 },
599 "parents" : [
600 {
601 "type" : "Test",
602 "id" : "alice"
603 },
604 {
605 "type" : "Test",
606 "id" : "bob"
607 }
608 ]
609 }
610 ]);
611
612 let addl_entities = parser.iter_from_json_value(new).unwrap();
613 let es = simple_entities(&parser)
614 .add_entities(
615 addl_entities,
616 None::<&NoEntitiesSchema>,
617 TCComputation::EnforceAlreadyComputed,
618 Extensions::all_available(),
619 )
620 .unwrap();
621 let euid = r#"Test::"jeff""#.parse().unwrap();
622 let jeff = es.entity(&euid).unwrap();
623 assert!(jeff.is_descendant_of(&r#"Test::"alice""#.parse().unwrap()));
624 assert!(jeff.is_descendant_of(&r#"Test::"bob""#.parse().unwrap()));
625 assert!(!jeff.is_descendant_of(&r#"Test::"george""#.parse().unwrap()));
626 simple_entities_still_sane(&es);
627 }
628
629 #[test]
630 fn adds_extends_tc_connecting() {
631 let parser: EntityJsonParser<'_, '_> =
632 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
633 let new = serde_json::json!([
634 {
635 "uid" : {
636 "type" : "Test",
637 "id" : "george"
638 },
639 "attrs" : { "foo" : 3},
640 "parents" : [
641 {
642 "type" : "Test",
643 "id" : "henry"
644 }
645 ]
646 }
647 ]);
648
649 let addl_entities = parser.iter_from_json_value(new).unwrap();
650 let es = simple_entities(&parser)
651 .add_entities(
652 addl_entities,
653 None::<&NoEntitiesSchema>,
654 TCComputation::ComputeNow,
655 Extensions::all_available(),
656 )
657 .unwrap();
658 let euid = r#"Test::"george""#.parse().unwrap();
659 let jeff = es.entity(&euid).unwrap();
660 assert!(jeff.is_descendant_of(&r#"Test::"henry""#.parse().unwrap()));
661 let alice = es.entity(&r#"Test::"janet""#.parse().unwrap()).unwrap();
662 assert!(alice.is_descendant_of(&r#"Test::"henry""#.parse().unwrap()));
663 simple_entities_still_sane(&es);
664 }
665
666 #[test]
667 fn adds_extends_tc() {
668 let parser: EntityJsonParser<'_, '_> =
669 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
670 let new = serde_json::json!([
671 {
672 "uid" : {
673 "type" : "Test",
674 "id" : "jeff"
675 },
676 "attrs" : {
677 "foo" : 3
678 },
679 "parents" : [
680 {
681 "type" : "Test",
682 "id" : "alice"
683 }
684 ]
685 }
686 ]);
687
688 let addl_entities = parser.iter_from_json_value(new).unwrap();
689 let es = simple_entities(&parser)
690 .add_entities(
691 addl_entities,
692 None::<&NoEntitiesSchema>,
693 TCComputation::ComputeNow,
694 Extensions::all_available(),
695 )
696 .unwrap();
697 let euid = r#"Test::"jeff""#.parse().unwrap();
698 let jeff = es.entity(&euid).unwrap();
699 assert!(jeff.is_descendant_of(&r#"Test::"alice""#.parse().unwrap()));
700 assert!(jeff.is_descendant_of(&r#"Test::"bob""#.parse().unwrap()));
701 simple_entities_still_sane(&es);
702 }
703
704 #[test]
705 fn adds_works() {
706 let parser: EntityJsonParser<'_, '_> =
707 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
708 let new = serde_json::json!([
709 {
710 "uid" : {
711 "type" : "Test",
712 "id" : "jeff"
713 },
714 "attrs" : {
715 "foo" : 3
716 },
717 "parents" : [
718 {
719 "type" : "Test",
720 "id" : "susan"
721 }
722 ]
723 }
724 ]);
725
726 let addl_entities = parser.iter_from_json_value(new).unwrap();
727 let es = simple_entities(&parser)
728 .add_entities(
729 addl_entities,
730 None::<&NoEntitiesSchema>,
731 TCComputation::ComputeNow,
732 Extensions::all_available(),
733 )
734 .unwrap();
735 let euid = r#"Test::"jeff""#.parse().unwrap();
736 let jeff = es.entity(&euid).unwrap();
737 let value = jeff.get("foo").unwrap();
738 assert_eq!(value, &PartialValue::from(3));
739 assert!(jeff.is_descendant_of(&r#"Test::"susan""#.parse().unwrap()));
740 simple_entities_still_sane(&es);
741 }
742
743 #[test]
744 fn add_duplicates_fail2() {
745 let parser: EntityJsonParser<'_, '_> =
746 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
747 let new = serde_json::json!([
748 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []},
749 {"uid":{ "type" : "Test", "id" : "jeff" }, "attrs" : {}, "parents" : []}]);
750
751 let addl_entities = parser.iter_from_json_value(new).unwrap();
752 let err = simple_entities(&parser)
753 .add_entities(
754 addl_entities,
755 None::<&NoEntitiesSchema>,
756 TCComputation::ComputeNow,
757 Extensions::all_available(),
758 )
759 .err()
760 .unwrap();
761 let expected = r#"Test::"jeff""#.parse().unwrap();
762 assert_matches!(err, EntitiesError::Duplicate(d) => assert_eq!(d.euid(), &expected));
763 }
764
765 #[test]
766 fn add_duplicates_fail1() {
767 let parser: EntityJsonParser<'_, '_> =
768 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
769 let new = serde_json::json!([{"uid":{ "type": "Test", "id": "alice" }, "attrs" : {}, "parents" : []}]);
770 let addl_entities = parser.iter_from_json_value(new).unwrap();
771 let err = simple_entities(&parser).add_entities(
772 addl_entities,
773 None::<&NoEntitiesSchema>,
774 TCComputation::ComputeNow,
775 Extensions::all_available(),
776 );
777 let expected = r#"Test::"alice""#.parse().unwrap();
778 assert_matches!(err, Err(EntitiesError::Duplicate(d)) => assert_eq!(d.euid(), &expected));
779 }
780
781 #[test]
782 fn simple_entities_correct() {
783 let parser: EntityJsonParser<'_, '_> =
784 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
785 simple_entities(&parser);
786 }
787
788 fn simple_entities(parser: &EntityJsonParser<'_, '_>) -> Entities {
789 let json = serde_json::json!(
790 [
791 {
792 "uid" : { "type" : "Test", "id": "alice" },
793 "attrs" : { "bar" : 2},
794 "parents" : [
795 {
796 "type" : "Test",
797 "id" : "bob"
798 }
799 ]
800 },
801 {
802 "uid" : { "type" : "Test", "id" : "janet"},
803 "attrs" : { "bar" : 2},
804 "parents" : [
805 {
806 "type" : "Test",
807 "id" : "george"
808 }
809 ]
810 },
811 {
812 "uid" : { "type" : "Test", "id" : "bob"},
813 "attrs" : {},
814 "parents" : []
815 },
816 {
817 "uid" : { "type" : "Test", "id" : "henry"},
818 "attrs" : {},
819 "parents" : []
820 },
821 ]
822 );
823 parser.from_json_value(json).expect("JSON is correct")
824 }
825
826 fn simple_entities_still_sane(e: &Entities) {
828 let bob = r#"Test::"bob""#.parse().unwrap();
829 let alice = e.entity(&r#"Test::"alice""#.parse().unwrap()).unwrap();
830 let bar = alice.get("bar").unwrap();
831 assert_eq!(bar, &PartialValue::from(2));
832 assert!(alice.is_descendant_of(&bob));
833 let bob = e.entity(&bob).unwrap();
834 assert!(bob.ancestors().collect::<Vec<_>>().is_empty());
835 }
836
837 #[cfg(feature = "partial-eval")]
838 #[test]
839 fn basic_partial() {
840 let json = serde_json::json!(
842 [
843 {
844 "uid" : {
845 "type" : "test_entity_type",
846 "id" : "alice"
847 },
848 "attrs": {},
849 "parents": [
850 {
851 "type" : "test_entity_type",
852 "id" : "jane"
853 }
854 ]
855 },
856 {
857 "uid" : {
858 "type" : "test_entity_type",
859 "id" : "jane"
860 },
861 "attrs": {},
862 "parents": [
863 {
864 "type" : "test_entity_type",
865 "id" : "bob",
866 }
867 ]
868 },
869 {
870 "uid" : {
871 "type" : "test_entity_type",
872 "id" : "bob"
873 },
874 "attrs": {},
875 "parents": []
876 }
877 ]
878 );
879
880 let eparser: EntityJsonParser<'_, '_> =
881 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
882 let es = eparser
883 .from_json_value(json)
884 .expect("JSON is correct")
885 .partial();
886
887 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
888 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
890
891 let janice = es.entity(&EntityUID::with_eid("janice"));
892
893 assert_matches!(janice, Dereference::Residual(_));
894 }
895
896 #[test]
897 fn basic() {
898 let json = serde_json::json!([
900 {
901 "uid" : {
902 "type" : "test_entity_type",
903 "id" : "alice"
904 },
905 "attrs": {},
906 "parents": [
907 {
908 "type" : "test_entity_type",
909 "id" : "jane"
910 }
911 ]
912 },
913 {
914 "uid" : {
915 "type" : "test_entity_type",
916 "id" : "jane"
917 },
918 "attrs": {},
919 "parents": [
920 {
921 "type" : "test_entity_type",
922 "id" : "bob"
923 }
924 ]
925 },
926 {
927 "uid" : {
928 "type" : "test_entity_type",
929 "id" : "bob"
930 },
931 "attrs": {},
932 "parents": []
933 }
934 ]
935 );
936
937 let eparser: EntityJsonParser<'_, '_> =
938 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
939 let es = eparser.from_json_value(json).expect("JSON is correct");
940
941 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
942 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
944 }
945
946 #[test]
947 fn no_expr_escapes1() {
948 let json = serde_json::json!(
949 [
950 {
951 "uid" : r#"test_entity_type::"Alice""#,
952 "attrs": {
953 "bacon": "eggs",
954 "pancakes": [1, 2, 3],
955 "waffles": { "key": "value" },
956 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
957 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
958 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
959 },
960 "parents": [
961 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
962 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
963 ]
964 },
965 ]);
966 let eparser: EntityJsonParser<'_, '_> =
967 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
968 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
969 expect_err(
970 &json,
971 &miette::Report::new(e),
972 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
973 .source(r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"test_entity_type::\"Alice\""`"#)
974 .help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
975 .build()
976 );
977 });
978 }
979
980 #[test]
981 fn no_expr_escapes2() {
982 let json = serde_json::json!(
983 [
984 {
985 "uid" : {
986 "__expr" :
987 r#"test_entity_type::"Alice""#
988 },
989 "attrs": {
990 "bacon": "eggs",
991 "pancakes": [1, 2, 3],
992 "waffles": { "key": "value" },
993 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
994 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
995 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
996 },
997 "parents": [
998 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
999 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1000 ]
1001 }
1002 ]);
1003 let eparser: EntityJsonParser<'_, '_> =
1004 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1005 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1006 expect_err(
1007 &json,
1008 &miette::Report::new(e),
1009 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1010 .source(r#"in uid field of <unknown entity>, the `__expr` escape is no longer supported"#)
1011 .help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
1012 .build()
1013 );
1014 });
1015 }
1016
1017 #[test]
1018 fn no_expr_escapes3() {
1019 let json = serde_json::json!(
1020 [
1021 {
1022 "uid" : {
1023 "type" : "test_entity_type",
1024 "id" : "Alice"
1025 },
1026 "attrs": {
1027 "bacon": "eggs",
1028 "pancakes": { "__expr" : "[1,2,3]" },
1029 "waffles": { "key": "value" },
1030 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
1031 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1032 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1033 },
1034 "parents": [
1035 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
1036 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1037 ]
1038 }
1039 ]);
1040 let eparser: EntityJsonParser<'_, '_> =
1041 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1042 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1043 expect_err(
1044 &json,
1045 &miette::Report::new(e),
1046 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1047 .source(r#"in attribute `pancakes` on `test_entity_type::"Alice"`, the `__expr` escape is no longer supported"#)
1048 .help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
1049 .build()
1050 );
1051 });
1052 }
1053
1054 #[test]
1055 fn no_expr_escapes4() {
1056 let json = serde_json::json!(
1057 [
1058 {
1059 "uid" : {
1060 "type" : "test_entity_type",
1061 "id" : "Alice"
1062 },
1063 "attrs": {
1064 "bacon": "eggs",
1065 "waffles": { "key": "value" },
1066 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1067 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1068 },
1069 "parents": [
1070 { "__expr": "test_entity_type::\"Alice\"" },
1071 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1072 ]
1073 }
1074 ]);
1075 let eparser: EntityJsonParser<'_, '_> =
1076 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1077 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1078 expect_err(
1079 &json,
1080 &miette::Report::new(e),
1081 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1082 .source(r#"in parents field of `test_entity_type::"Alice"`, the `__expr` escape is no longer supported"#)
1083 .help(r#"to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"#)
1084 .build()
1085 );
1086 });
1087 }
1088
1089 #[test]
1090 fn no_expr_escapes5() {
1091 let json = serde_json::json!(
1092 [
1093 {
1094 "uid" : {
1095 "type" : "test_entity_type",
1096 "id" : "Alice"
1097 },
1098 "attrs": {
1099 "bacon": "eggs",
1100 "waffles": { "key": "value" },
1101 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1102 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1103 },
1104 "parents": [
1105 "test_entity_type::\"bob\"",
1106 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1107 ]
1108 }
1109 ]);
1110 let eparser: EntityJsonParser<'_, '_> =
1111 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1112 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1113 expect_err(
1114 &json,
1115 &miette::Report::new(e),
1116 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1117 .source(r#"in parents field of `test_entity_type::"Alice"`, expected a literal entity reference, but got `"test_entity_type::\"bob\""`"#)
1118 .help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
1119 .build()
1120 );
1121 });
1122 }
1123
1124 #[cfg(feature = "ipaddr")]
1125 #[test]
1127 fn more_escapes() {
1128 let json = serde_json::json!(
1129 [
1130 {
1131 "uid" : {
1132 "type" : "test_entity_type",
1133 "id" : "alice"
1134 },
1135 "attrs": {
1136 "bacon": "eggs",
1137 "pancakes": [1, 2, 3],
1138 "waffles": { "key": "value" },
1139 "toast" : { "__extn" : { "fn" : "decimal", "arg" : "33.47" }},
1140 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
1141 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
1142 },
1143 "parents": [
1144 { "__entity": { "type" : "test_entity_type", "id" : "bob"} },
1145 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
1146 ]
1147 },
1148 {
1149 "uid" : {
1150 "type" : "test_entity_type",
1151 "id" : "bob"
1152 },
1153 "attrs": {},
1154 "parents": []
1155 },
1156 {
1157 "uid" : {
1158 "type" : "test_entity_type",
1159 "id" : "catherine"
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.from_json_value(json).expect("JSON is correct");
1170
1171 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
1172 assert_eq!(alice.get("bacon"), Some(&PartialValue::from("eggs")));
1173 assert_eq!(
1174 alice.get("pancakes"),
1175 Some(&PartialValue::from(vec![
1176 Value::from(1),
1177 Value::from(2),
1178 Value::from(3),
1179 ])),
1180 );
1181 assert_eq!(
1182 alice.get("waffles"),
1183 Some(&PartialValue::from(Value::record(
1184 vec![("key", Value::from("value"),)],
1185 None
1186 ))),
1187 );
1188 assert_eq!(
1189 alice.get("toast").cloned().map(RestrictedExpr::try_from),
1190 Some(Ok(RestrictedExpr::call_extension_fn(
1191 "decimal".parse().expect("should be a valid Name"),
1192 vec![RestrictedExpr::val("33.47")],
1193 ))),
1194 );
1195 assert_eq!(
1196 alice.get("12345"),
1197 Some(&PartialValue::from(EntityUID::with_eid("bob"))),
1198 );
1199 assert_eq!(
1200 alice.get("a b c").cloned().map(RestrictedExpr::try_from),
1201 Some(Ok(RestrictedExpr::call_extension_fn(
1202 "ip".parse().expect("should be a valid Name"),
1203 vec![RestrictedExpr::val("222.222.222.0/24")],
1204 ))),
1205 );
1206 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
1207 assert!(alice.is_descendant_of(&EntityUID::with_eid("catherine")));
1208 }
1209
1210 #[test]
1211 fn implicit_and_explicit_escapes() {
1212 let json = serde_json::json!(
1215 [
1216 {
1217 "uid": { "type" : "test_entity_type", "id" : "alice" },
1218 "attrs": {},
1219 "parents": [
1220 { "type" : "test_entity_type", "id" : "bob" },
1221 { "__entity": { "type": "test_entity_type", "id": "charles" } },
1222 { "type": "test_entity_type", "id": "elaine" }
1223 ]
1224 },
1225 {
1226 "uid": { "__entity": { "type": "test_entity_type", "id": "bob" }},
1227 "attrs": {},
1228 "parents": []
1229 },
1230 {
1231 "uid" : {
1232 "type" : "test_entity_type",
1233 "id" : "charles"
1234 },
1235 "attrs" : {},
1236 "parents" : []
1237 },
1238 {
1239 "uid": { "type": "test_entity_type", "id": "darwin" },
1240 "attrs": {},
1241 "parents": []
1242 },
1243 {
1244 "uid": { "type": "test_entity_type", "id": "elaine" },
1245 "attrs": {},
1246 "parents" : [
1247 {
1248 "type" : "test_entity_type",
1249 "id" : "darwin"
1250 }
1251 ]
1252 }
1253 ]
1254 );
1255
1256 let eparser: EntityJsonParser<'_, '_> =
1257 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1258 let es = eparser.from_json_value(json).expect("JSON is correct");
1259
1260 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
1262 let bob = es.entity(&EntityUID::with_eid("bob")).unwrap();
1263 let charles = es.entity(&EntityUID::with_eid("charles")).unwrap();
1264 let darwin = es.entity(&EntityUID::with_eid("darwin")).unwrap();
1265 let elaine = es.entity(&EntityUID::with_eid("elaine")).unwrap();
1266
1267 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
1269 assert!(alice.is_descendant_of(&EntityUID::with_eid("charles")));
1270 assert!(alice.is_descendant_of(&EntityUID::with_eid("darwin")));
1271 assert!(alice.is_descendant_of(&EntityUID::with_eid("elaine")));
1272 assert_eq!(bob.ancestors().next(), None);
1273 assert_eq!(charles.ancestors().next(), None);
1274 assert_eq!(darwin.ancestors().next(), None);
1275 assert!(elaine.is_descendant_of(&EntityUID::with_eid("darwin")));
1276 assert!(!elaine.is_descendant_of(&EntityUID::with_eid("bob")));
1277 }
1278
1279 #[test]
1280 fn uid_failures() {
1281 let eparser: EntityJsonParser<'_, '_> =
1283 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1284
1285 let json = serde_json::json!(
1286 [
1287 {
1288 "uid": "hello",
1289 "attrs": {},
1290 "parents": []
1291 }
1292 ]
1293 );
1294 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1295 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1296 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"hello"`"#,
1297 ).help(
1298 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1299 ).build());
1300 });
1301
1302 let json = serde_json::json!(
1303 [
1304 {
1305 "uid": "\"hello\"",
1306 "attrs": {},
1307 "parents": []
1308 }
1309 ]
1310 );
1311 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1312 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1313 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `"\"hello\""`"#,
1314 ).help(
1315 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1316 ).build());
1317 });
1318
1319 let json = serde_json::json!(
1320 [
1321 {
1322 "uid": { "type": "foo", "spam": "eggs" },
1323 "attrs": {},
1324 "parents": []
1325 }
1326 ]
1327 );
1328 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1329 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1330 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"spam":"eggs","type":"foo"}`"#,
1331 ).help(
1332 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1333 ).build());
1334 });
1335
1336 let json = serde_json::json!(
1337 [
1338 {
1339 "uid": { "type": "foo", "id": "bar" },
1340 "attrs": {},
1341 "parents": "foo::\"help\""
1342 }
1343 ]
1344 );
1345 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1346 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1347 r#"invalid type: string "foo::\"help\"", expected a sequence"#
1348 ).build());
1349 });
1350
1351 let json = serde_json::json!(
1352 [
1353 {
1354 "uid": { "type": "foo", "id": "bar" },
1355 "attrs": {},
1356 "parents": [
1357 "foo::\"help\"",
1358 { "__extn": { "fn": "ip", "arg": "222.222.222.0" } }
1359 ]
1360 }
1361 ]
1362 );
1363 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1364 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1365 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `"foo::\"help\""`"#,
1366 ).help(
1367 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1368 ).build());
1369 });
1370 }
1371
1372 #[test]
1375 fn null_failures() {
1376 let eparser: EntityJsonParser<'_, '_> =
1377 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1378
1379 let json = serde_json::json!(
1380 [
1381 {
1382 "uid": null,
1383 "attrs": {},
1384 "parents": [],
1385 }
1386 ]
1387 );
1388 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1389 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1390 "in uid field of <unknown entity>, expected a literal entity reference, but got `null`",
1391 ).help(
1392 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1393 ).build());
1394 });
1395
1396 let json = serde_json::json!(
1397 [
1398 {
1399 "uid": { "type": null, "id": "bar" },
1400 "attrs": {},
1401 "parents": [],
1402 }
1403 ]
1404 );
1405 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1406 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1407 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":"bar","type":null}`"#,
1408 ).help(
1409 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1410 ).build());
1411 });
1412
1413 let json = serde_json::json!(
1414 [
1415 {
1416 "uid": { "type": "foo", "id": null },
1417 "attrs": {},
1418 "parents": [],
1419 }
1420 ]
1421 );
1422 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1423 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1424 r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
1425 ).help(
1426 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1427 ).build());
1428 });
1429
1430 let json = serde_json::json!(
1431 [
1432 {
1433 "uid": { "type": "foo", "id": "bar" },
1434 "attrs": null,
1435 "parents": [],
1436 }
1437 ]
1438 );
1439 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1440 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1441 "invalid type: null, expected a map"
1442 ).build());
1443 });
1444
1445 let json = serde_json::json!(
1446 [
1447 {
1448 "uid": { "type": "foo", "id": "bar" },
1449 "attrs": { "attr": null },
1450 "parents": [],
1451 }
1452 ]
1453 );
1454 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1455 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1456 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1457 ).build());
1458 });
1459
1460 let json = serde_json::json!(
1461 [
1462 {
1463 "uid": { "type": "foo", "id": "bar" },
1464 "attrs": { "attr": { "subattr": null } },
1465 "parents": [],
1466 }
1467 ]
1468 );
1469 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1470 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1471 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1472 ).build());
1473 });
1474
1475 let json = serde_json::json!(
1476 [
1477 {
1478 "uid": { "type": "foo", "id": "bar" },
1479 "attrs": { "attr": [ 3, null ] },
1480 "parents": [],
1481 }
1482 ]
1483 );
1484 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1485 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1486 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1487 ).build());
1488 });
1489
1490 let json = serde_json::json!(
1491 [
1492 {
1493 "uid": { "type": "foo", "id": "bar" },
1494 "attrs": { "attr": [ 3, { "subattr" : null } ] },
1495 "parents": [],
1496 }
1497 ]
1498 );
1499 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1500 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1501 r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1502 ).build());
1503 });
1504
1505 let json = serde_json::json!(
1506 [
1507 {
1508 "uid": { "type": "foo", "id": "bar" },
1509 "attrs": { "__extn": { "fn": null, "args": [] } },
1510 "parents": [],
1511 }
1512 ]
1513 );
1514 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1515 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1516 r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1517 ).build());
1518 });
1519
1520 let json = serde_json::json!(
1521 [
1522 {
1523 "uid": { "type": "foo", "id": "bar" },
1524 "attrs": { "__extn": { "fn": "ip", "args": null } },
1525 "parents": [],
1526 }
1527 ]
1528 );
1529 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1530 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1531 r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1532 ).build());
1533 });
1534
1535 let json = serde_json::json!(
1536 [
1537 {
1538 "uid": { "type": "foo", "id": "bar" },
1539 "attrs": { "__extn": { "fn": "ip", "args": [ null ] } },
1540 "parents": [],
1541 }
1542 ]
1543 );
1544 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1545 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1546 r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
1547 ).build());
1548 });
1549
1550 let json = serde_json::json!(
1551 [
1552 {
1553 "uid": { "type": "foo", "id": "bar" },
1554 "attrs": { "attr": 2 },
1555 "parents": null,
1556 }
1557 ]
1558 );
1559 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1560 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1561 "invalid type: null, expected a sequence"
1562 ).build());
1563 });
1564
1565 let json = serde_json::json!(
1566 [
1567 {
1568 "uid": { "type": "foo", "id": "bar" },
1569 "attrs": { "attr": 2 },
1570 "parents": [ null ],
1571 }
1572 ]
1573 );
1574 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1575 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1576 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
1577 ).help(
1578 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1579 ).build());
1580 });
1581
1582 let json = serde_json::json!(
1583 [
1584 {
1585 "uid": { "type": "foo", "id": "bar" },
1586 "attrs": { "attr": 2 },
1587 "parents": [ { "type": "foo", "id": null } ],
1588 }
1589 ]
1590 );
1591 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1592 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1593 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
1594 ).help(
1595 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1596 ).build());
1597 });
1598
1599 let json = serde_json::json!(
1600 [
1601 {
1602 "uid": { "type": "foo", "id": "bar" },
1603 "attrs": { "attr": 2 },
1604 "parents": [ { "type": "foo", "id": "parent" }, null ],
1605 }
1606 ]
1607 );
1608 assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
1609 expect_err(&json, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
1610 r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
1611 ).help(
1612 r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
1613 ).build());
1614 });
1615 }
1616
1617 fn roundtrip(entities: &Entities) -> Result<Entities> {
1619 let mut buf = Vec::new();
1620 entities.write_to_json(&mut buf)?;
1621 let eparser: EntityJsonParser<'_, '_> =
1622 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1623 eparser.from_json_str(&String::from_utf8(buf).expect("should be valid UTF-8"))
1624 }
1625
1626 fn test_entities() -> (Entity, Entity, Entity, Entity) {
1628 (
1629 Entity::with_uid(EntityUID::with_eid("test_principal")),
1630 Entity::with_uid(EntityUID::with_eid("test_action")),
1631 Entity::with_uid(EntityUID::with_eid("test_resource")),
1632 Entity::with_uid(EntityUID::with_eid("test")),
1633 )
1634 }
1635
1636 #[test]
1639 fn json_roundtripping() {
1640 let empty_entities = Entities::new();
1641 assert_eq!(
1642 empty_entities,
1643 roundtrip(&empty_entities).expect("should roundtrip without errors")
1644 );
1645
1646 let (e0, e1, e2, e3) = test_entities();
1647 let entities = Entities::from_entities(
1648 [e0, e1, e2, e3],
1649 None::<&NoEntitiesSchema>,
1650 TCComputation::ComputeNow,
1651 Extensions::none(),
1652 )
1653 .expect("Failed to construct entities");
1654 assert_eq!(
1655 entities,
1656 roundtrip(&entities).expect("should roundtrip without errors")
1657 );
1658
1659 let complicated_entity = Entity::new(
1660 EntityUID::with_eid("complicated"),
1661 [
1662 ("foo".into(), RestrictedExpr::val(false)),
1663 ("bar".into(), RestrictedExpr::val(-234)),
1664 ("ham".into(), RestrictedExpr::val(r"a b c * / ? \")),
1665 (
1666 "123".into(),
1667 RestrictedExpr::val(EntityUID::with_eid("mom")),
1668 ),
1669 (
1670 "set".into(),
1671 RestrictedExpr::set([
1672 RestrictedExpr::val(0),
1673 RestrictedExpr::val(EntityUID::with_eid("pancakes")),
1674 RestrictedExpr::val("mmm"),
1675 ]),
1676 ),
1677 (
1678 "rec".into(),
1679 RestrictedExpr::record([
1680 ("nested".into(), RestrictedExpr::val("attr")),
1681 (
1682 "another".into(),
1683 RestrictedExpr::val(EntityUID::with_eid("foo")),
1684 ),
1685 ])
1686 .unwrap(),
1687 ),
1688 (
1689 "src_ip".into(),
1690 RestrictedExpr::call_extension_fn(
1691 "ip".parse().expect("should be a valid Name"),
1692 vec![RestrictedExpr::val("222.222.222.222")],
1693 ),
1694 ),
1695 ]
1696 .into_iter()
1697 .collect(),
1698 [
1699 EntityUID::with_eid("parent1"),
1700 EntityUID::with_eid("parent2"),
1701 ]
1702 .into_iter()
1703 .collect(),
1704 Extensions::all_available(),
1705 )
1706 .unwrap();
1707 let entities = Entities::from_entities(
1708 [
1709 complicated_entity,
1710 Entity::with_uid(EntityUID::with_eid("parent1")),
1711 Entity::with_uid(EntityUID::with_eid("parent2")),
1712 ],
1713 None::<&NoEntitiesSchema>,
1714 TCComputation::ComputeNow,
1715 Extensions::all_available(),
1716 )
1717 .expect("Failed to construct entities");
1718 assert_eq!(
1719 entities,
1720 roundtrip(&entities).expect("should roundtrip without errors")
1721 );
1722
1723 let oops_entity = Entity::new(
1724 EntityUID::with_eid("oops"),
1725 [(
1726 "oops".into(),
1728 RestrictedExpr::record([("__entity".into(), RestrictedExpr::val("hi"))]).unwrap(),
1729 )]
1730 .into_iter()
1731 .collect(),
1732 [
1733 EntityUID::with_eid("parent1"),
1734 EntityUID::with_eid("parent2"),
1735 ]
1736 .into_iter()
1737 .collect(),
1738 Extensions::all_available(),
1739 )
1740 .unwrap();
1741 let entities = Entities::from_entities(
1742 [
1743 oops_entity,
1744 Entity::with_uid(EntityUID::with_eid("parent1")),
1745 Entity::with_uid(EntityUID::with_eid("parent2")),
1746 ],
1747 None::<&NoEntitiesSchema>,
1748 TCComputation::ComputeNow,
1749 Extensions::all_available(),
1750 )
1751 .expect("Failed to construct entities");
1752 assert_matches!(
1753 roundtrip(&entities),
1754 Err(EntitiesError::Serialization(JsonSerializationError::ReservedKey(reserved))) if reserved.key().as_ref() == "__entity"
1755 );
1756 }
1757
1758 #[test]
1760 fn bad_action_parent() {
1761 let json = serde_json::json!(
1762 [
1763 {
1764 "uid": { "type": "XYZ::Action", "id": "view" },
1765 "attrs": {},
1766 "parents": [
1767 { "type": "User", "id": "alice" }
1768 ]
1769 }
1770 ]
1771 );
1772 let eparser: EntityJsonParser<'_, '_> =
1773 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1774 assert_matches!(eparser.from_json_value(json.clone()), Err(e) => {
1775 expect_err(
1776 &json,
1777 &miette::Report::new(e),
1778 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1779 .source(r#"action `XYZ::Action::"view"` has a non-action parent `User::"alice"`"#)
1780 .help(r#"parents of actions need to have type `Action` themselves, perhaps namespaced"#)
1781 .build()
1782 );
1783 });
1784 }
1785
1786 #[test]
1790 fn not_bad_action_parent() {
1791 let json = serde_json::json!(
1792 [
1793 {
1794 "uid": { "type": "User", "id": "alice" },
1795 "attrs": {},
1796 "parents": [
1797 { "type": "XYZ::Action", "id": "view" },
1798 ]
1799 }
1800 ]
1801 );
1802 let eparser: EntityJsonParser<'_, '_> =
1803 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1804 assert_matches!(eparser.from_json_value(json), Ok(_));
1805 }
1806
1807 #[test]
1809 fn duplicate_keys() {
1810 let json = r#"
1813 [
1814 {
1815 "uid": { "type": "User", "id": "alice "},
1816 "attrs": {
1817 "foo": {
1818 "hello": "goodbye",
1819 "bar": 2,
1820 "spam": "eggs",
1821 "bar": 3
1822 }
1823 },
1824 "parents": []
1825 }
1826 ]
1827 "#;
1828 let eparser: EntityJsonParser<'_, '_> =
1829 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
1830 assert_matches!(eparser.from_json_str(json), Err(e) => {
1831 expect_err(
1833 json,
1834 &miette::Report::new(e),
1835 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
1836 .source(r#"the key `bar` occurs two or more times in the same JSON object at line 11 column 25"#)
1837 .build()
1838 );
1839 });
1840 }
1841}
1842
1843#[allow(clippy::panic)]
1845#[cfg(test)]
1846#[allow(clippy::panic)]
1848mod entities_tests {
1849 use super::*;
1850
1851 #[test]
1852 fn empty_entities() {
1853 let e = Entities::new();
1854 let es = e.iter().collect::<Vec<_>>();
1855 assert!(es.is_empty(), "This vec should be empty");
1856 }
1857
1858 fn test_entities() -> (Entity, Entity, Entity, Entity) {
1860 (
1861 Entity::with_uid(EntityUID::with_eid("test_principal")),
1862 Entity::with_uid(EntityUID::with_eid("test_action")),
1863 Entity::with_uid(EntityUID::with_eid("test_resource")),
1864 Entity::with_uid(EntityUID::with_eid("test")),
1865 )
1866 }
1867
1868 #[test]
1869 fn test_iter() {
1870 let (e0, e1, e2, e3) = test_entities();
1871 let v = vec![e0.clone(), e1.clone(), e2.clone(), e3.clone()];
1872 let es = Entities::from_entities(
1873 v,
1874 None::<&NoEntitiesSchema>,
1875 TCComputation::ComputeNow,
1876 Extensions::all_available(),
1877 )
1878 .expect("Failed to construct entities");
1879 let es_v = es.iter().collect::<Vec<_>>();
1880 assert!(es_v.len() == 4, "All entities should be in the vec");
1881 assert!(es_v.contains(&&e0));
1882 assert!(es_v.contains(&&e1));
1883 assert!(es_v.contains(&&e2));
1884 assert!(es_v.contains(&&e3));
1885 }
1886
1887 #[test]
1888 fn test_enforce_already_computed_fail() {
1889 let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
1893 let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
1894 let e3 = Entity::with_uid(EntityUID::with_eid("c"));
1895 e1.add_ancestor(EntityUID::with_eid("b"));
1896 e2.add_ancestor(EntityUID::with_eid("c"));
1897
1898 let es = Entities::from_entities(
1899 vec![e1, e2, e3],
1900 None::<&NoEntitiesSchema>,
1901 TCComputation::EnforceAlreadyComputed,
1902 Extensions::all_available(),
1903 );
1904 match es {
1905 Ok(_) => panic!("Was not transitively closed!"),
1906 Err(EntitiesError::TransitiveClosureError(_)) => (),
1907 Err(_) => panic!("Wrong Error!"),
1908 };
1909 }
1910
1911 #[test]
1912 fn test_enforce_already_computed_succeed() {
1913 let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
1918 let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
1919 let e3 = Entity::with_uid(EntityUID::with_eid("c"));
1920 e1.add_ancestor(EntityUID::with_eid("b"));
1921 e1.add_ancestor(EntityUID::with_eid("c"));
1922 e2.add_ancestor(EntityUID::with_eid("c"));
1923
1924 Entities::from_entities(
1925 vec![e1, e2, e3],
1926 None::<&NoEntitiesSchema>,
1927 TCComputation::EnforceAlreadyComputed,
1928 Extensions::all_available(),
1929 )
1930 .expect("Should have succeeded");
1931 }
1932}
1933
1934#[allow(clippy::panic)]
1936#[cfg(test)]
1937mod schema_based_parsing_tests {
1938 use super::*;
1939 use crate::extensions::Extensions;
1940 use crate::test_utils::*;
1941 use cool_asserts::assert_matches;
1942 use serde_json::json;
1943 use smol_str::SmolStr;
1944 use std::collections::HashSet;
1945 use std::sync::Arc;
1946
1947 struct MockSchema;
1949 impl Schema for MockSchema {
1950 type EntityTypeDescription = MockEmployeeDescription;
1951 type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
1952 fn entity_type(&self, entity_type: &EntityType) -> Option<MockEmployeeDescription> {
1953 match entity_type.to_string().as_str() {
1954 "Employee" => Some(MockEmployeeDescription),
1955 _ => None,
1956 }
1957 }
1958 fn action(&self, action: &EntityUID) -> Option<Arc<Entity>> {
1959 match action.to_string().as_str() {
1960 r#"Action::"view""# => Some(Arc::new(Entity::new_with_attr_partial_value(
1961 action.clone(),
1962 [(SmolStr::from("foo"), PartialValue::from(34))]
1963 .into_iter()
1964 .collect(),
1965 [r#"Action::"readOnly""#.parse().expect("valid uid")]
1966 .into_iter()
1967 .collect(),
1968 ))),
1969 r#"Action::"readOnly""# => Some(Arc::new(Entity::with_uid(
1970 r#"Action::"readOnly""#.parse().expect("valid uid"),
1971 ))),
1972 _ => None,
1973 }
1974 }
1975 fn entity_types_with_basename<'a>(
1976 &'a self,
1977 basename: &'a UnreservedId,
1978 ) -> Box<dyn Iterator<Item = EntityType> + 'a> {
1979 match basename.as_ref() {
1980 "Employee" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
1981 basename.clone(),
1982 )))),
1983 "Action" => Box::new(std::iter::once(EntityType::from(Name::unqualified_name(
1984 basename.clone(),
1985 )))),
1986 _ => Box::new(std::iter::empty()),
1987 }
1988 }
1989 fn action_entities(&self) -> Self::ActionEntityIterator {
1990 std::iter::empty()
1991 }
1992 }
1993
1994 struct MockEmployeeDescription;
1996 impl EntityTypeDescription for MockEmployeeDescription {
1997 fn entity_type(&self) -> EntityType {
1998 EntityType::from(Name::parse_unqualified_name("Employee").expect("valid"))
1999 }
2000
2001 fn attr_type(&self, attr: &str) -> Option<SchemaType> {
2002 let employee_ty = || SchemaType::Entity {
2003 ty: self.entity_type(),
2004 };
2005 let hr_ty = || SchemaType::Entity {
2006 ty: EntityType::from(Name::parse_unqualified_name("HR").expect("valid")),
2007 };
2008 match attr {
2009 "isFullTime" => Some(SchemaType::Bool),
2010 "numDirectReports" => Some(SchemaType::Long),
2011 "department" => Some(SchemaType::String),
2012 "manager" => Some(employee_ty()),
2013 "hr_contacts" => Some(SchemaType::Set {
2014 element_ty: Box::new(hr_ty()),
2015 }),
2016 "json_blob" => Some(SchemaType::Record {
2017 attrs: [
2018 ("inner1".into(), AttributeType::required(SchemaType::Bool)),
2019 ("inner2".into(), AttributeType::required(SchemaType::String)),
2020 (
2021 "inner3".into(),
2022 AttributeType::required(SchemaType::Record {
2023 attrs: [(
2024 "innerinner".into(),
2025 AttributeType::required(employee_ty()),
2026 )]
2027 .into_iter()
2028 .collect(),
2029 open_attrs: false,
2030 }),
2031 ),
2032 ]
2033 .into_iter()
2034 .collect(),
2035 open_attrs: false,
2036 }),
2037 "home_ip" => Some(SchemaType::Extension {
2038 name: Name::parse_unqualified_name("ipaddr").expect("valid"),
2039 }),
2040 "work_ip" => Some(SchemaType::Extension {
2041 name: Name::parse_unqualified_name("ipaddr").expect("valid"),
2042 }),
2043 "trust_score" => Some(SchemaType::Extension {
2044 name: Name::parse_unqualified_name("decimal").expect("valid"),
2045 }),
2046 "tricky" => Some(SchemaType::Record {
2047 attrs: [
2048 ("type".into(), AttributeType::required(SchemaType::String)),
2049 ("id".into(), AttributeType::required(SchemaType::String)),
2050 ]
2051 .into_iter()
2052 .collect(),
2053 open_attrs: false,
2054 }),
2055 _ => None,
2056 }
2057 }
2058
2059 fn required_attrs(&self) -> Box<dyn Iterator<Item = SmolStr>> {
2060 Box::new(
2061 [
2062 "isFullTime",
2063 "numDirectReports",
2064 "department",
2065 "manager",
2066 "hr_contacts",
2067 "json_blob",
2068 "home_ip",
2069 "work_ip",
2070 "trust_score",
2071 ]
2072 .map(SmolStr::new)
2073 .into_iter(),
2074 )
2075 }
2076
2077 fn allowed_parent_types(&self) -> Arc<HashSet<EntityType>> {
2078 Arc::new(HashSet::new())
2079 }
2080
2081 fn open_attributes(&self) -> bool {
2082 false
2083 }
2084 }
2085
2086 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2087 #[test]
2089 fn with_and_without_schema() {
2090 let entitiesjson = json!(
2091 [
2092 {
2093 "uid": { "type": "Employee", "id": "12UA45" },
2094 "attrs": {
2095 "isFullTime": true,
2096 "numDirectReports": 3,
2097 "department": "Sales",
2098 "manager": { "type": "Employee", "id": "34FB87" },
2099 "hr_contacts": [
2100 { "type": "HR", "id": "aaaaa" },
2101 { "type": "HR", "id": "bbbbb" }
2102 ],
2103 "json_blob": {
2104 "inner1": false,
2105 "inner2": "-*/",
2106 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2107 },
2108 "home_ip": "222.222.222.101",
2109 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2110 "trust_score": "5.7",
2111 "tricky": { "type": "Employee", "id": "34FB87" }
2112 },
2113 "parents": []
2114 }
2115 ]
2116 );
2117 let eparser: EntityJsonParser<'_, '_> =
2121 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
2122 let parsed = eparser
2123 .from_json_value(entitiesjson.clone())
2124 .expect("Should parse without error");
2125 assert_eq!(parsed.iter().count(), 1);
2126 let parsed = parsed
2127 .entity(&r#"Employee::"12UA45""#.parse().unwrap())
2128 .expect("that should be the employee id");
2129 let home_ip = parsed.get("home_ip").expect("home_ip attr should exist");
2130 assert_matches!(
2131 home_ip,
2132 &PartialValue::Value(Value {
2133 value: ValueKind::Lit(Literal::String(_)),
2134 ..
2135 }),
2136 );
2137 let trust_score = parsed
2138 .get("trust_score")
2139 .expect("trust_score attr should exist");
2140 assert_matches!(
2141 trust_score,
2142 &PartialValue::Value(Value {
2143 value: ValueKind::Lit(Literal::String(_)),
2144 ..
2145 }),
2146 );
2147 let manager = parsed.get("manager").expect("manager attr should exist");
2148 assert_matches!(
2149 manager,
2150 &PartialValue::Value(Value {
2151 value: ValueKind::Record(_),
2152 ..
2153 })
2154 );
2155 let work_ip = parsed.get("work_ip").expect("work_ip attr should exist");
2156 assert_matches!(
2157 work_ip,
2158 &PartialValue::Value(Value {
2159 value: ValueKind::Record(_),
2160 ..
2161 })
2162 );
2163 let hr_contacts = parsed
2164 .get("hr_contacts")
2165 .expect("hr_contacts attr should exist");
2166 assert_matches!(hr_contacts, PartialValue::Value(Value { value: ValueKind::Set(set), .. }) => {
2167 let contact = set.iter().next().expect("should be at least one contact");
2168 assert_matches!(contact, &Value { value: ValueKind::Record(_), .. });
2169 });
2170 let json_blob = parsed
2171 .get("json_blob")
2172 .expect("json_blob attr should exist");
2173 assert_matches!(json_blob, PartialValue::Value(Value { value: ValueKind::Record(record), .. }) => {
2174 let (_, inner1) = record
2175 .iter()
2176 .find(|(k, _)| *k == "inner1")
2177 .expect("inner1 attr should exist");
2178 assert_matches!(inner1, Value { value: ValueKind::Lit(Literal::Bool(_)), .. });
2179 let (_, inner3) = record
2180 .iter()
2181 .find(|(k, _)| *k == "inner3")
2182 .expect("inner3 attr should exist");
2183 assert_matches!(inner3, Value { value: ValueKind::Record(innerrecord), .. } => {
2184 let (_, innerinner) = innerrecord
2185 .iter()
2186 .find(|(k, _)| *k == "innerinner")
2187 .expect("innerinner attr should exist");
2188 assert_matches!(innerinner, Value { value: ValueKind::Record(_), .. });
2189 });
2190 });
2191 let eparser = EntityJsonParser::new(
2193 Some(&MockSchema),
2194 Extensions::all_available(),
2195 TCComputation::ComputeNow,
2196 );
2197 let parsed = eparser
2198 .from_json_value(entitiesjson)
2199 .expect("Should parse without error");
2200 assert_eq!(parsed.iter().count(), 1);
2201 let parsed = parsed
2202 .entity(&r#"Employee::"12UA45""#.parse().unwrap())
2203 .expect("that should be the employee id");
2204 let is_full_time = parsed
2205 .get("isFullTime")
2206 .expect("isFullTime attr should exist");
2207 assert_eq!(is_full_time, &PartialValue::Value(Value::from(true)),);
2208 let num_direct_reports = parsed
2209 .get("numDirectReports")
2210 .expect("numDirectReports attr should exist");
2211 assert_eq!(num_direct_reports, &PartialValue::Value(Value::from(3)),);
2212 let department = parsed
2213 .get("department")
2214 .expect("department attr should exist");
2215 assert_eq!(department, &PartialValue::Value(Value::from("Sales")),);
2216 let manager = parsed.get("manager").expect("manager attr should exist");
2217 assert_eq!(
2218 manager,
2219 &PartialValue::Value(Value::from(
2220 "Employee::\"34FB87\"".parse::<EntityUID>().expect("valid")
2221 )),
2222 );
2223 let hr_contacts = parsed
2224 .get("hr_contacts")
2225 .expect("hr_contacts attr should exist");
2226 assert_matches!(hr_contacts, PartialValue::Value(Value { value: ValueKind::Set(set), .. }) => {
2227 let contact = set.iter().next().expect("should be at least one contact");
2228 assert_matches!(contact, &Value { value: ValueKind::Lit(Literal::EntityUID(_)), .. });
2229 });
2230 let json_blob = parsed
2231 .get("json_blob")
2232 .expect("json_blob attr should exist");
2233 assert_matches!(json_blob, PartialValue::Value(Value { value: ValueKind::Record(record), .. }) => {
2234 let (_, inner1) = record
2235 .iter()
2236 .find(|(k, _)| *k == "inner1")
2237 .expect("inner1 attr should exist");
2238 assert_matches!(inner1, Value { value: ValueKind::Lit(Literal::Bool(_)), .. });
2239 let (_, inner3) = record
2240 .iter()
2241 .find(|(k, _)| *k == "inner3")
2242 .expect("inner3 attr should exist");
2243 assert_matches!(inner3, Value { value: ValueKind::Record(innerrecord), .. } => {
2244 let (_, innerinner) = innerrecord
2245 .iter()
2246 .find(|(k, _)| *k == "innerinner")
2247 .expect("innerinner attr should exist");
2248 assert_matches!(innerinner, Value { value: ValueKind::Lit(Literal::EntityUID(_)), .. });
2249 });
2250 });
2251 assert_eq!(
2252 parsed.get("home_ip").cloned().map(RestrictedExpr::try_from),
2253 Some(Ok(RestrictedExpr::call_extension_fn(
2254 Name::parse_unqualified_name("ip").expect("valid"),
2255 vec![RestrictedExpr::val("222.222.222.101")]
2256 ))),
2257 );
2258 assert_eq!(
2259 parsed.get("work_ip").cloned().map(RestrictedExpr::try_from),
2260 Some(Ok(RestrictedExpr::call_extension_fn(
2261 Name::parse_unqualified_name("ip").expect("valid"),
2262 vec![RestrictedExpr::val("2.2.2.0/24")]
2263 ))),
2264 );
2265 assert_eq!(
2266 parsed
2267 .get("trust_score")
2268 .cloned()
2269 .map(RestrictedExpr::try_from),
2270 Some(Ok(RestrictedExpr::call_extension_fn(
2271 Name::parse_unqualified_name("decimal").expect("valid"),
2272 vec![RestrictedExpr::val("5.7")]
2273 ))),
2274 );
2275 }
2276
2277 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2278 #[test]
2280 fn type_mismatch_string_long() {
2281 let entitiesjson = json!(
2282 [
2283 {
2284 "uid": { "type": "Employee", "id": "12UA45" },
2285 "attrs": {
2286 "isFullTime": true,
2287 "numDirectReports": "3",
2288 "department": "Sales",
2289 "manager": { "type": "Employee", "id": "34FB87" },
2290 "hr_contacts": [
2291 { "type": "HR", "id": "aaaaa" },
2292 { "type": "HR", "id": "bbbbb" }
2293 ],
2294 "json_blob": {
2295 "inner1": false,
2296 "inner2": "-*/",
2297 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2298 },
2299 "home_ip": "222.222.222.101",
2300 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2301 "trust_score": "5.7",
2302 "tricky": { "type": "Employee", "id": "34FB87" }
2303 },
2304 "parents": []
2305 }
2306 ]
2307 );
2308 let eparser = EntityJsonParser::new(
2309 Some(&MockSchema),
2310 Extensions::all_available(),
2311 TCComputation::ComputeNow,
2312 );
2313 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2314 expect_err(
2315 &entitiesjson,
2316 &miette::Report::new(e),
2317 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2318 .source(r#"in attribute `numDirectReports` on `Employee::"12UA45"`, type mismatch: value was expected to have type long, but it actually has type string: `"3"`"#)
2319 .build()
2320 );
2321 });
2322 }
2323
2324 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2325 #[test]
2327 fn type_mismatch_entity_record() {
2328 let entitiesjson = json!(
2329 [
2330 {
2331 "uid": { "type": "Employee", "id": "12UA45" },
2332 "attrs": {
2333 "isFullTime": true,
2334 "numDirectReports": 3,
2335 "department": "Sales",
2336 "manager": "34FB87",
2337 "hr_contacts": [
2338 { "type": "HR", "id": "aaaaa" },
2339 { "type": "HR", "id": "bbbbb" }
2340 ],
2341 "json_blob": {
2342 "inner1": false,
2343 "inner2": "-*/",
2344 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2345 },
2346 "home_ip": "222.222.222.101",
2347 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2348 "trust_score": "5.7",
2349 "tricky": { "type": "Employee", "id": "34FB87" }
2350 },
2351 "parents": []
2352 }
2353 ]
2354 );
2355 let eparser = EntityJsonParser::new(
2356 Some(&MockSchema),
2357 Extensions::all_available(),
2358 TCComputation::ComputeNow,
2359 );
2360 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2361 expect_err(
2362 &entitiesjson,
2363 &miette::Report::new(e),
2364 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2365 .source(r#"in attribute `manager` on `Employee::"12UA45"`, expected a literal entity reference, but got `"34FB87"`"#)
2366 .help(r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#)
2367 .build()
2368 );
2369 });
2370 }
2371
2372 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2373 #[test]
2375 fn type_mismatch_set_element() {
2376 let entitiesjson = json!(
2377 [
2378 {
2379 "uid": { "type": "Employee", "id": "12UA45" },
2380 "attrs": {
2381 "isFullTime": true,
2382 "numDirectReports": 3,
2383 "department": "Sales",
2384 "manager": { "type": "Employee", "id": "34FB87" },
2385 "hr_contacts": { "type": "HR", "id": "aaaaa" },
2386 "json_blob": {
2387 "inner1": false,
2388 "inner2": "-*/",
2389 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2390 },
2391 "home_ip": "222.222.222.101",
2392 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2393 "trust_score": "5.7",
2394 "tricky": { "type": "Employee", "id": "34FB87" }
2395 },
2396 "parents": []
2397 }
2398 ]
2399 );
2400 let eparser = EntityJsonParser::new(
2401 Some(&MockSchema),
2402 Extensions::all_available(),
2403 TCComputation::ComputeNow,
2404 );
2405 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2406 expect_err(
2407 &entitiesjson,
2408 &miette::Report::new(e),
2409 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2410 .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"}`"#)
2411 .build()
2412 );
2413 });
2414 }
2415
2416 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2417 #[test]
2419 fn type_mismatch_entity_types() {
2420 let entitiesjson = json!(
2421 [
2422 {
2423 "uid": { "type": "Employee", "id": "12UA45" },
2424 "attrs": {
2425 "isFullTime": true,
2426 "numDirectReports": 3,
2427 "department": "Sales",
2428 "manager": { "type": "HR", "id": "34FB87" },
2429 "hr_contacts": [
2430 { "type": "HR", "id": "aaaaa" },
2431 { "type": "HR", "id": "bbbbb" }
2432 ],
2433 "json_blob": {
2434 "inner1": false,
2435 "inner2": "-*/",
2436 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2437 },
2438 "home_ip": "222.222.222.101",
2439 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2440 "trust_score": "5.7",
2441 "tricky": { "type": "Employee", "id": "34FB87" }
2442 },
2443 "parents": []
2444 }
2445 ]
2446 );
2447 let eparser = EntityJsonParser::new(
2448 Some(&MockSchema),
2449 Extensions::all_available(),
2450 TCComputation::ComputeNow,
2451 );
2452 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2453 expect_err(
2454 &entitiesjson,
2455 &miette::Report::new(e),
2456 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2457 .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"`"#)
2458 .build()
2459 );
2460 });
2461 }
2462
2463 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2464 #[test]
2467 fn type_mismatch_extension_types() {
2468 let entitiesjson = json!(
2469 [
2470 {
2471 "uid": { "type": "Employee", "id": "12UA45" },
2472 "attrs": {
2473 "isFullTime": true,
2474 "numDirectReports": 3,
2475 "department": "Sales",
2476 "manager": { "type": "Employee", "id": "34FB87" },
2477 "hr_contacts": [
2478 { "type": "HR", "id": "aaaaa" },
2479 { "type": "HR", "id": "bbbbb" }
2480 ],
2481 "json_blob": {
2482 "inner1": false,
2483 "inner2": "-*/",
2484 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2485 },
2486 "home_ip": { "fn": "decimal", "arg": "3.33" },
2487 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2488 "trust_score": "5.7",
2489 "tricky": { "type": "Employee", "id": "34FB87" }
2490 },
2491 "parents": []
2492 }
2493 ]
2494 );
2495 let eparser = EntityJsonParser::new(
2496 Some(&MockSchema),
2497 Extensions::all_available(),
2498 TCComputation::ComputeNow,
2499 );
2500 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2501 expect_err(
2502 &entitiesjson,
2503 &miette::Report::new(e),
2504 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2505 .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")`"#)
2506 .build()
2507 );
2508 });
2509 }
2510
2511 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2512 #[test]
2513 fn missing_record_attr() {
2514 let entitiesjson = json!(
2516 [
2517 {
2518 "uid": { "type": "Employee", "id": "12UA45" },
2519 "attrs": {
2520 "isFullTime": true,
2521 "numDirectReports": 3,
2522 "department": "Sales",
2523 "manager": { "type": "Employee", "id": "34FB87" },
2524 "hr_contacts": [
2525 { "type": "HR", "id": "aaaaa" },
2526 { "type": "HR", "id": "bbbbb" }
2527 ],
2528 "json_blob": {
2529 "inner1": false,
2530 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2531 },
2532 "home_ip": "222.222.222.101",
2533 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2534 "trust_score": "5.7",
2535 "tricky": { "type": "Employee", "id": "34FB87" }
2536 },
2537 "parents": []
2538 }
2539 ]
2540 );
2541 let eparser = EntityJsonParser::new(
2542 Some(&MockSchema),
2543 Extensions::all_available(),
2544 TCComputation::ComputeNow,
2545 );
2546 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2547 expect_err(
2548 &entitiesjson,
2549 &miette::Report::new(e),
2550 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2551 .source(r#"in attribute `json_blob` on `Employee::"12UA45"`, expected the record to have an attribute `inner2`, but it does not"#)
2552 .build()
2553 );
2554 });
2555 }
2556
2557 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2558 #[test]
2560 fn type_mismatch_in_record_attr() {
2561 let entitiesjson = json!(
2562 [
2563 {
2564 "uid": { "type": "Employee", "id": "12UA45" },
2565 "attrs": {
2566 "isFullTime": true,
2567 "numDirectReports": 3,
2568 "department": "Sales",
2569 "manager": { "type": "Employee", "id": "34FB87" },
2570 "hr_contacts": [
2571 { "type": "HR", "id": "aaaaa" },
2572 { "type": "HR", "id": "bbbbb" }
2573 ],
2574 "json_blob": {
2575 "inner1": 33,
2576 "inner2": "-*/",
2577 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2578 },
2579 "home_ip": "222.222.222.101",
2580 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2581 "trust_score": "5.7",
2582 "tricky": { "type": "Employee", "id": "34FB87" }
2583 },
2584 "parents": []
2585 }
2586 ]
2587 );
2588 let eparser = EntityJsonParser::new(
2589 Some(&MockSchema),
2590 Extensions::all_available(),
2591 TCComputation::ComputeNow,
2592 );
2593 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2594 expect_err(
2595 &entitiesjson,
2596 &miette::Report::new(e),
2597 &ExpectedErrorMessageBuilder::error_starts_with("entity does not conform to the schema")
2598 .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`"#)
2599 .build()
2600 );
2601 });
2602
2603 let entitiesjson = json!(
2604 [
2605 {
2606 "uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
2607 "attrs": {
2608 "isFullTime": true,
2609 "numDirectReports": 3,
2610 "department": "Sales",
2611 "manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
2612 "hr_contacts": [
2613 { "type": "HR", "id": "aaaaa" },
2614 { "type": "HR", "id": "bbbbb" }
2615 ],
2616 "json_blob": {
2617 "inner1": false,
2618 "inner2": "-*/",
2619 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2620 },
2621 "home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
2622 "work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
2623 "trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
2624 "tricky": { "type": "Employee", "id": "34FB87" }
2625 },
2626 "parents": []
2627 }
2628 ]
2629 );
2630 let _ = eparser
2631 .from_json_value(entitiesjson)
2632 .expect("this version with explicit __entity and __extn escapes should also pass");
2633 }
2634
2635 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2636 #[test]
2638 fn unexpected_record_attr() {
2639 let entitiesjson = json!(
2640 [
2641 {
2642 "uid": { "type": "Employee", "id": "12UA45" },
2643 "attrs": {
2644 "isFullTime": true,
2645 "numDirectReports": 3,
2646 "department": "Sales",
2647 "manager": { "type": "Employee", "id": "34FB87" },
2648 "hr_contacts": [
2649 { "type": "HR", "id": "aaaaa" },
2650 { "type": "HR", "id": "bbbbb" }
2651 ],
2652 "json_blob": {
2653 "inner1": false,
2654 "inner2": "-*/",
2655 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2656 "inner4": "wat?"
2657 },
2658 "home_ip": "222.222.222.101",
2659 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2660 "trust_score": "5.7",
2661 "tricky": { "type": "Employee", "id": "34FB87" }
2662 },
2663 "parents": []
2664 }
2665 ]
2666 );
2667 let eparser = EntityJsonParser::new(
2668 Some(&MockSchema),
2669 Extensions::all_available(),
2670 TCComputation::ComputeNow,
2671 );
2672 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2673 expect_err(
2674 &entitiesjson,
2675 &miette::Report::new(e),
2676 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2677 .source(r#"in attribute `json_blob` on `Employee::"12UA45"`, record attribute `inner4` should not exist according to the schema"#)
2678 .build()
2679 );
2680 });
2681 }
2682
2683 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2684 #[test]
2686 fn missing_required_attr() {
2687 let entitiesjson = json!(
2688 [
2689 {
2690 "uid": { "type": "Employee", "id": "12UA45" },
2691 "attrs": {
2692 "isFullTime": true,
2693 "department": "Sales",
2694 "manager": { "type": "Employee", "id": "34FB87" },
2695 "hr_contacts": [
2696 { "type": "HR", "id": "aaaaa" },
2697 { "type": "HR", "id": "bbbbb" }
2698 ],
2699 "json_blob": {
2700 "inner1": false,
2701 "inner2": "-*/",
2702 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2703 },
2704 "home_ip": "222.222.222.101",
2705 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2706 "trust_score": "5.7",
2707 "tricky": { "type": "Employee", "id": "34FB87" }
2708 },
2709 "parents": []
2710 }
2711 ]
2712 );
2713 let eparser = EntityJsonParser::new(
2714 Some(&MockSchema),
2715 Extensions::all_available(),
2716 TCComputation::ComputeNow,
2717 );
2718 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2719 expect_err(
2720 &entitiesjson,
2721 &miette::Report::new(e),
2722 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2723 .source(r#"expected entity `Employee::"12UA45"` to have attribute `numDirectReports`, but it does not"#)
2724 .build()
2725 );
2726 });
2727 }
2728
2729 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2730 #[test]
2732 fn unexpected_entity_attr() {
2733 let entitiesjson = json!(
2734 [
2735 {
2736 "uid": { "type": "Employee", "id": "12UA45" },
2737 "attrs": {
2738 "isFullTime": true,
2739 "numDirectReports": 3,
2740 "department": "Sales",
2741 "manager": { "type": "Employee", "id": "34FB87" },
2742 "hr_contacts": [
2743 { "type": "HR", "id": "aaaaa" },
2744 { "type": "HR", "id": "bbbbb" }
2745 ],
2746 "json_blob": {
2747 "inner1": false,
2748 "inner2": "-*/",
2749 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2750 },
2751 "home_ip": "222.222.222.101",
2752 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2753 "trust_score": "5.7",
2754 "tricky": { "type": "Employee", "id": "34FB87" },
2755 "wat": "???",
2756 },
2757 "parents": []
2758 }
2759 ]
2760 );
2761 let eparser = EntityJsonParser::new(
2762 Some(&MockSchema),
2763 Extensions::all_available(),
2764 TCComputation::ComputeNow,
2765 );
2766 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2767 expect_err(
2768 &entitiesjson,
2769 &miette::Report::new(e),
2770 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2771 .source(r#"attribute `wat` on `Employee::"12UA45"` should not exist according to the schema"#)
2772 .build()
2773 );
2774 });
2775 }
2776
2777 #[cfg(all(feature = "decimal", feature = "ipaddr"))]
2778 #[test]
2780 fn parents_wrong_type() {
2781 let entitiesjson = json!(
2782 [
2783 {
2784 "uid": { "type": "Employee", "id": "12UA45" },
2785 "attrs": {
2786 "isFullTime": true,
2787 "numDirectReports": 3,
2788 "department": "Sales",
2789 "manager": { "type": "Employee", "id": "34FB87" },
2790 "hr_contacts": [
2791 { "type": "HR", "id": "aaaaa" },
2792 { "type": "HR", "id": "bbbbb" }
2793 ],
2794 "json_blob": {
2795 "inner1": false,
2796 "inner2": "-*/",
2797 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
2798 },
2799 "home_ip": "222.222.222.101",
2800 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
2801 "trust_score": "5.7",
2802 "tricky": { "type": "Employee", "id": "34FB87" }
2803 },
2804 "parents": [
2805 { "type": "Employee", "id": "34FB87" }
2806 ]
2807 }
2808 ]
2809 );
2810 let eparser = EntityJsonParser::new(
2811 Some(&MockSchema),
2812 Extensions::all_available(),
2813 TCComputation::ComputeNow,
2814 );
2815 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2816 expect_err(
2817 &entitiesjson,
2818 &miette::Report::new(e),
2819 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2820 .source(r#"`Employee::"12UA45"` is not allowed to have an ancestor of type `Employee` according to the schema"#)
2821 .build()
2822 );
2823 });
2824 }
2825
2826 #[test]
2828 fn undeclared_entity_type() {
2829 let entitiesjson = json!(
2830 [
2831 {
2832 "uid": { "type": "CEO", "id": "abcdef" },
2833 "attrs": {},
2834 "parents": []
2835 }
2836 ]
2837 );
2838 let eparser = EntityJsonParser::new(
2839 Some(&MockSchema),
2840 Extensions::all_available(),
2841 TCComputation::ComputeNow,
2842 );
2843 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2844 expect_err(
2845 &entitiesjson,
2846 &miette::Report::new(e),
2847 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
2848 .source(r#"entity `CEO::"abcdef"` has type `CEO` which is not declared in the schema"#)
2849 .build()
2850 );
2851 });
2852 }
2853
2854 #[test]
2856 fn undeclared_action() {
2857 let entitiesjson = json!(
2858 [
2859 {
2860 "uid": { "type": "Action", "id": "update" },
2861 "attrs": {},
2862 "parents": []
2863 }
2864 ]
2865 );
2866 let eparser = EntityJsonParser::new(
2867 Some(&MockSchema),
2868 Extensions::all_available(),
2869 TCComputation::ComputeNow,
2870 );
2871 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2872 expect_err(
2873 &entitiesjson,
2874 &miette::Report::new(e),
2875 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2876 .source(r#"found action entity `Action::"update"`, but it was not declared as an action in the schema"#)
2877 .build()
2878 );
2879 });
2880 }
2881
2882 #[test]
2884 fn action_declared_both_places() {
2885 let entitiesjson = json!(
2886 [
2887 {
2888 "uid": { "type": "Action", "id": "view" },
2889 "attrs": {
2890 "foo": 34
2891 },
2892 "parents": [
2893 { "type": "Action", "id": "readOnly" }
2894 ]
2895 }
2896 ]
2897 );
2898 let eparser = EntityJsonParser::new(
2899 Some(&MockSchema),
2900 Extensions::all_available(),
2901 TCComputation::ComputeNow,
2902 );
2903 let entities = eparser
2904 .from_json_value(entitiesjson)
2905 .expect("should parse sucessfully");
2906 assert_eq!(entities.iter().count(), 1);
2907 let expected_uid = r#"Action::"view""#.parse().expect("valid uid");
2908 let parsed_entity = match entities.entity(&expected_uid) {
2909 Dereference::Data(e) => e,
2910 _ => panic!("expected entity to exist and be concrete"),
2911 };
2912 assert_eq!(parsed_entity.uid(), &expected_uid);
2913 }
2914
2915 #[test]
2917 fn action_attr_wrong_val() {
2918 let entitiesjson = json!(
2919 [
2920 {
2921 "uid": { "type": "Action", "id": "view" },
2922 "attrs": {
2923 "foo": 6789
2924 },
2925 "parents": [
2926 { "type": "Action", "id": "readOnly" }
2927 ]
2928 }
2929 ]
2930 );
2931 let eparser = EntityJsonParser::new(
2932 Some(&MockSchema),
2933 Extensions::all_available(),
2934 TCComputation::ComputeNow,
2935 );
2936 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2937 expect_err(
2938 &entitiesjson,
2939 &miette::Report::new(e),
2940 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2941 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
2942 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
2943 .build()
2944 );
2945 });
2946 }
2947
2948 #[test]
2950 fn action_attr_wrong_type() {
2951 let entitiesjson = json!(
2952 [
2953 {
2954 "uid": { "type": "Action", "id": "view" },
2955 "attrs": {
2956 "foo": "bar"
2957 },
2958 "parents": [
2959 { "type": "Action", "id": "readOnly" }
2960 ]
2961 }
2962 ]
2963 );
2964 let eparser = EntityJsonParser::new(
2965 Some(&MockSchema),
2966 Extensions::all_available(),
2967 TCComputation::ComputeNow,
2968 );
2969 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
2970 expect_err(
2971 &entitiesjson,
2972 &miette::Report::new(e),
2973 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
2974 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
2975 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
2976 .build()
2977 );
2978 });
2979 }
2980
2981 #[test]
2983 fn action_attr_missing_in_json() {
2984 let entitiesjson = json!(
2985 [
2986 {
2987 "uid": { "type": "Action", "id": "view" },
2988 "attrs": {},
2989 "parents": [
2990 { "type": "Action", "id": "readOnly" }
2991 ]
2992 }
2993 ]
2994 );
2995 let eparser = EntityJsonParser::new(
2996 Some(&MockSchema),
2997 Extensions::all_available(),
2998 TCComputation::ComputeNow,
2999 );
3000 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3001 expect_err(
3002 &entitiesjson,
3003 &miette::Report::new(e),
3004 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3005 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3006 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3007 .build()
3008 );
3009 });
3010 }
3011
3012 #[test]
3014 fn action_attr_missing_in_schema() {
3015 let entitiesjson = json!(
3016 [
3017 {
3018 "uid": { "type": "Action", "id": "view" },
3019 "attrs": {
3020 "foo": "bar",
3021 "wow": false
3022 },
3023 "parents": [
3024 { "type": "Action", "id": "readOnly" }
3025 ]
3026 }
3027 ]
3028 );
3029 let eparser = EntityJsonParser::new(
3030 Some(&MockSchema),
3031 Extensions::all_available(),
3032 TCComputation::ComputeNow,
3033 );
3034 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3035 expect_err(
3036 &entitiesjson,
3037 &miette::Report::new(e),
3038 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3039 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3040 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3041 .build()
3042 );
3043 });
3044 }
3045
3046 #[test]
3048 fn action_parent_missing_in_json() {
3049 let entitiesjson = json!(
3050 [
3051 {
3052 "uid": { "type": "Action", "id": "view" },
3053 "attrs": {
3054 "foo": 34
3055 },
3056 "parents": []
3057 }
3058 ]
3059 );
3060 let eparser = EntityJsonParser::new(
3061 Some(&MockSchema),
3062 Extensions::all_available(),
3063 TCComputation::ComputeNow,
3064 );
3065 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3066 expect_err(
3067 &entitiesjson,
3068 &miette::Report::new(e),
3069 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3070 .source(r#"definition of action `Action::"view"` does not match its schema declaration"#)
3071 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3072 .build()
3073 );
3074 });
3075 }
3076
3077 #[test]
3079 fn action_parent_missing_in_schema() {
3080 let entitiesjson = json!(
3081 [
3082 {
3083 "uid": { "type": "Action", "id": "view" },
3084 "attrs": {
3085 "foo": 34
3086 },
3087 "parents": [
3088 { "type": "Action", "id": "readOnly" },
3089 { "type": "Action", "id": "coolActions" }
3090 ]
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#"definition of action `Action::"view"` does not match its schema declaration"#)
3105 .help(r#"to use the schema's definition of `Action::"view"`, simply omit it from the entities input data"#)
3106 .build()
3107 );
3108 });
3109 }
3110
3111 #[test]
3113 fn namespaces() {
3114 use std::str::FromStr;
3115
3116 struct MockSchema;
3117 impl Schema for MockSchema {
3118 type EntityTypeDescription = MockEmployeeDescription;
3119 type ActionEntityIterator = std::iter::Empty<Arc<Entity>>;
3120 fn entity_type(&self, entity_type: &EntityType) -> Option<MockEmployeeDescription> {
3121 if &entity_type.to_string() == "XYZCorp::Employee" {
3122 Some(MockEmployeeDescription)
3123 } else {
3124 None
3125 }
3126 }
3127 fn action(&self, _action: &EntityUID) -> Option<Arc<Entity>> {
3128 None
3129 }
3130 fn entity_types_with_basename<'a>(
3131 &'a self,
3132 basename: &'a UnreservedId,
3133 ) -> Box<dyn Iterator<Item = EntityType> + 'a> {
3134 match basename.as_ref() {
3135 "Employee" => Box::new(std::iter::once(EntityType::from(
3136 Name::from_str("XYZCorp::Employee").expect("valid name"),
3137 ))),
3138 _ => Box::new(std::iter::empty()),
3139 }
3140 }
3141 fn action_entities(&self) -> Self::ActionEntityIterator {
3142 std::iter::empty()
3143 }
3144 }
3145
3146 struct MockEmployeeDescription;
3147 impl EntityTypeDescription for MockEmployeeDescription {
3148 fn entity_type(&self) -> EntityType {
3149 "XYZCorp::Employee".parse().expect("valid")
3150 }
3151
3152 fn attr_type(&self, attr: &str) -> Option<SchemaType> {
3153 match attr {
3154 "isFullTime" => Some(SchemaType::Bool),
3155 "department" => Some(SchemaType::String),
3156 "manager" => Some(SchemaType::Entity {
3157 ty: self.entity_type(),
3158 }),
3159 _ => None,
3160 }
3161 }
3162
3163 fn required_attrs(&self) -> Box<dyn Iterator<Item = SmolStr>> {
3164 Box::new(
3165 ["isFullTime", "department", "manager"]
3166 .map(SmolStr::new)
3167 .into_iter(),
3168 )
3169 }
3170
3171 fn allowed_parent_types(&self) -> Arc<HashSet<EntityType>> {
3172 Arc::new(HashSet::new())
3173 }
3174
3175 fn open_attributes(&self) -> bool {
3176 false
3177 }
3178 }
3179
3180 let entitiesjson = json!(
3181 [
3182 {
3183 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3184 "attrs": {
3185 "isFullTime": true,
3186 "department": "Sales",
3187 "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
3188 },
3189 "parents": []
3190 }
3191 ]
3192 );
3193 let eparser = EntityJsonParser::new(
3194 Some(&MockSchema),
3195 Extensions::all_available(),
3196 TCComputation::ComputeNow,
3197 );
3198 let parsed = eparser
3199 .from_json_value(entitiesjson)
3200 .expect("Should parse without error");
3201 assert_eq!(parsed.iter().count(), 1);
3202 let parsed = parsed
3203 .entity(&r#"XYZCorp::Employee::"12UA45""#.parse().unwrap())
3204 .expect("that should be the employee type and id");
3205 let is_full_time = parsed
3206 .get("isFullTime")
3207 .expect("isFullTime attr should exist");
3208 assert_eq!(is_full_time, &PartialValue::from(true));
3209 let department = parsed
3210 .get("department")
3211 .expect("department attr should exist");
3212 assert_eq!(department, &PartialValue::from("Sales"),);
3213 let manager = parsed.get("manager").expect("manager attr should exist");
3214 assert_eq!(
3215 manager,
3216 &PartialValue::from(
3217 "XYZCorp::Employee::\"34FB87\""
3218 .parse::<EntityUID>()
3219 .expect("valid")
3220 ),
3221 );
3222
3223 let entitiesjson = json!(
3224 [
3225 {
3226 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3227 "attrs": {
3228 "isFullTime": true,
3229 "department": "Sales",
3230 "manager": { "type": "Employee", "id": "34FB87" }
3231 },
3232 "parents": []
3233 }
3234 ]
3235 );
3236
3237 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3238 expect_err(
3239 &entitiesjson,
3240 &miette::Report::new(e),
3241 &ExpectedErrorMessageBuilder::error("entity does not conform to the schema")
3242 .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"`"#)
3243 .build()
3244 );
3245 });
3246
3247 let entitiesjson = json!(
3248 [
3249 {
3250 "uid": { "type": "Employee", "id": "12UA45" },
3251 "attrs": {
3252 "isFullTime": true,
3253 "department": "Sales",
3254 "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
3255 },
3256 "parents": []
3257 }
3258 ]
3259 );
3260
3261 assert_matches!(eparser.from_json_value(entitiesjson.clone()), Err(e) => {
3262 expect_err(
3263 &entitiesjson,
3264 &miette::Report::new(e),
3265 &ExpectedErrorMessageBuilder::error("error during entity deserialization")
3266 .source(r#"entity `Employee::"12UA45"` has type `Employee` which is not declared in the schema"#)
3267 .help(r#"did you mean `XYZCorp::Employee`?"#)
3268 .build()
3269 );
3270 });
3271 }
3272}