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