cedar_policy_core/ast/
entity.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
17use crate::ast::*;
18use crate::entities::{err::EntitiesError, json::err::JsonSerializationError, EntityJson};
19use crate::evaluator::{EvaluationError, RestrictedEvaluator};
20use crate::extensions::Extensions;
21use crate::parser::err::ParseErrors;
22use crate::parser::Loc;
23use crate::transitive_closure::TCNode;
24use crate::FromNormalizedStr;
25use itertools::Itertools;
26use miette::Diagnostic;
27use ref_cast::RefCast;
28use serde::{Deserialize, Serialize};
29use serde_with::{serde_as, TryFromInto};
30use smol_str::SmolStr;
31use std::collections::{BTreeMap, HashMap, HashSet};
32use std::str::FromStr;
33use std::sync::Arc;
34use thiserror::Error;
35
36/// The entity type that Actions must have
37pub static ACTION_ENTITY_TYPE: &str = "Action";
38
39/// Entity type names are just [`Name`]s, but we have some operations on them specific to entity types.
40#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord, RefCast)]
41#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
42#[serde(transparent)]
43#[repr(transparent)]
44pub struct EntityType(Name);
45
46impl EntityType {
47    /// Is this an Action entity type?
48    /// Returns true when an entity type is an action entity type. This compares the
49    /// base name for the type, so this will return true for any entity type named
50    /// `Action` regardless of namespaces.
51    pub fn is_action(&self) -> bool {
52        self.0.as_ref().basename() == &Id::new_unchecked(ACTION_ENTITY_TYPE)
53    }
54
55    /// The name of this entity type
56    pub fn name(&self) -> &Name {
57        &self.0
58    }
59
60    /// The source location of this entity type
61    pub fn loc(&self) -> Option<&Loc> {
62        self.0.as_ref().loc()
63    }
64
65    /// Calls [`Name::qualify_with_name`] on the underlying [`Name`]
66    pub fn qualify_with(&self, namespace: Option<&Name>) -> Self {
67        Self(self.0.qualify_with_name(namespace))
68    }
69
70    /// Wraps [`Name::from_normalized_str`]
71    pub fn from_normalized_str(src: &str) -> Result<Self, ParseErrors> {
72        Name::from_normalized_str(src).map(Into::into)
73    }
74}
75
76impl From<Name> for EntityType {
77    fn from(n: Name) -> Self {
78        Self(n)
79    }
80}
81
82impl From<EntityType> for Name {
83    fn from(ty: EntityType) -> Name {
84        ty.0
85    }
86}
87
88impl AsRef<Name> for EntityType {
89    fn as_ref(&self) -> &Name {
90        &self.0
91    }
92}
93
94impl FromStr for EntityType {
95    type Err = ParseErrors;
96
97    fn from_str(s: &str) -> Result<Self, Self::Err> {
98        s.parse().map(Self)
99    }
100}
101
102impl std::fmt::Display for EntityType {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}", self.0)
105    }
106}
107
108/// Unique ID for an entity. These represent entities in the AST.
109#[derive(Serialize, Deserialize, Debug, Clone)]
110pub struct EntityUID {
111    /// Typename of the entity
112    ty: EntityType,
113    /// EID of the entity
114    eid: Eid,
115    /// Location of the entity in policy source
116    #[serde(skip)]
117    loc: Option<Loc>,
118}
119
120/// `PartialEq` implementation ignores the `loc`.
121impl PartialEq for EntityUID {
122    fn eq(&self, other: &Self) -> bool {
123        self.ty == other.ty && self.eid == other.eid
124    }
125}
126impl Eq for EntityUID {}
127
128impl std::hash::Hash for EntityUID {
129    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
130        // hash the ty and eid, in line with the `PartialEq` impl which compares
131        // the ty and eid.
132        self.ty.hash(state);
133        self.eid.hash(state);
134    }
135}
136
137impl PartialOrd for EntityUID {
138    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
139        Some(self.cmp(other))
140    }
141}
142impl Ord for EntityUID {
143    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
144        self.ty.cmp(&other.ty).then(self.eid.cmp(&other.eid))
145    }
146}
147
148impl StaticallyTyped for EntityUID {
149    fn type_of(&self) -> Type {
150        Type::Entity {
151            ty: self.ty.clone(),
152        }
153    }
154}
155
156impl EntityUID {
157    /// Create an `EntityUID` with the given string as its EID.
158    /// Useful for testing.
159    #[cfg(test)]
160    pub(crate) fn with_eid(eid: &str) -> Self {
161        Self {
162            ty: Self::test_entity_type(),
163            eid: Eid(eid.into()),
164            loc: None,
165        }
166    }
167    // by default, Coverlay does not track coverage for lines after a line
168    // containing #[cfg(test)].
169    // we use the following sentinel to "turn back on" coverage tracking for
170    // remaining lines of this file, until the next #[cfg(test)]
171    // GRCOV_BEGIN_COVERAGE
172
173    /// The type of entities created with the above `with_eid()`.
174    #[cfg(test)]
175    pub(crate) fn test_entity_type() -> EntityType {
176        let name = Name::parse_unqualified_name("test_entity_type")
177            .expect("test_entity_type should be a valid identifier");
178        EntityType(name)
179    }
180    // by default, Coverlay does not track coverage for lines after a line
181    // containing #[cfg(test)].
182    // we use the following sentinel to "turn back on" coverage tracking for
183    // remaining lines of this file, until the next #[cfg(test)]
184    // GRCOV_BEGIN_COVERAGE
185
186    /// Create an `EntityUID` with the given (unqualified) typename, and the given string as its EID.
187    pub fn with_eid_and_type(typename: &str, eid: &str) -> Result<Self, ParseErrors> {
188        Ok(Self {
189            ty: EntityType(Name::parse_unqualified_name(typename)?),
190            eid: Eid(eid.into()),
191            loc: None,
192        })
193    }
194
195    /// Split into the `EntityType` representing the entity type, and the `Eid`
196    /// representing its name
197    pub fn components(self) -> (EntityType, Eid) {
198        (self.ty, self.eid)
199    }
200
201    /// Get the source location for this `EntityUID`.
202    pub fn loc(&self) -> Option<&Loc> {
203        self.loc.as_ref()
204    }
205
206    /// Create an [`EntityUID`] with the given typename and [`Eid`]
207    pub fn from_components(ty: EntityType, eid: Eid, loc: Option<Loc>) -> Self {
208        Self { ty, eid, loc }
209    }
210
211    /// Get the type component.
212    pub fn entity_type(&self) -> &EntityType {
213        &self.ty
214    }
215
216    /// Get the Eid component.
217    pub fn eid(&self) -> &Eid {
218        &self.eid
219    }
220
221    /// Does this EntityUID refer to an action entity?
222    pub fn is_action(&self) -> bool {
223        self.entity_type().is_action()
224    }
225}
226
227impl std::fmt::Display for EntityUID {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        write!(f, "{}::\"{}\"", self.entity_type(), self.eid.escaped())
230    }
231}
232
233// allow `.parse()` on a string to make an `EntityUID`
234impl std::str::FromStr for EntityUID {
235    type Err = ParseErrors;
236
237    fn from_str(s: &str) -> Result<Self, Self::Err> {
238        crate::parser::parse_euid(s)
239    }
240}
241
242impl FromNormalizedStr for EntityUID {
243    fn describe_self() -> &'static str {
244        "Entity UID"
245    }
246}
247
248#[cfg(feature = "arbitrary")]
249impl<'a> arbitrary::Arbitrary<'a> for EntityUID {
250    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
251        Ok(Self {
252            ty: u.arbitrary()?,
253            eid: u.arbitrary()?,
254            loc: None,
255        })
256    }
257}
258
259/// The `Eid` type represents the id of an `Entity`, without the typename.
260/// Together with the typename it comprises an `EntityUID`.
261/// For example, in `User::"alice"`, the `Eid` is `alice`.
262///
263/// `Eid` does not implement `Display`, partly because it is unclear whether
264/// `Display` should produce an escaped representation or an unescaped representation
265/// (see [#884](https://github.com/cedar-policy/cedar/issues/884)).
266/// To get an escaped representation, use `.escaped()`.
267/// To get an unescaped representation, use `.as_ref()`.
268#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)]
269pub struct Eid(SmolStr);
270
271impl Eid {
272    /// Construct an Eid
273    pub fn new(eid: impl Into<SmolStr>) -> Self {
274        Eid(eid.into())
275    }
276
277    /// Get the contents of the `Eid` as an escaped string
278    pub fn escaped(&self) -> SmolStr {
279        self.0.escape_debug().collect()
280    }
281}
282
283impl AsRef<SmolStr> for Eid {
284    fn as_ref(&self) -> &SmolStr {
285        &self.0
286    }
287}
288
289impl AsRef<str> for Eid {
290    fn as_ref(&self) -> &str {
291        &self.0
292    }
293}
294
295#[cfg(feature = "arbitrary")]
296impl<'a> arbitrary::Arbitrary<'a> for Eid {
297    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
298        let x: String = u.arbitrary()?;
299        Ok(Self(x.into()))
300    }
301}
302
303/// Entity datatype
304#[derive(Debug, Clone, Serialize)]
305pub struct Entity {
306    /// UID
307    uid: EntityUID,
308
309    /// Internal BTreMap of attributes.
310    /// We use a btreemap so that the keys have a deterministic order.
311    ///
312    /// In the serialized form of `Entity`, attribute values appear as
313    /// `RestrictedExpr`s, for mostly historical reasons.
314    attrs: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
315
316    /// Set of ancestors of this `Entity` (i.e., all direct and transitive
317    /// parents), as UIDs
318    ancestors: HashSet<EntityUID>,
319
320    /// Tags on this entity (RFC 82)
321    ///
322    /// Like for `attrs`, we use a `BTreeMap` so that the tags have a
323    /// deterministic order.
324    /// And like in `attrs`, the values in `tags` appear as `RestrictedExpr` in
325    /// the serialized form of `Entity`.
326    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
327    tags: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
328}
329
330impl std::hash::Hash for Entity {
331    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
332        self.uid.hash(state);
333    }
334}
335
336impl Entity {
337    /// Create a new `Entity` with this UID, attributes, ancestors, and tags
338    ///
339    /// # Errors
340    /// - Will error if any of the [`RestrictedExpr]`s in `attrs` or `tags` error when evaluated
341    pub fn new(
342        uid: EntityUID,
343        attrs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
344        ancestors: HashSet<EntityUID>,
345        tags: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
346        extensions: &Extensions<'_>,
347    ) -> Result<Self, EntityAttrEvaluationError> {
348        let evaluator = RestrictedEvaluator::new(extensions);
349        let evaluate_kvs = |(k, v): (SmolStr, RestrictedExpr), was_attr: bool| {
350            let attr_val = evaluator
351                .partial_interpret(v.as_borrowed())
352                .map_err(|err| EntityAttrEvaluationError {
353                    uid: uid.clone(),
354                    attr_or_tag: k.clone(),
355                    was_attr,
356                    err,
357                })?;
358            Ok((k, attr_val.into()))
359        };
360        let evaluated_attrs = attrs
361            .into_iter()
362            .map(|kv| evaluate_kvs(kv, true))
363            .collect::<Result<_, EntityAttrEvaluationError>>()?;
364        let evaluated_tags = tags
365            .into_iter()
366            .map(|kv| evaluate_kvs(kv, false))
367            .collect::<Result<_, EntityAttrEvaluationError>>()?;
368        Ok(Entity {
369            uid,
370            attrs: evaluated_attrs,
371            ancestors,
372            tags: evaluated_tags,
373        })
374    }
375
376    /// Create a new `Entity` with this UID, attributes, and ancestors (and no tags)
377    ///
378    /// Unlike in `Entity::new()`, in this constructor, attributes are expressed
379    /// as `PartialValue`.
380    ///
381    /// Callers should consider directly using [`Entity::new_with_attr_partial_value_serialized_as_expr`]
382    /// if they would call this method by first building a map, as it will
383    /// deconstruct and re-build the map perhaps unnecessarily.
384    pub fn new_with_attr_partial_value(
385        uid: EntityUID,
386        attrs: impl IntoIterator<Item = (SmolStr, PartialValue)>,
387        ancestors: HashSet<EntityUID>,
388    ) -> Self {
389        Self::new_with_attr_partial_value_serialized_as_expr(
390            uid,
391            attrs.into_iter().map(|(k, v)| (k, v.into())).collect(),
392            ancestors,
393        )
394    }
395
396    /// Create a new `Entity` with this UID, attributes, and ancestors (and no tags)
397    ///
398    /// Unlike in `Entity::new()`, in this constructor, attributes are expressed
399    /// as `PartialValueSerializedAsExpr`.
400    pub fn new_with_attr_partial_value_serialized_as_expr(
401        uid: EntityUID,
402        attrs: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
403        ancestors: HashSet<EntityUID>,
404    ) -> Self {
405        Entity {
406            uid,
407            attrs,
408            ancestors,
409            tags: BTreeMap::new(),
410        }
411    }
412
413    /// Get the UID of this entity
414    pub fn uid(&self) -> &EntityUID {
415        &self.uid
416    }
417
418    /// Get the value for the given attribute, or `None` if not present
419    pub fn get(&self, attr: &str) -> Option<&PartialValue> {
420        self.attrs.get(attr).map(|v| v.as_ref())
421    }
422
423    /// Get the value for the given tag, or `None` if not present
424    pub fn get_tag(&self, tag: &str) -> Option<&PartialValue> {
425        self.tags.get(tag).map(|v| v.as_ref())
426    }
427
428    /// Is this `Entity` a descendant of `e` in the entity hierarchy?
429    pub fn is_descendant_of(&self, e: &EntityUID) -> bool {
430        self.ancestors.contains(e)
431    }
432
433    /// Iterate over this entity's ancestors
434    pub fn ancestors(&self) -> impl Iterator<Item = &EntityUID> {
435        self.ancestors.iter()
436    }
437
438    /// Get the number of attributes on this entity
439    pub fn attrs_len(&self) -> usize {
440        self.attrs.len()
441    }
442
443    /// Get the number of tags on this entity
444    pub fn tags_len(&self) -> usize {
445        self.tags.len()
446    }
447
448    /// Iterate over this entity's attribute names
449    pub fn keys(&self) -> impl Iterator<Item = &SmolStr> {
450        self.attrs.keys()
451    }
452
453    /// Iterate over this entity's tag names
454    pub fn tag_keys(&self) -> impl Iterator<Item = &SmolStr> {
455        self.tags.keys()
456    }
457
458    /// Iterate over this entity's attributes
459    pub fn attrs(&self) -> impl Iterator<Item = (&SmolStr, &PartialValue)> {
460        self.attrs.iter().map(|(k, v)| (k, v.as_ref()))
461    }
462
463    /// Iterate over this entity's tags
464    pub fn tags(&self) -> impl Iterator<Item = (&SmolStr, &PartialValue)> {
465        self.tags.iter().map(|(k, v)| (k, v.as_ref()))
466    }
467
468    /// Create an `Entity` with the given UID, no attributes, no parents, and no tags.
469    pub fn with_uid(uid: EntityUID) -> Self {
470        Self {
471            uid,
472            attrs: BTreeMap::new(),
473            ancestors: HashSet::new(),
474            tags: BTreeMap::new(),
475        }
476    }
477
478    /// Test if two `Entity` objects are deep/structurally equal.
479    /// That is, not only do they have the same UID, but also the same
480    /// attributes, attribute values, and ancestors.
481    pub(crate) fn deep_eq(&self, other: &Self) -> bool {
482        self.uid == other.uid && self.attrs == other.attrs && self.ancestors == other.ancestors
483    }
484
485    /// Set the UID to the given value.
486    // Only used for convenience in some tests
487    #[cfg(test)]
488    pub fn set_uid(&mut self, uid: EntityUID) {
489        self.uid = uid;
490    }
491
492    /// Set the given attribute to the given value.
493    // Only used for convenience in some tests and when fuzzing
494    #[cfg(any(test, fuzzing))]
495    pub fn set_attr(
496        &mut self,
497        attr: SmolStr,
498        val: RestrictedExpr,
499        extensions: &Extensions<'_>,
500    ) -> Result<(), EvaluationError> {
501        let val = RestrictedEvaluator::new(extensions).partial_interpret(val.as_borrowed())?;
502        self.attrs.insert(attr, val.into());
503        Ok(())
504    }
505
506    /// Set the given tag to the given value.
507    // Only used for convenience in some tests and when fuzzing
508    #[cfg(any(test, fuzzing))]
509    pub fn set_tag(
510        &mut self,
511        tag: SmolStr,
512        val: RestrictedExpr,
513        extensions: &Extensions<'_>,
514    ) -> Result<(), EvaluationError> {
515        let val = RestrictedEvaluator::new(extensions).partial_interpret(val.as_borrowed())?;
516        self.tags.insert(tag, val.into());
517        Ok(())
518    }
519
520    /// Mark the given `UID` as an ancestor of this `Entity`.
521    // When fuzzing, `add_ancestor()` is fully `pub`.
522    #[cfg(not(fuzzing))]
523    pub(crate) fn add_ancestor(&mut self, uid: EntityUID) {
524        self.ancestors.insert(uid);
525    }
526    /// Mark the given `UID` as an ancestor of this `Entity`
527    #[cfg(fuzzing)]
528    pub fn add_ancestor(&mut self, uid: EntityUID) {
529        self.ancestors.insert(uid);
530    }
531
532    /// Consume the entity and return the entity's owned Uid, attributes, parents, and tags.
533    pub fn into_inner(
534        self,
535    ) -> (
536        EntityUID,
537        HashMap<SmolStr, PartialValue>,
538        HashSet<EntityUID>,
539        HashMap<SmolStr, PartialValue>,
540    ) {
541        let Self {
542            uid,
543            attrs,
544            ancestors,
545            tags,
546        } = self;
547        (
548            uid,
549            attrs.into_iter().map(|(k, v)| (k, v.0)).collect(),
550            ancestors,
551            tags.into_iter().map(|(k, v)| (k, v.0)).collect(),
552        )
553    }
554
555    /// Write the entity to a json document
556    pub fn write_to_json(&self, f: impl std::io::Write) -> Result<(), EntitiesError> {
557        let ejson = EntityJson::from_entity(self)?;
558        serde_json::to_writer_pretty(f, &ejson).map_err(JsonSerializationError::from)?;
559        Ok(())
560    }
561
562    /// write the entity to a json value
563    pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
564        let ejson = EntityJson::from_entity(self)?;
565        let v = serde_json::to_value(ejson).map_err(JsonSerializationError::from)?;
566        Ok(v)
567    }
568
569    /// write the entity to a json string
570    pub fn to_json_string(&self) -> Result<String, EntitiesError> {
571        let ejson = EntityJson::from_entity(self)?;
572        let string = serde_json::to_string(&ejson).map_err(JsonSerializationError::from)?;
573        Ok(string)
574    }
575}
576
577impl PartialEq for Entity {
578    fn eq(&self, other: &Self) -> bool {
579        self.uid() == other.uid()
580    }
581}
582
583impl Eq for Entity {}
584
585impl StaticallyTyped for Entity {
586    fn type_of(&self) -> Type {
587        self.uid.type_of()
588    }
589}
590
591impl TCNode<EntityUID> for Entity {
592    fn get_key(&self) -> EntityUID {
593        self.uid().clone()
594    }
595
596    fn add_edge_to(&mut self, k: EntityUID) {
597        self.add_ancestor(k)
598    }
599
600    fn out_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
601        Box::new(self.ancestors())
602    }
603
604    fn has_edge_to(&self, e: &EntityUID) -> bool {
605        self.is_descendant_of(e)
606    }
607}
608
609impl TCNode<EntityUID> for Arc<Entity> {
610    fn get_key(&self) -> EntityUID {
611        self.uid().clone()
612    }
613
614    fn add_edge_to(&mut self, k: EntityUID) {
615        // Use Arc::make_mut to get a mutable reference to the inner value
616        Arc::make_mut(self).add_ancestor(k)
617    }
618
619    fn out_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
620        Box::new(self.ancestors())
621    }
622
623    fn has_edge_to(&self, e: &EntityUID) -> bool {
624        self.is_descendant_of(e)
625    }
626}
627
628impl std::fmt::Display for Entity {
629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630        write!(
631            f,
632            "{}:\n  attrs:{}\n  ancestors:{}",
633            self.uid,
634            self.attrs
635                .iter()
636                .map(|(k, v)| format!("{}: {}", k, v))
637                .join("; "),
638            self.ancestors.iter().join(", ")
639        )
640    }
641}
642
643/// `PartialValue`, but serialized as a `RestrictedExpr`.
644///
645/// (Extension values can't be directly serialized, but can be serialized as
646/// `RestrictedExpr`)
647#[serde_as]
648#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
649pub struct PartialValueSerializedAsExpr(
650    #[serde_as(as = "TryFromInto<RestrictedExpr>")] PartialValue,
651);
652
653impl AsRef<PartialValue> for PartialValueSerializedAsExpr {
654    fn as_ref(&self) -> &PartialValue {
655        &self.0
656    }
657}
658
659impl std::ops::Deref for PartialValueSerializedAsExpr {
660    type Target = PartialValue;
661    fn deref(&self) -> &Self::Target {
662        &self.0
663    }
664}
665
666impl From<PartialValue> for PartialValueSerializedAsExpr {
667    fn from(value: PartialValue) -> PartialValueSerializedAsExpr {
668        PartialValueSerializedAsExpr(value)
669    }
670}
671
672impl From<PartialValueSerializedAsExpr> for PartialValue {
673    fn from(value: PartialValueSerializedAsExpr) -> PartialValue {
674        value.0
675    }
676}
677
678impl std::fmt::Display for PartialValueSerializedAsExpr {
679    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680        write!(f, "{}", self.0)
681    }
682}
683
684/// Error type for evaluation errors when evaluating an entity attribute or tag.
685/// Contains some extra contextual information and the underlying
686/// `EvaluationError`.
687//
688// This is NOT a publicly exported error type.
689#[derive(Debug, Diagnostic, Error)]
690#[error("failed to evaluate {} `{attr_or_tag}` of `{uid}`: {err}", if *.was_attr { "attribute" } else { "tag" })]
691pub struct EntityAttrEvaluationError {
692    /// UID of the entity where the error was encountered
693    pub uid: EntityUID,
694    /// Attribute or tag of the entity where the error was encountered
695    pub attr_or_tag: SmolStr,
696    /// If `attr_or_tag` was an attribute (`true`) or tag (`false`)
697    pub was_attr: bool,
698    /// Underlying evaluation error
699    #[diagnostic(transparent)]
700    pub err: EvaluationError,
701}
702
703#[cfg(test)]
704mod test {
705    use std::str::FromStr;
706
707    use super::*;
708
709    #[test]
710    fn display() {
711        let e = EntityUID::with_eid("eid");
712        assert_eq!(format!("{e}"), "test_entity_type::\"eid\"");
713    }
714
715    #[test]
716    fn test_euid_equality() {
717        let e1 = EntityUID::with_eid("foo");
718        let e2 = EntityUID::from_components(
719            Name::parse_unqualified_name("test_entity_type")
720                .expect("should be a valid identifier")
721                .into(),
722            Eid("foo".into()),
723            None,
724        );
725        let e3 = EntityUID::from_components(
726            Name::parse_unqualified_name("Unspecified")
727                .expect("should be a valid identifier")
728                .into(),
729            Eid("foo".into()),
730            None,
731        );
732
733        // an EUID is equal to itself
734        assert_eq!(e1, e1);
735        assert_eq!(e2, e2);
736
737        // constructing with `with_euid` or `from_components` is the same
738        assert_eq!(e1, e2);
739
740        // other pairs are not equal
741        assert!(e1 != e3);
742    }
743
744    #[test]
745    fn action_checker() {
746        let euid = EntityUID::from_str("Action::\"view\"").unwrap();
747        assert!(euid.is_action());
748        let euid = EntityUID::from_str("Foo::Action::\"view\"").unwrap();
749        assert!(euid.is_action());
750        let euid = EntityUID::from_str("Foo::\"view\"").unwrap();
751        assert!(!euid.is_action());
752        let euid = EntityUID::from_str("Action::Foo::\"view\"").unwrap();
753        assert!(!euid.is_action());
754    }
755
756    #[test]
757    fn action_type_is_valid_id() {
758        assert!(Id::from_normalized_str(ACTION_ENTITY_TYPE).is_ok());
759    }
760}