1use crate::ast::*;
20use crate::transitive_closure::{compute_tc, enforce_tc_and_dag};
21use std::collections::HashMap;
22
23use serde::{Deserialize, Serialize};
24use serde_with::serde_as;
25
26mod err;
27pub use err::*;
28mod json;
29pub use json::*;
30
31#[serde_as]
38#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
39pub struct Entities {
40 #[serde_as(as = "Vec<(_, _)>")]
48 entities: HashMap<EntityUID, Entity>,
49
50 #[serde(default)]
55 #[serde(skip_deserializing)]
56 #[serde(skip_serializing)]
57 mode: Mode,
58}
59
60impl Entities {
61 pub fn new() -> Self {
63 Self {
64 entities: HashMap::new(),
65 mode: Mode::default(),
66 }
67 }
68
69 pub fn partial(self) -> Self {
73 Self {
74 entities: self.entities,
75 mode: Mode::Partial,
76 }
77 }
78
79 pub fn entity(&self, uid: &EntityUID) -> Dereference<'_, Entity> {
81 match self.entities.get(uid) {
82 Some(e) => Dereference::Data(e),
83 None => match self.mode {
84 Mode::Concrete => Dereference::NoSuchEntity,
85 Mode::Partial => Dereference::Residual(Expr::unknown(format!("{uid}"))),
86 },
87 }
88 }
89
90 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
92 self.entities.values()
93 }
94
95 pub fn from_entities(
100 entities: impl IntoIterator<Item = Entity>,
101 tc_computation: TCComputation,
102 ) -> Result<Self> {
103 let mut entity_map = entities.into_iter().map(|e| (e.uid(), e)).collect();
104 match tc_computation {
105 TCComputation::AssumeAlreadyComputed => {}
106 TCComputation::EnforceAlreadyComputed => {
107 enforce_tc_and_dag(&entity_map).map_err(Box::new)?;
108 }
109 TCComputation::ComputeNow => {
110 compute_tc(&mut entity_map, true).map_err(Box::new)?;
111 }
112 }
113 Ok(Self {
114 entities: entity_map,
115 mode: Mode::default(),
116 })
117 }
118
119 pub fn to_json_value(&self) -> Result<serde_json::Value> {
126 let ejsons: Vec<EntityJSON> = self.to_ejsons()?;
127 serde_json::to_value(ejsons)
128 .map_err(JsonSerializationError::from)
129 .map_err(Into::into)
130 }
131
132 pub fn write_to_json(&self, f: impl std::io::Write) -> Result<()> {
140 let ejsons: Vec<EntityJSON> = self.to_ejsons()?;
141 serde_json::to_writer_pretty(f, &ejsons).map_err(JsonSerializationError::from)?;
142 Ok(())
143 }
144
145 fn to_ejsons(&self) -> Result<Vec<EntityJSON>> {
147 self.entities
148 .values()
149 .map(EntityJSON::from_entity)
150 .collect::<std::result::Result<_, JsonSerializationError>>()
151 .map_err(Into::into)
152 }
153}
154
155impl std::fmt::Display for Entities {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 if self.entities.is_empty() {
158 write!(f, "<empty Entities>")
159 } else {
160 for e in self.entities.values() {
161 writeln!(f, "{e}")?;
162 }
163 Ok(())
164 }
165 }
166}
167
168#[derive(Debug, Clone)]
170pub enum Dereference<'a, T> {
171 NoSuchEntity,
173 Residual(Expr),
175 Data(&'a T),
177}
178
179impl<'a, T> Dereference<'a, T>
180where
181 T: std::fmt::Debug,
182{
183 pub fn unwrap(self) -> &'a T {
193 match self {
194 Self::Data(e) => e,
195 e => panic!("unwrap() called on {:?}", e),
196 }
197 }
198
199 pub fn expect(self, msg: &str) -> &'a T {
209 match self {
210 Self::Data(e) => e,
211 e => panic!("expect() called on {:?}, msg: {msg}", e),
212 }
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217enum Mode {
218 Concrete,
219 Partial,
220}
221
222impl Default for Mode {
223 fn default() -> Self {
224 Self::Concrete
225 }
226}
227
228#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
232pub enum TCComputation {
233 AssumeAlreadyComputed,
236 EnforceAlreadyComputed,
240 ComputeNow,
244}
245
246#[cfg(test)]
247mod json_parsing_tests {
248 use super::*;
249 use crate::extensions::Extensions;
250
251 #[test]
252 fn basic_partial() {
253 let json = serde_json::json!(
255 [
256 {
257 "uid": { "__expr": "test_entity_type::\"alice\"" },
258 "attrs": {},
259 "parents": [
260 { "__expr": "test_entity_type::\"jane\"" }
261 ]
262 },
263 {
264 "uid": { "__expr": "test_entity_type::\"jane\"" },
265 "attrs": {},
266 "parents": [
267 { "__expr": "test_entity_type::\"bob\"" }
268 ]
269 },
270 {
271 "uid": { "__expr": "test_entity_type::\"bob\"" },
272 "attrs": {},
273 "parents": []
274 }
275 ]
276 );
277
278 let eparser: EntityJsonParser<'_, '_> =
279 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
280 let es = eparser
281 .from_json_value(json)
282 .expect("JSON is correct")
283 .partial();
284
285 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
286 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
288
289 let janice = es.entity(&EntityUID::with_eid("janice"));
290
291 assert!(matches!(janice, Dereference::Residual(_)));
292 }
293
294 #[test]
295 fn basic() {
296 let json = serde_json::json!(
298 [
299 {
300 "uid": { "__expr": "test_entity_type::\"alice\"" },
301 "attrs": {},
302 "parents": [
303 { "__expr": "test_entity_type::\"jane\"" }
304 ]
305 },
306 {
307 "uid": { "__expr": "test_entity_type::\"jane\"" },
308 "attrs": {},
309 "parents": [
310 { "__expr": "test_entity_type::\"bob\"" }
311 ]
312 },
313 {
314 "uid": { "__expr": "test_entity_type::\"bob\"" },
315 "attrs": {},
316 "parents": []
317 }
318 ]
319 );
320
321 let eparser: EntityJsonParser<'_, '_> =
322 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
323 let es = eparser.from_json_value(json).expect("JSON is correct");
324
325 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
326 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
328 }
329
330 fn assert_attr_vals_are_shape_equal(
332 actual: Option<&RestrictedExpr>,
333 expected: &RestrictedExpr,
334 ) {
335 assert_eq!(
336 actual.map(|re| RestrictedExprShapeOnly::new(re.as_borrowed())),
337 Some(RestrictedExprShapeOnly::new(expected.as_borrowed()))
338 )
339 }
340
341 #[test]
343 fn more_escapes() {
344 let json = serde_json::json!(
345 [
346 {
347 "uid": { "__entity": { "type": "test_entity_type", "id": "alice" } },
348 "attrs": {
349 "bacon": "eggs",
350 "pancakes": [1, 2, 3],
351 "waffles": { "key": "value" },
352 "toast": { "__expr": "decimal(\"33.47\")" },
353 "12345": { "__entity": { "type": "test_entity_type", "id": "bob" } },
354 "a b c": { "__extn": { "fn": "ip", "arg": "222.222.222.0/24" } }
355 },
356 "parents": [
357 { "__expr": "test_entity_type::\"bob\"" },
358 { "__entity": { "type": "test_entity_type", "id": "catherine" } }
359 ]
360 },
361 {
362 "uid": { "__expr": "test_entity_type::\"bob\"" },
363 "attrs": {},
364 "parents": []
365 },
366 {
367 "uid": { "__expr": "test_entity_type::\"catherine\"" },
368 "attrs": {},
369 "parents": []
370 }
371 ]
372 );
373
374 let eparser: EntityJsonParser<'_, '_> =
375 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
376 let es = eparser.from_json_value(json).expect("JSON is correct");
377
378 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
379 assert_attr_vals_are_shape_equal(alice.get("bacon"), &RestrictedExpr::val("eggs"));
380 assert_attr_vals_are_shape_equal(
381 alice.get("pancakes"),
382 &RestrictedExpr::set([
383 RestrictedExpr::val(1),
384 RestrictedExpr::val(2),
385 RestrictedExpr::val(3),
386 ]),
387 );
388 assert_attr_vals_are_shape_equal(
389 alice.get("waffles"),
390 &RestrictedExpr::record([("key".into(), RestrictedExpr::val("value"))]),
391 );
392 assert_attr_vals_are_shape_equal(
393 alice.get("toast"),
394 &RestrictedExpr::call_extension_fn(
395 "decimal".parse().expect("should be a valid Name"),
396 vec![RestrictedExpr::val("33.47")],
397 ),
398 );
399 assert_attr_vals_are_shape_equal(
400 alice.get("12345"),
401 &RestrictedExpr::val(EntityUID::with_eid("bob")),
402 );
403 assert_attr_vals_are_shape_equal(
404 alice.get("a b c"),
405 &RestrictedExpr::call_extension_fn(
406 "ip".parse().expect("should be a valid Name"),
407 vec![RestrictedExpr::val("222.222.222.0/24")],
408 ),
409 );
410 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
411 assert!(alice.is_descendant_of(&EntityUID::with_eid("catherine")));
412 }
413
414 #[test]
415 fn implicit_and_explicit_escapes() {
416 let json = serde_json::json!(
419 [
420 {
421 "uid": { "__expr": "test_entity_type::\"alice\"" },
422 "attrs": {},
423 "parents": [
424 { "__expr": "test_entity_type::\"bob\"" },
425 { "__entity": { "type": "test_entity_type", "id": "charles" } },
426 "test_entity_type::\"darwin\"",
427 { "type": "test_entity_type", "id": "elaine" }
428 ]
429 },
430 {
431 "uid": { "__entity": { "type": "test_entity_type", "id": "bob" }},
432 "attrs": {},
433 "parents": []
434 },
435 {
436 "uid": "test_entity_type::\"charles\"",
437 "attrs": {},
438 "parents": []
439 },
440 {
441 "uid": { "type": "test_entity_type", "id": "darwin" },
442 "attrs": {},
443 "parents": []
444 },
445 {
446 "uid": { "type": "test_entity_type", "id": "elaine" },
447 "attrs": {},
448 "parents": [ "test_entity_type::\"darwin\"" ]
449 }
450 ]
451 );
452
453 let eparser: EntityJsonParser<'_, '_> =
454 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
455 let es = eparser.from_json_value(json).expect("JSON is correct");
456
457 let alice = es.entity(&EntityUID::with_eid("alice")).unwrap();
459 let bob = es.entity(&EntityUID::with_eid("bob")).unwrap();
460 let charles = es.entity(&EntityUID::with_eid("charles")).unwrap();
461 let darwin = es.entity(&EntityUID::with_eid("darwin")).unwrap();
462 let elaine = es.entity(&EntityUID::with_eid("elaine")).unwrap();
463
464 assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
466 assert!(alice.is_descendant_of(&EntityUID::with_eid("charles")));
467 assert!(alice.is_descendant_of(&EntityUID::with_eid("darwin")));
468 assert!(alice.is_descendant_of(&EntityUID::with_eid("elaine")));
469 assert_eq!(bob.ancestors().next(), None);
470 assert_eq!(charles.ancestors().next(), None);
471 assert_eq!(darwin.ancestors().next(), None);
472 assert!(elaine.is_descendant_of(&EntityUID::with_eid("darwin")));
473 assert!(!elaine.is_descendant_of(&EntityUID::with_eid("bob")));
474 }
475
476 #[test]
477 fn uid_failures() {
478 let eparser: EntityJsonParser<'_, '_> =
480 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
481
482 let json = serde_json::json!(
483 [
484 {
485 "uid": "hello",
486 "attrs": {},
487 "parents": []
488 }
489 ]
490 );
491 let err = eparser
492 .from_json_value(json)
493 .expect_err("should be an invalid uid field");
494 match err {
495 EntitiesError::DeserializationError(err) => {
496 assert!(
497 err.to_string().contains(
498 "In uid field of <unknown entity>, expected a literal entity reference, but got \"hello\""
499 ),
500 "actual error message was {}",
501 err
502 )
503 }
504 _ => panic!("expected deserialization error, got a different error: {err}"),
505 }
506
507 let json = serde_json::json!(
508 [
509 {
510 "uid": "\"hello\"",
511 "attrs": {},
512 "parents": []
513 }
514 ]
515 );
516 let err = eparser
517 .from_json_value(json)
518 .expect_err("should be an invalid uid field");
519 match err {
520 EntitiesError::DeserializationError(err) => assert!(
521 err.to_string()
522 .contains("expected a literal entity reference, but got \"hello\""),
523 "actual error message was {}",
524 err
525 ),
526 _ => panic!("expected deserialization error, got a different error: {err}"),
527 }
528
529 let json = serde_json::json!(
530 [
531 {
532 "uid": { "type": "foo", "spam": "eggs" },
533 "attrs": {},
534 "parents": []
535 }
536 ]
537 );
538 let err = eparser
539 .from_json_value(json)
540 .expect_err("should be an invalid uid field");
541 match err {
542 EntitiesError::DeserializationError(err) => assert!(err
543 .to_string()
544 .contains("did not match any variant of untagged enum")),
545 _ => panic!("expected deserialization error, got a different error: {err}"),
546 }
547
548 let json = serde_json::json!(
549 [
550 {
551 "uid": { "type": "foo", "id": "bar" },
552 "attrs": {},
553 "parents": "foo::\"help\""
554 }
555 ]
556 );
557 let err = eparser
558 .from_json_value(json)
559 .expect_err("should be an invalid parents field");
560 match err {
561 EntitiesError::DeserializationError(err) => {
562 assert!(err.to_string().contains("invalid type: string"))
563 }
564 _ => panic!("expected deserialization error, got a different error: {err}"),
565 }
566
567 let json = serde_json::json!(
568 [
569 {
570 "uid": { "type": "foo", "id": "bar" },
571 "attrs": {},
572 "parents": [
573 "foo::\"help\"",
574 { "__extn": { "fn": "ip", "arg": "222.222.222.0" } }
575 ]
576 }
577 ]
578 );
579 let err = eparser
580 .from_json_value(json)
581 .expect_err("should be an invalid parents field");
582 match err {
583 EntitiesError::DeserializationError(err) => assert!(err
584 .to_string()
585 .contains("did not match any variant of untagged enum")),
586 _ => panic!("expected deserialization error, got a different error: {err}"),
587 }
588 }
589
590 fn roundtrip(entities: &Entities) -> Result<Entities> {
592 let mut buf = Vec::new();
593 entities.write_to_json(&mut buf)?;
594 let eparser: EntityJsonParser<'_, '_> =
595 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
596 eparser.from_json_str(&String::from_utf8(buf).expect("should be valid UTF-8"))
597 }
598
599 fn test_entities() -> (Entity, Entity, Entity, Entity) {
601 (
602 Entity::with_uid(EntityUID::with_eid("test_principal")),
603 Entity::with_uid(EntityUID::with_eid("test_action")),
604 Entity::with_uid(EntityUID::with_eid("test_resource")),
605 Entity::with_uid(EntityUID::with_eid("test")),
606 )
607 }
608
609 #[test]
612 fn json_roundtripping() {
613 let empty_entities = Entities::new();
614 assert_eq!(
615 empty_entities,
616 roundtrip(&empty_entities).expect("should roundtrip without errors")
617 );
618
619 let (e0, e1, e2, e3) = test_entities();
620 let entities = Entities::from_entities([e0, e1, e2, e3], TCComputation::ComputeNow)
621 .expect("Failed to construct entities");
622 assert_eq!(
623 entities,
624 roundtrip(&entities).expect("should roundtrip without errors")
625 );
626
627 let complicated_entity = Entity::new(
628 EntityUID::with_eid("complicated"),
629 [
630 ("foo".into(), RestrictedExpr::val(false)),
631 ("bar".into(), RestrictedExpr::val(-234)),
632 ("ham".into(), RestrictedExpr::val(r#"a b c * / ? \"#)),
633 (
634 "123".into(),
635 RestrictedExpr::val(EntityUID::with_eid("mom")),
636 ),
637 (
638 "set".into(),
639 RestrictedExpr::set([
640 RestrictedExpr::val(0),
641 RestrictedExpr::val(EntityUID::with_eid("pancakes")),
642 RestrictedExpr::val("mmm"),
643 ]),
644 ),
645 (
646 "rec".into(),
647 RestrictedExpr::record([
648 ("nested".into(), RestrictedExpr::val("attr")),
649 (
650 "another".into(),
651 RestrictedExpr::val(EntityUID::with_eid("foo")),
652 ),
653 ]),
654 ),
655 (
656 "src_ip".into(),
657 RestrictedExpr::call_extension_fn(
658 "ip".parse().expect("should be a valid Name"),
659 vec![RestrictedExpr::val("222.222.222.222")],
660 ),
661 ),
662 ]
663 .into_iter()
664 .collect(),
665 [
666 EntityUID::with_eid("parent1"),
667 EntityUID::with_eid("parent2"),
668 ]
669 .into_iter()
670 .collect(),
671 );
672 let entities = Entities::from_entities(
673 [
674 complicated_entity,
675 Entity::with_uid(EntityUID::with_eid("parent1")),
676 Entity::with_uid(EntityUID::with_eid("parent2")),
677 ],
678 TCComputation::ComputeNow,
679 )
680 .expect("Failed to construct entities");
681 assert_eq!(
682 entities,
683 roundtrip(&entities).expect("should roundtrip without errors")
684 );
685
686 let oops_entity = Entity::new(
687 EntityUID::with_eid("oops"),
688 [(
689 "oops".into(),
691 RestrictedExpr::record([("__entity".into(), RestrictedExpr::val("hi"))]),
692 )]
693 .into_iter()
694 .collect(),
695 [
696 EntityUID::with_eid("parent1"),
697 EntityUID::with_eid("parent2"),
698 ]
699 .into_iter()
700 .collect(),
701 );
702 let entities = Entities::from_entities(
703 [
704 oops_entity,
705 Entity::with_uid(EntityUID::with_eid("parent1")),
706 Entity::with_uid(EntityUID::with_eid("parent2")),
707 ],
708 TCComputation::ComputeNow,
709 )
710 .expect("Failed to construct entities");
711 assert!(matches!(
712 roundtrip(&entities),
713 Err(EntitiesError::SerializationError(JsonSerializationError::ReservedKey { key })) if key.as_str() == "__entity"
714 ));
715 }
716}
717
718#[cfg(test)]
719mod entities_tests {
720 use super::*;
721
722 #[test]
723 fn empty_entities() {
724 let e = Entities::new();
725 let es = e.iter().collect::<Vec<_>>();
726 assert!(es.is_empty(), "This vec should be empty");
727 }
728
729 fn test_entities() -> (Entity, Entity, Entity, Entity) {
731 (
732 Entity::with_uid(EntityUID::with_eid("test_principal")),
733 Entity::with_uid(EntityUID::with_eid("test_action")),
734 Entity::with_uid(EntityUID::with_eid("test_resource")),
735 Entity::with_uid(EntityUID::with_eid("test")),
736 )
737 }
738
739 #[test]
740 fn test_iter() {
741 let (e0, e1, e2, e3) = test_entities();
742 let v = vec![e0.clone(), e1.clone(), e2.clone(), e3.clone()];
743 let es = Entities::from_entities(v, TCComputation::ComputeNow)
744 .expect("Failed to construct entities");
745 let es_v = es.iter().collect::<Vec<_>>();
746 assert!(es_v.len() == 4, "All entities should be in the vec");
747 assert!(es_v.contains(&&e0));
748 assert!(es_v.contains(&&e1));
749 assert!(es_v.contains(&&e2));
750 assert!(es_v.contains(&&e3));
751 }
752
753 #[test]
754 fn test_enforce_already_computed_fail() {
755 let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
759 let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
760 let e3 = Entity::with_uid(EntityUID::with_eid("c"));
761 e1.add_ancestor(EntityUID::with_eid("b"));
762 e2.add_ancestor(EntityUID::with_eid("c"));
763
764 let es = Entities::from_entities(vec![e1, e2, e3], TCComputation::EnforceAlreadyComputed);
765 match es {
766 Ok(_) => panic!("Was not transitively closed!"),
767 Err(EntitiesError::TransitiveClosureError(_)) => (),
768 Err(_) => panic!("Wrong Error!"),
769 };
770 }
771
772 #[test]
773 fn test_enforce_already_computed_succeed() {
774 let mut e1 = Entity::with_uid(EntityUID::with_eid("a"));
779 let mut e2 = Entity::with_uid(EntityUID::with_eid("b"));
780 let e3 = Entity::with_uid(EntityUID::with_eid("c"));
781 e1.add_ancestor(EntityUID::with_eid("b"));
782 e1.add_ancestor(EntityUID::with_eid("c"));
783 e2.add_ancestor(EntityUID::with_eid("c"));
784
785 Entities::from_entities(vec![e1, e2, e3], TCComputation::EnforceAlreadyComputed)
786 .expect("Should have succeeded");
787 }
788}
789
790#[cfg(test)]
791mod schema_based_parsing_tests {
792 use super::*;
793 use crate::extensions::Extensions;
794 use serde_json::json;
795 use smol_str::SmolStr;
796
797 #[test]
799 fn attr_types() {
800 struct MockSchema;
801 impl Schema for MockSchema {
802 fn attr_type(&self, entity_type: &EntityType, attr: &str) -> Option<SchemaType> {
803 let employee_ty = || SchemaType::Entity {
804 ty: EntityType::Concrete(
805 Name::parse_unqualified_name("Employee").expect("valid"),
806 ),
807 };
808 let hr_ty = || SchemaType::Entity {
809 ty: EntityType::Concrete(Name::parse_unqualified_name("HR").expect("valid")),
810 };
811 match entity_type.to_string().as_str() {
812 "Employee" => match attr {
813 "isFullTime" => Some(SchemaType::Bool),
814 "numDirectReports" => Some(SchemaType::Long),
815 "department" => Some(SchemaType::String),
816 "manager" => Some(employee_ty()),
817 "hr_contacts" => Some(SchemaType::Set {
818 element_ty: Box::new(hr_ty()),
819 }),
820 "json_blob" => Some(SchemaType::Record {
821 attrs: [
822 ("inner1".into(), AttributeType::required(SchemaType::Bool)),
823 ("inner2".into(), AttributeType::required(SchemaType::String)),
824 (
825 "inner3".into(),
826 AttributeType::required(SchemaType::Record {
827 attrs: [(
828 "innerinner".into(),
829 AttributeType::required(employee_ty()),
830 )]
831 .into_iter()
832 .collect(),
833 }),
834 ),
835 ]
836 .into_iter()
837 .collect(),
838 }),
839 "home_ip" => Some(SchemaType::Extension {
840 name: Name::parse_unqualified_name("ipaddr").expect("valid"),
841 }),
842 "work_ip" => Some(SchemaType::Extension {
843 name: Name::parse_unqualified_name("ipaddr").expect("valid"),
844 }),
845 "trust_score" => Some(SchemaType::Extension {
846 name: Name::parse_unqualified_name("decimal").expect("valid"),
847 }),
848 "tricky" => Some(SchemaType::Record {
849 attrs: [
850 ("type".into(), AttributeType::required(SchemaType::String)),
851 ("id".into(), AttributeType::required(SchemaType::String)),
852 ]
853 .into_iter()
854 .collect(),
855 }),
856 _ => None,
857 },
858 _ => None,
859 }
860 }
861
862 fn required_attrs(
863 &self,
864 _entity_type: &EntityType,
865 ) -> Box<dyn Iterator<Item = SmolStr>> {
866 Box::new(
867 [
868 "isFullTime",
869 "numDirectReports",
870 "department",
871 "manager",
872 "hr_contacts",
873 "json_blob",
874 "home_ip",
875 "work_ip",
876 "trust_score",
877 ]
878 .map(SmolStr::new)
879 .into_iter(),
880 )
881 }
882 }
883
884 let entitiesjson = json!(
885 [
886 {
887 "uid": { "type": "Employee", "id": "12UA45" },
888 "attrs": {
889 "isFullTime": true,
890 "numDirectReports": 3,
891 "department": "Sales",
892 "manager": { "type": "Employee", "id": "34FB87" },
893 "hr_contacts": [
894 { "type": "HR", "id": "aaaaa" },
895 { "type": "HR", "id": "bbbbb" }
896 ],
897 "json_blob": {
898 "inner1": false,
899 "inner2": "-*/",
900 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
901 },
902 "home_ip": "222.222.222.101",
903 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
904 "trust_score": "5.7",
905 "tricky": { "type": "Employee", "id": "34FB87" }
906 },
907 "parents": []
908 }
909 ]
910 );
911 let eparser: EntityJsonParser<'_, '_> =
915 EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
916 let parsed = eparser
917 .from_json_value(entitiesjson.clone())
918 .expect("Should parse without error");
919 assert_eq!(parsed.iter().count(), 1);
920 let parsed = parsed
921 .entity(&r#"Employee::"12UA45""#.parse().unwrap())
922 .expect("that should be the employee id");
923 let home_ip = parsed.get("home_ip").expect("home_ip attr should exist");
924 assert!(matches!(
925 home_ip.expr_kind(),
926 &ExprKind::Lit(Literal::String(_)),
927 ));
928 let trust_score = parsed
929 .get("trust_score")
930 .expect("trust_score attr should exist");
931 assert!(matches!(
932 trust_score.expr_kind(),
933 &ExprKind::Lit(Literal::String(_)),
934 ));
935 let manager = parsed.get("manager").expect("manager attr should exist");
936 assert!(matches!(manager.expr_kind(), &ExprKind::Record { .. }));
937 let work_ip = parsed.get("work_ip").expect("work_ip attr should exist");
938 assert!(matches!(work_ip.expr_kind(), &ExprKind::Record { .. }));
939 let hr_contacts = parsed
940 .get("hr_contacts")
941 .expect("hr_contacts attr should exist");
942 assert!(matches!(hr_contacts.expr_kind(), &ExprKind::Set(_)));
943 let contact = {
944 let ExprKind::Set(set) = hr_contacts.expr_kind() else { panic!("already checked it was Set") };
945 set.iter().next().expect("should be at least one contact")
946 };
947 assert!(matches!(contact.expr_kind(), &ExprKind::Record { .. }));
948 let json_blob = parsed
949 .get("json_blob")
950 .expect("json_blob attr should exist");
951 let ExprKind::Record { pairs } = json_blob.expr_kind() else { panic!("expected json_blob to be a Record") };
952 let (_, inner1) = pairs
953 .iter()
954 .find(|(k, _)| k == "inner1")
955 .expect("inner1 attr should exist");
956 assert!(matches!(
957 inner1.expr_kind(),
958 &ExprKind::Lit(Literal::Bool(_))
959 ));
960 let (_, inner3) = pairs
961 .iter()
962 .find(|(k, _)| k == "inner3")
963 .expect("inner3 attr should exist");
964 assert!(matches!(inner3.expr_kind(), &ExprKind::Record { .. }));
965 let ExprKind::Record { pairs: innerpairs } = inner3.expr_kind() else { panic!("already checked it was Record") };
966 let (_, innerinner) = innerpairs
967 .iter()
968 .find(|(k, _)| k == "innerinner")
969 .expect("innerinner attr should exist");
970 assert!(matches!(innerinner.expr_kind(), &ExprKind::Record { .. }));
971 let eparser = EntityJsonParser::new(
973 Some(&MockSchema),
974 Extensions::all_available(),
975 TCComputation::ComputeNow,
976 );
977 let parsed = eparser
978 .from_json_value(entitiesjson)
979 .expect("Should parse without error");
980 assert_eq!(parsed.iter().count(), 1);
981 let parsed = parsed
982 .entity(&r#"Employee::"12UA45""#.parse().unwrap())
983 .expect("that should be the employee id");
984 let is_full_time = parsed
985 .get("isFullTime")
986 .expect("isFullTime attr should exist");
987 assert_eq!(
988 RestrictedExprShapeOnly::new(is_full_time.as_borrowed()),
989 RestrictedExprShapeOnly::new(RestrictedExpr::val(true).as_borrowed())
990 );
991 let num_direct_reports = parsed
992 .get("numDirectReports")
993 .expect("numDirectReports attr should exist");
994 assert_eq!(
995 RestrictedExprShapeOnly::new(num_direct_reports.as_borrowed()),
996 RestrictedExprShapeOnly::new(RestrictedExpr::val(3).as_borrowed())
997 );
998 let department = parsed
999 .get("department")
1000 .expect("department attr should exist");
1001 assert_eq!(
1002 RestrictedExprShapeOnly::new(department.as_borrowed()),
1003 RestrictedExprShapeOnly::new(RestrictedExpr::val("Sales").as_borrowed())
1004 );
1005 let manager = parsed.get("manager").expect("manager attr should exist");
1006 assert_eq!(
1007 RestrictedExprShapeOnly::new(manager.as_borrowed()),
1008 RestrictedExprShapeOnly::new(
1009 RestrictedExpr::val("Employee::\"34FB87\"".parse::<EntityUID>().expect("valid"))
1010 .as_borrowed()
1011 )
1012 );
1013 let hr_contacts = parsed
1014 .get("hr_contacts")
1015 .expect("hr_contacts attr should exist");
1016 assert!(matches!(hr_contacts.expr_kind(), &ExprKind::Set(_)));
1017 let contact = {
1018 let ExprKind::Set(set) = hr_contacts.expr_kind() else { panic!("already checked it was Set") };
1019 set.iter().next().expect("should be at least one contact")
1020 };
1021 assert!(matches!(
1022 contact.expr_kind(),
1023 &ExprKind::Lit(Literal::EntityUID(_))
1024 ));
1025 let json_blob = parsed
1026 .get("json_blob")
1027 .expect("json_blob attr should exist");
1028 let ExprKind::Record { pairs } = json_blob.expr_kind() else { panic!("expected json_blob to be a Record") };
1029 let (_, inner1) = pairs
1030 .iter()
1031 .find(|(k, _)| k == "inner1")
1032 .expect("inner1 attr should exist");
1033 assert!(matches!(
1034 inner1.expr_kind(),
1035 &ExprKind::Lit(Literal::Bool(_))
1036 ));
1037 let (_, inner3) = pairs
1038 .iter()
1039 .find(|(k, _)| k == "inner3")
1040 .expect("inner3 attr should exist");
1041 assert!(matches!(inner3.expr_kind(), &ExprKind::Record { .. }));
1042 let ExprKind::Record { pairs: innerpairs } = inner3.expr_kind() else { panic!("already checked it was Record") };
1043 let (_, innerinner) = innerpairs
1044 .iter()
1045 .find(|(k, _)| k == "innerinner")
1046 .expect("innerinner attr should exist");
1047 assert!(matches!(
1048 innerinner.expr_kind(),
1049 &ExprKind::Lit(Literal::EntityUID(_))
1050 ));
1051 assert_eq!(
1052 parsed.get("home_ip"),
1053 Some(&RestrictedExpr::call_extension_fn(
1054 Name::parse_unqualified_name("ip").expect("valid"),
1055 vec![RestrictedExpr::val("222.222.222.101")]
1056 )),
1057 );
1058 assert_eq!(
1059 parsed.get("work_ip"),
1060 Some(&RestrictedExpr::call_extension_fn(
1061 Name::parse_unqualified_name("ip").expect("valid"),
1062 vec![RestrictedExpr::val("2.2.2.0/24")]
1063 )),
1064 );
1065 assert_eq!(
1066 parsed.get("trust_score"),
1067 Some(&RestrictedExpr::call_extension_fn(
1068 Name::parse_unqualified_name("decimal").expect("valid"),
1069 vec![RestrictedExpr::val("5.7")]
1070 )),
1071 );
1072
1073 let entitiesjson = json!(
1075 [
1076 {
1077 "uid": { "type": "Employee", "id": "12UA45" },
1078 "attrs": {
1079 "isFullTime": true,
1080 "numDirectReports": "3",
1081 "department": "Sales",
1082 "manager": { "type": "Employee", "id": "34FB87" },
1083 "hr_contacts": [
1084 { "type": "HR", "id": "aaaaa" },
1085 { "type": "HR", "id": "bbbbb" }
1086 ],
1087 "json_blob": {
1088 "inner1": false,
1089 "inner2": "-*/",
1090 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1091 },
1092 "home_ip": "222.222.222.101",
1093 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1094 "trust_score": "5.7",
1095 "tricky": { "type": "Employee", "id": "34FB87" }
1096 },
1097 "parents": []
1098 }
1099 ]
1100 );
1101 let err = eparser
1102 .from_json_value(entitiesjson)
1103 .expect_err("should fail due to type mismatch on numDirectReports");
1104 assert!(
1105 err.to_string().contains(r#"In attribute "numDirectReports" on Employee::"12UA45", type mismatch: attribute was expected to have type long, but actually has type string"#),
1106 "actual error message was {}",
1107 err
1108 );
1109
1110 let entitiesjson = json!(
1112 [
1113 {
1114 "uid": { "type": "Employee", "id": "12UA45" },
1115 "attrs": {
1116 "isFullTime": true,
1117 "numDirectReports": 3,
1118 "department": "Sales",
1119 "manager": "34FB87",
1120 "hr_contacts": [
1121 { "type": "HR", "id": "aaaaa" },
1122 { "type": "HR", "id": "bbbbb" }
1123 ],
1124 "json_blob": {
1125 "inner1": false,
1126 "inner2": "-*/",
1127 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1128 },
1129 "home_ip": "222.222.222.101",
1130 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1131 "trust_score": "5.7",
1132 "tricky": { "type": "Employee", "id": "34FB87" }
1133 },
1134 "parents": []
1135 }
1136 ]
1137 );
1138 let err = eparser
1139 .from_json_value(entitiesjson)
1140 .expect_err("should fail due to type mismatch on manager");
1141 assert!(
1142 err.to_string()
1143 .contains(r#"In attribute "manager" on Employee::"12UA45", expected a literal entity reference, but got "34FB87""#),
1144 "actual error message was {}",
1145 err
1146 );
1147
1148 let entitiesjson = json!(
1150 [
1151 {
1152 "uid": { "type": "Employee", "id": "12UA45" },
1153 "attrs": {
1154 "isFullTime": true,
1155 "numDirectReports": 3,
1156 "department": "Sales",
1157 "manager": { "type": "Employee", "id": "34FB87" },
1158 "hr_contacts": { "type": "HR", "id": "aaaaa" },
1159 "json_blob": {
1160 "inner1": false,
1161 "inner2": "-*/",
1162 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1163 },
1164 "home_ip": "222.222.222.101",
1165 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1166 "trust_score": "5.7",
1167 "tricky": { "type": "Employee", "id": "34FB87" }
1168 },
1169 "parents": []
1170 }
1171 ]
1172 );
1173 let err = eparser
1174 .from_json_value(entitiesjson)
1175 .expect_err("should fail due to type mismatch on hr_contacts");
1176 assert!(
1177 err.to_string().contains(r#"In attribute "hr_contacts" on Employee::"12UA45", type mismatch: attribute was expected to have type (set of (entity of type HR)), but actually has type record"#),
1178 "actual error message was {}",
1179 err
1180 );
1181
1182 let entitiesjson = json!(
1184 [
1185 {
1186 "uid": { "type": "Employee", "id": "12UA45" },
1187 "attrs": {
1188 "isFullTime": true,
1189 "numDirectReports": 3,
1190 "department": "Sales",
1191 "manager": { "type": "HR", "id": "34FB87" },
1192 "hr_contacts": [
1193 { "type": "HR", "id": "aaaaa" },
1194 { "type": "HR", "id": "bbbbb" }
1195 ],
1196 "json_blob": {
1197 "inner1": false,
1198 "inner2": "-*/",
1199 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1200 },
1201 "home_ip": "222.222.222.101",
1202 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1203 "trust_score": "5.7",
1204 "tricky": { "type": "Employee", "id": "34FB87" }
1205 },
1206 "parents": []
1207 }
1208 ]
1209 );
1210 let err = eparser
1211 .from_json_value(entitiesjson)
1212 .expect_err("should fail due to type mismatch on manager");
1213 assert!(
1214 err.to_string().contains(r#"In attribute "manager" on Employee::"12UA45", type mismatch: attribute was expected to have type (entity of type Employee), but actually has type (entity of type HR)"#),
1215 "actual error message was {}",
1216 err
1217 );
1218
1219 let entitiesjson = json!(
1222 [
1223 {
1224 "uid": { "type": "Employee", "id": "12UA45" },
1225 "attrs": {
1226 "isFullTime": true,
1227 "numDirectReports": 3,
1228 "department": "Sales",
1229 "manager": { "type": "Employee", "id": "34FB87" },
1230 "hr_contacts": [
1231 { "type": "HR", "id": "aaaaa" },
1232 { "type": "HR", "id": "bbbbb" }
1233 ],
1234 "json_blob": {
1235 "inner1": false,
1236 "inner2": "-*/",
1237 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1238 },
1239 "home_ip": { "fn": "decimal", "arg": "3.33" },
1240 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1241 "trust_score": "5.7",
1242 "tricky": { "type": "Employee", "id": "34FB87" }
1243 },
1244 "parents": []
1245 }
1246 ]
1247 );
1248 let err = eparser
1249 .from_json_value(entitiesjson)
1250 .expect_err("should fail due to type mismatch on home_ip");
1251 assert!(
1252 err.to_string().contains(r#"In attribute "home_ip" on Employee::"12UA45", type mismatch: attribute was expected to have type ipaddr, but actually has type decimal"#),
1253 "actual error message was {}",
1254 err
1255 );
1256
1257 let entitiesjson = json!(
1259 [
1260 {
1261 "uid": { "type": "Employee", "id": "12UA45" },
1262 "attrs": {
1263 "isFullTime": true,
1264 "numDirectReports": 3,
1265 "department": "Sales",
1266 "manager": { "type": "Employee", "id": "34FB87" },
1267 "hr_contacts": [
1268 { "type": "HR", "id": "aaaaa" },
1269 { "type": "HR", "id": "bbbbb" }
1270 ],
1271 "json_blob": {
1272 "inner1": false,
1273 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1274 },
1275 "home_ip": "222.222.222.101",
1276 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1277 "trust_score": "5.7",
1278 "tricky": { "type": "Employee", "id": "34FB87" }
1279 },
1280 "parents": []
1281 }
1282 ]
1283 );
1284 let err = eparser
1285 .from_json_value(entitiesjson)
1286 .expect_err("should fail due to missing attribute \"inner2\"");
1287 assert!(
1288 err.to_string().contains(r#"In attribute "json_blob" on Employee::"12UA45", expected the record to have an attribute "inner2", but it didn't"#),
1289 "actual error message was {}",
1290 err
1291 );
1292
1293 let entitiesjson = json!(
1295 [
1296 {
1297 "uid": { "type": "Employee", "id": "12UA45" },
1298 "attrs": {
1299 "isFullTime": true,
1300 "numDirectReports": 3,
1301 "department": "Sales",
1302 "manager": { "type": "Employee", "id": "34FB87" },
1303 "hr_contacts": [
1304 { "type": "HR", "id": "aaaaa" },
1305 { "type": "HR", "id": "bbbbb" }
1306 ],
1307 "json_blob": {
1308 "inner1": 33,
1309 "inner2": "-*/",
1310 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1311 },
1312 "home_ip": "222.222.222.101",
1313 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1314 "trust_score": "5.7",
1315 "tricky": { "type": "Employee", "id": "34FB87" }
1316 },
1317 "parents": []
1318 }
1319 ]
1320 );
1321 let err = eparser
1322 .from_json_value(entitiesjson)
1323 .expect_err("should fail due to type mismatch on attribute \"inner1\"");
1324 assert!(
1325 err.to_string().contains(r#"In attribute "json_blob" on Employee::"12UA45", type mismatch: attribute was expected to have type record with attributes: "#),
1326 "actual error message was {}",
1327 err
1328 );
1329
1330 let entitiesjson = json!(
1331 [
1332 {
1333 "uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
1334 "attrs": {
1335 "isFullTime": true,
1336 "numDirectReports": 3,
1337 "department": "Sales",
1338 "manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
1339 "hr_contacts": [
1340 { "type": "HR", "id": "aaaaa" },
1341 { "type": "HR", "id": "bbbbb" }
1342 ],
1343 "json_blob": {
1344 "inner1": false,
1345 "inner2": "-*/",
1346 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1347 },
1348 "home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
1349 "work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
1350 "trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
1351 "tricky": { "type": "Employee", "id": "34FB87" }
1352 },
1353 "parents": []
1354 }
1355 ]
1356 );
1357 let _ = eparser
1358 .from_json_value(entitiesjson)
1359 .expect("this version with explicit __entity and __extn escapes should also pass");
1360
1361 let entitiesjson = json!(
1363 [
1364 {
1365 "uid": { "type": "Employee", "id": "12UA45" },
1366 "attrs": {
1367 "isFullTime": true,
1368 "numDirectReports": 3,
1369 "department": "Sales",
1370 "manager": { "type": "Employee", "id": "34FB87" },
1371 "hr_contacts": [
1372 { "type": "HR", "id": "aaaaa" },
1373 { "type": "HR", "id": "bbbbb" }
1374 ],
1375 "json_blob": {
1376 "inner1": false,
1377 "inner2": "-*/",
1378 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1379 "inner4": "wat?"
1380 },
1381 "home_ip": "222.222.222.101",
1382 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1383 "trust_score": "5.7",
1384 "tricky": { "type": "Employee", "id": "34FB87" }
1385 },
1386 "parents": []
1387 }
1388 ]
1389 );
1390 let err = eparser
1391 .from_json_value(entitiesjson)
1392 .expect_err("should fail due to unexpected attribute \"inner4\"");
1393 assert!(
1394 err.to_string().contains(r#"In attribute "json_blob" on Employee::"12UA45", record attribute "inner4" shouldn't exist"#),
1395 "actual error message was {}",
1396 err
1397 );
1398
1399 let entitiesjson = json!(
1401 [
1402 {
1403 "uid": { "type": "Employee", "id": "12UA45" },
1404 "attrs": {
1405 "isFullTime": true,
1406 "department": "Sales",
1407 "manager": { "type": "Employee", "id": "34FB87" },
1408 "hr_contacts": [
1409 { "type": "HR", "id": "aaaaa" },
1410 { "type": "HR", "id": "bbbbb" }
1411 ],
1412 "json_blob": {
1413 "inner1": false,
1414 "inner2": "-*/",
1415 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1416 },
1417 "home_ip": "222.222.222.101",
1418 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1419 "trust_score": "5.7",
1420 "tricky": { "type": "Employee", "id": "34FB87" }
1421 },
1422 "parents": []
1423 }
1424 ]
1425 );
1426 let err = eparser
1427 .from_json_value(entitiesjson)
1428 .expect_err("should fail due to missing attribute \"numDirectReports\"");
1429 assert!(
1430 err.to_string().contains(r#"Expected Employee::"12UA45" to have an attribute "numDirectReports", but it didn't"#),
1431 "actual error message was {}",
1432 err
1433 );
1434
1435 let entitiesjson = json!(
1437 [
1438 {
1439 "uid": { "type": "Employee", "id": "12UA45" },
1440 "attrs": {
1441 "isFullTime": true,
1442 "numDirectReports": 3,
1443 "department": "Sales",
1444 "manager": { "type": "Employee", "id": "34FB87" },
1445 "hr_contacts": [
1446 { "type": "HR", "id": "aaaaa" },
1447 { "type": "HR", "id": "bbbbb" }
1448 ],
1449 "json_blob": {
1450 "inner1": false,
1451 "inner2": "-*/",
1452 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
1453 },
1454 "home_ip": "222.222.222.101",
1455 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
1456 "trust_score": "5.7",
1457 "tricky": { "type": "Employee", "id": "34FB87" },
1458 "wat": "???",
1459 },
1460 "parents": []
1461 }
1462 ]
1463 );
1464 let err = eparser
1465 .from_json_value(entitiesjson)
1466 .expect_err("should fail due to unexpected attribute \"wat\"");
1467 assert!(
1468 err.to_string().contains(
1469 r#"Attribute "wat" on Employee::"12UA45" shouldn't exist according to the schema"#
1470 ),
1471 "actual error message was {}",
1472 err
1473 );
1474 }
1475
1476 #[test]
1478 fn namespaces() {
1479 struct MockSchema;
1480 impl Schema for MockSchema {
1481 fn attr_type(&self, entity_type: &EntityType, attr: &str) -> Option<SchemaType> {
1482 match entity_type.to_string().as_str() {
1483 "XYZCorp::Employee" => match attr {
1484 "isFullTime" => Some(SchemaType::Bool),
1485 "department" => Some(SchemaType::String),
1486 "manager" => Some(SchemaType::Entity {
1487 ty: EntityType::Concrete("XYZCorp::Employee".parse().expect("valid")),
1488 }),
1489 _ => None,
1490 },
1491 _ => None,
1492 }
1493 }
1494
1495 fn required_attrs(
1496 &self,
1497 _entity_type: &EntityType,
1498 ) -> Box<dyn Iterator<Item = SmolStr>> {
1499 Box::new(
1500 ["isFullTime", "department", "manager"]
1501 .map(SmolStr::new)
1502 .into_iter(),
1503 )
1504 }
1505 }
1506
1507 let entitiesjson = json!(
1508 [
1509 {
1510 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
1511 "attrs": {
1512 "isFullTime": true,
1513 "department": "Sales",
1514 "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
1515 },
1516 "parents": []
1517 }
1518 ]
1519 );
1520 let eparser = EntityJsonParser::new(
1521 Some(&MockSchema),
1522 Extensions::all_available(),
1523 TCComputation::ComputeNow,
1524 );
1525 let parsed = eparser
1526 .from_json_value(entitiesjson)
1527 .expect("Should parse without error");
1528 assert_eq!(parsed.iter().count(), 1);
1529 let parsed = parsed
1530 .entity(&r#"XYZCorp::Employee::"12UA45""#.parse().unwrap())
1531 .expect("that should be the employee type and id");
1532 let is_full_time = parsed
1533 .get("isFullTime")
1534 .expect("isFullTime attr should exist");
1535 assert_eq!(
1536 RestrictedExprShapeOnly::new(is_full_time.as_borrowed()),
1537 RestrictedExprShapeOnly::new(RestrictedExpr::val(true).as_borrowed())
1538 );
1539 let department = parsed
1540 .get("department")
1541 .expect("department attr should exist");
1542 assert_eq!(
1543 RestrictedExprShapeOnly::new(department.as_borrowed()),
1544 RestrictedExprShapeOnly::new(RestrictedExpr::val("Sales").as_borrowed())
1545 );
1546 let manager = parsed.get("manager").expect("manager attr should exist");
1547 assert_eq!(
1548 RestrictedExprShapeOnly::new(manager.as_borrowed()),
1549 RestrictedExprShapeOnly::new(
1550 RestrictedExpr::val(
1551 "XYZCorp::Employee::\"34FB87\""
1552 .parse::<EntityUID>()
1553 .expect("valid")
1554 )
1555 .as_borrowed()
1556 )
1557 );
1558
1559 let entitiesjson = json!(
1560 [
1561 {
1562 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
1563 "attrs": {
1564 "isFullTime": true,
1565 "department": "Sales",
1566 "manager": { "type": "Employee", "id": "34FB87" }
1567 },
1568 "parents": []
1569 }
1570 ]
1571 );
1572
1573 let err = eparser
1574 .from_json_value(entitiesjson)
1575 .expect_err("should fail due to manager being wrong entity type (missing namespace)");
1576 assert!(
1577 err.to_string().contains(r#"In attribute "manager" on XYZCorp::Employee::"12UA45", type mismatch: attribute was expected to have type (entity of type XYZCorp::Employee), but actually has type (entity of type Employee)"#),
1578 "actual error message was {}",
1579 err
1580 );
1581 }
1582}