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