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