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