cedar_policy_core/
entities.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! This module contains the `Entities` type and related functionality.
18
19use 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/// Represents an entity hierarchy, and allows looking up `Entity` objects by
32/// UID.
33//
34/// Note that `Entities` is `Serialize` and `Deserialize`, but currently this is
35/// only used for the Dafny-FFI layer in DRT. All others use (and should use) the
36/// `from_json_*()` and `write_to_json()` methods as necessary.
37#[serde_as]
38#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
39pub struct Entities {
40    /// Serde cannot serialize a HashMap to JSON when the key to the map cannot
41    /// be serialized to a JSON string. This is a limitation of the JSON format.
42    /// `serde_as` annotation are used to serialize the data as associative
43    /// lists instead.
44    ///
45    /// Important internal invariant: for any `Entities` object that exists, the
46    /// the `ancestor` relation is transitively closed.
47    #[serde_as(as = "Vec<(_, _)>")]
48    entities: HashMap<EntityUID, Entity>,
49
50    /// The mode flag determines whether this store functions as a partial store or
51    /// as a fully concrete store.
52    /// Mode::Concrete means that the store is fully concrete, and failed dereferences are an error.
53    /// Mode::Partial means the store is partial, and failed dereferences result in a residual.
54    #[serde(default)]
55    #[serde(skip_deserializing)]
56    #[serde(skip_serializing)]
57    mode: Mode,
58}
59
60impl Entities {
61    /// Create a fresh `Entities` with no entities
62    pub fn new() -> Self {
63        Self {
64            entities: HashMap::new(),
65            mode: Mode::default(),
66        }
67    }
68
69    /// Transform the store into a partial store, where
70    /// attempting to dereference a non-existent EntityUID results in
71    /// a residual instead of an error.
72    pub fn partial(self) -> Self {
73        Self {
74            entities: self.entities,
75            mode: Mode::Partial,
76        }
77    }
78
79    /// Get the `Entity` with the given UID, if any
80    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    /// Iterate over the `Entity`s in the `Entities`
91    pub fn iter(&self) -> impl Iterator<Item = &Entity> {
92        self.entities.values()
93    }
94
95    /// Create an `Entities` object with the given entities.
96    ///
97    /// If you pass `TCComputation::AssumeAlreadyComputed`, then the caller is
98    /// responsible for ensuring that TC and DAG hold before calling this method.
99    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    /// Convert an `Entities` object into a JSON value suitable for parsing in
120    /// via `EntityJsonParser`.
121    ///
122    /// The returned JSON value will be parse-able even with no `Schema`.
123    ///
124    /// To parse an `Entities` object from a JSON value, use `EntityJsonParser`.
125    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    /// Dump an `Entities` object into an entities JSON file.
133    ///
134    /// The resulting JSON will be suitable for parsing in via
135    /// `EntityJsonParser`, and will be parse-able even with no `Schema`.
136    ///
137    /// To read an `Entities` object from an entities JSON file, use
138    /// `EntityJsonParser`.
139    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    /// Internal helper function to convert this `Entities` into a `Vec<EntityJSON>`
146    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/// Results from dereferencing values from the Entity Store
169#[derive(Debug, Clone)]
170pub enum Dereference<'a, T> {
171    /// No entity with the dereferenced EntityUID exists. This is an error.
172    NoSuchEntity,
173    /// The entity store has returned a residual
174    Residual(Expr),
175    /// The entity store has returned the requested data.
176    Data(&'a T),
177}
178
179impl<'a, T> Dereference<'a, T>
180where
181    T: std::fmt::Debug,
182{
183    /// Returns the contained `Data` value, consuming the `self` value.
184    ///
185    /// Because this function may panic, its use is generally discouraged.
186    /// Instead, prefer to use pattern matching and handle the `NoSuchEntity`
187    /// and `Residual` cases explicitly.
188    ///
189    /// # Panics
190    ///
191    /// Panics if the self value is not `Data`.
192    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    /// Returns the contained `Data` value, consuming the `self` value.
200    ///
201    /// Because this function may panic, its use is generally discouraged.
202    /// Instead, prefer to use pattern matching and handle the `NoSuchEntity`
203    /// and `Residual` cases explicitly.
204    ///
205    /// # Panics
206    ///
207    /// Panics if the self value is not `Data`.
208    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/// Describes the option for how the TC (transitive closure) of the entity
229/// hierarchy is computed
230#[allow(dead_code)] // only `ComputeNow` is used currently, that's intentional
231#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
232pub enum TCComputation {
233    /// Assume that the TC has already been computed and that the input is a DAG before the call of
234    /// `Entities::from_entities`.
235    AssumeAlreadyComputed,
236    /// Enforce that the TC must have already been computed before the call of
237    /// `Entities::from_entities`. If the given entities don't include all
238    /// transitive hierarchy relations, return an error. Also checks for cycles and returns an error if found.
239    EnforceAlreadyComputed,
240    /// Compute the TC ourselves during the call of `Entities::from_entities`.
241    /// This doesn't make any assumptions about the input, which can in fact
242    /// contain just parent edges and not transitive ancestor edges. Also checks for cycles and returns an error if found.
243    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        // Alice -> Jane -> Bob
254        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        // Double check transitive closure computation
287        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        // Alice -> Jane -> Bob
297        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        // Double check transitive closure computation
327        assert!(alice.is_descendant_of(&EntityUID::with_eid("bob")));
328    }
329
330    /// helper function which tests whether attribute values are shape-equal
331    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    /// this one uses `__expr`, `__entity`, and `__extn` escapes, in various positions
342    #[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        // this one tests the implicit and explicit forms of `__expr` and `__entity` escapes
417        // for the `uid` and `parents` fields
418        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        // check that all five entities exist
458        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        // and check the parent relations
465        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        // various JSON constructs that are invalid in `uid` and `parents` fields
479        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    /// helper function to round-trip an Entities (with no schema-based parsing)
591    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    /// helper function
600    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 that we can take an Entities, write it to JSON, parse that JSON
610    /// back in, and we have exactly the same Entities
611    #[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                // record literal that happens to look like an escape
690                "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    /// helper function
730    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        // Hierarchy
756        // a -> b -> c
757        // This isn't transitively closed, so it should fail
758        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        // Hierarchy
775        // a -> b -> c
776        // a -> c
777        // This is transitively closed, so it should succeed
778        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    /// Simple test that exercises a variety of attribute types.
798    #[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        // without schema-based parsing, `home_ip` and `trust_score` are
912        // strings, `manager` and `work_ip` are Records, `hr_contacts` contains
913        // Records, and `json_blob.inner3.innerinner` is a Record
914        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        // but with schema-based parsing, we get these other types
972        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        // simple type mismatch with expected type
1074        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        // another simple type mismatch with expected type
1111        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        // type mismatch where we expect a set and get just a single element
1149        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        // type mismatch where we just get the wrong entity type
1183        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        // type mismatch where we're expecting an extension type and get a
1220        // different extension type
1221        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        // missing a record attribute entirely
1258        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        // record attribute has the wrong type
1294        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        // unexpected record attribute
1362        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        // entity is missing a required attribute
1400        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        // unexpected entity attribute
1436        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 that involves namespaced entity types
1477    #[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}