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