cedar_policy_core/
entities.rs

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