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