cedar_policy_core/ast/
name.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 super::{id::Id, PrincipalOrResource, UnreservedId};
18use educe::Educe;
19use itertools::Itertools;
20use miette::Diagnostic;
21use ref_cast::RefCast;
22use regex::Regex;
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24use smol_str::ToSmolStr;
25use std::collections::HashSet;
26use std::fmt::Display;
27use std::str::FromStr;
28use std::sync::Arc;
29use thiserror::Error;
30
31use crate::parser::err::{ParseError, ParseErrors, ToASTError, ToASTErrorKind};
32use crate::parser::Loc;
33use crate::FromNormalizedStr;
34
35/// Represents the name of an entity type, function, etc.
36/// The name may include namespaces.
37/// Clone is O(1).
38///
39/// This type may contain any name valid for use internally, including names
40/// with reserved `__cedar` components (and also names without `__cedar`).
41#[derive(Educe, Debug, Clone)]
42#[educe(PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub struct InternalName {
44    /// Basename
45    pub(crate) id: Id,
46    /// Namespaces
47    pub(crate) path: Arc<Vec<Id>>,
48    /// Location of the name in source
49    #[educe(PartialEq(ignore))]
50    #[educe(Hash(ignore))]
51    #[educe(PartialOrd(ignore))]
52    pub(crate) loc: Option<Loc>,
53}
54
55/// A shortcut for [`InternalName::unqualified_name`]
56impl From<Id> for InternalName {
57    fn from(value: Id) -> Self {
58        Self::unqualified_name(value, None)
59    }
60}
61
62/// Convert a [`InternalName`] to an [`Id`]
63/// The error type is the unit type because the reason the conversion fails
64/// is obvious
65impl TryFrom<InternalName> for Id {
66    type Error = ();
67    fn try_from(value: InternalName) -> Result<Self, Self::Error> {
68        if value.is_unqualified() {
69            Ok(value.id)
70        } else {
71            Err(())
72        }
73    }
74}
75
76impl InternalName {
77    /// A full constructor for [`InternalName`]
78    pub fn new(basename: Id, path: impl IntoIterator<Item = Id>, loc: Option<Loc>) -> Self {
79        Self {
80            id: basename,
81            path: Arc::new(path.into_iter().collect()),
82            loc,
83        }
84    }
85
86    /// Create an [`InternalName`] with no path (no namespaces).
87    pub fn unqualified_name(id: Id, loc: Option<Loc>) -> Self {
88        Self {
89            id,
90            path: Arc::new(vec![]),
91            loc,
92        }
93    }
94
95    /// Get the [`InternalName`] representing the reserved `__cedar` namespace
96    pub fn __cedar() -> Self {
97        // using `Id::new_unchecked()` for performance reasons -- this function is called many times by validator code
98        Self::unqualified_name(Id::new_unchecked("__cedar"), None)
99    }
100
101    /// Create an [`InternalName`] with no path (no namespaces).
102    /// Returns an error if `s` is not a valid identifier.
103    pub fn parse_unqualified_name(s: &str) -> Result<Self, ParseErrors> {
104        Ok(Self {
105            id: s.parse()?,
106            path: Arc::new(vec![]),
107            loc: None,
108        })
109    }
110
111    /// Given a type basename and a namespace (as an [`InternalName`] itself),
112    /// return an [`InternalName`] representing the type's fully qualified name
113    pub fn type_in_namespace(
114        basename: Id,
115        namespace: InternalName,
116        loc: Option<Loc>,
117    ) -> InternalName {
118        let mut path = Arc::unwrap_or_clone(namespace.path);
119        path.push(namespace.id);
120        InternalName::new(basename, path, loc)
121    }
122
123    /// Get the source location
124    pub fn loc(&self) -> Option<&Loc> {
125        self.loc.as_ref()
126    }
127
128    /// Get the basename of the [`InternalName`] (ie, with namespaces stripped).
129    pub fn basename(&self) -> &Id {
130        &self.id
131    }
132
133    /// Get the namespace of the [`InternalName`], as components
134    pub fn namespace_components(&self) -> impl Iterator<Item = &Id> {
135        self.path.iter()
136    }
137
138    /// Get the full namespace of the [`InternalName`], as a single string.
139    ///
140    /// Examples:
141    /// - `foo::bar` --> the namespace is `"foo"`
142    /// - `bar` --> the namespace is `""`
143    /// - `foo::bar::baz` --> the namespace is `"foo::bar"`
144    pub fn namespace(&self) -> String {
145        self.path.iter().join("::")
146    }
147
148    /// Qualify the name with a namespace
149    ///
150    /// If the name already has a non-empty namespace, this method does not
151    /// apply any prefix and instead returns a copy of `self`.
152    ///
153    /// If `namespace` is `None`, that represents the empty namespace, so no
154    /// prefixing will be done.
155    ///
156    /// If the name does not already have an explicit namespace (i.e., it's
157    /// just a single `Id`), this applies `namespace` as a prefix (if it is
158    /// present).
159    ///
160    /// Examples:
161    /// - `A::B`.qualify_with(None) is `A::B`
162    /// - `A::B`.qualify_with(Some(C)) is also `A::B`
163    /// - `A`.qualify_with(None) is `A`
164    /// - `A`.qualify_with(Some(C)) is `C::A`
165    /// - `A`.qualify_with(Some(B::C)) is `B::C::A`
166    pub fn qualify_with(&self, namespace: Option<&InternalName>) -> InternalName {
167        if self.is_unqualified() {
168            match namespace {
169                Some(namespace) => Self::new(
170                    self.basename().clone(),
171                    namespace
172                        .namespace_components()
173                        .chain(std::iter::once(namespace.basename()))
174                        .cloned(),
175                    self.loc.clone(),
176                ),
177                None => self.clone(),
178            }
179        } else {
180            self.clone()
181        }
182    }
183
184    /// Like `qualify_with()`, but accepts a [`Name`] as the namespace to qualify with
185    pub fn qualify_with_name(&self, namespace: Option<&Name>) -> InternalName {
186        let ns = namespace.map(AsRef::as_ref);
187        self.qualify_with(ns)
188    }
189
190    /// Test if an [`InternalName`] is an [`Id`]
191    pub fn is_unqualified(&self) -> bool {
192        self.path.is_empty()
193    }
194
195    /// Test if an [`InternalName`] is reserved
196    /// i.e., any of its components matches `__cedar`
197    pub fn is_reserved(&self) -> bool {
198        self.path
199            .iter()
200            .chain(std::iter::once(&self.id))
201            .any(|id| id.is_reserved())
202    }
203}
204
205impl std::fmt::Display for InternalName {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        for elem in self.path.as_ref() {
208            write!(f, "{elem}::")?;
209        }
210        write!(f, "{}", self.id)?;
211        Ok(())
212    }
213}
214
215/// Serialize an [`InternalName`] using its `Display` implementation
216/// This serialization implementation is used in the JSON schema format.
217impl Serialize for InternalName {
218    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
219    where
220        S: Serializer,
221    {
222        self.to_smolstr().serialize(serializer)
223    }
224}
225
226// allow `.parse()` on a string to make an [`InternalName`]
227impl std::str::FromStr for InternalName {
228    type Err = ParseErrors;
229
230    fn from_str(s: &str) -> Result<Self, Self::Err> {
231        crate::parser::parse_internal_name(s)
232    }
233}
234
235impl FromNormalizedStr for InternalName {
236    fn describe_self() -> &'static str {
237        "internal name"
238    }
239}
240
241#[cfg(feature = "arbitrary")]
242impl<'a> arbitrary::Arbitrary<'a> for InternalName {
243    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
244        let path_size = u.int_in_range(0..=8)?;
245        Ok(Self {
246            id: u.arbitrary()?,
247            path: Arc::new(
248                (0..path_size)
249                    .map(|_| u.arbitrary())
250                    .collect::<Result<Vec<Id>, _>>()?,
251            ),
252            loc: None,
253        })
254    }
255}
256
257struct NameVisitor;
258
259impl serde::de::Visitor<'_> for NameVisitor {
260    type Value = InternalName;
261
262    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        formatter.write_str("a name consisting of an optional namespace and id")
264    }
265
266    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
267    where
268        E: serde::de::Error,
269    {
270        InternalName::from_normalized_str(value)
271            .map_err(|err| serde::de::Error::custom(format!("invalid name `{value}`: {err}")))
272    }
273}
274
275/// Deserialize an [`InternalName`] using `from_normalized_str`.
276/// This deserialization implementation is used in the JSON schema format.
277impl<'de> Deserialize<'de> for InternalName {
278    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
279    where
280        D: Deserializer<'de>,
281    {
282        deserializer.deserialize_str(NameVisitor)
283    }
284}
285
286/// Identifier for a slot
287/// Clone is O(1).
288// This simply wraps a separate enum -- currently [`ValidSlotId`] -- in case we
289// want to generalize later
290#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
291#[serde(transparent)]
292pub struct SlotId(pub(crate) ValidSlotId);
293
294impl SlotId {
295    /// Get the slot for `principal`
296    pub fn principal() -> Self {
297        Self(ValidSlotId::Principal)
298    }
299
300    /// Get the slot for `resource`
301    pub fn resource() -> Self {
302        Self(ValidSlotId::Resource)
303    }
304
305    /// Check if a slot represents a principal
306    pub fn is_principal(&self) -> bool {
307        matches!(self, Self(ValidSlotId::Principal))
308    }
309
310    /// Check if a slot represents a resource
311    pub fn is_resource(&self) -> bool {
312        matches!(self, Self(ValidSlotId::Resource))
313    }
314}
315
316impl From<PrincipalOrResource> for SlotId {
317    fn from(v: PrincipalOrResource) -> Self {
318        match v {
319            PrincipalOrResource::Principal => SlotId::principal(),
320            PrincipalOrResource::Resource => SlotId::resource(),
321        }
322    }
323}
324
325impl std::fmt::Display for SlotId {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        write!(f, "{}", self.0)
328    }
329}
330
331/// Two possible variants for Slots
332#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
333pub(crate) enum ValidSlotId {
334    #[serde(rename = "?principal")]
335    Principal,
336    #[serde(rename = "?resource")]
337    Resource,
338}
339
340impl std::fmt::Display for ValidSlotId {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        let s = match self {
343            ValidSlotId::Principal => "principal",
344            ValidSlotId::Resource => "resource",
345        };
346        write!(f, "?{s}")
347    }
348}
349
350/// [`SlotId`] plus a source location
351#[derive(Educe, Debug, Clone)]
352#[educe(PartialEq, Eq, Hash)]
353pub struct Slot {
354    /// [`SlotId`]
355    pub id: SlotId,
356    /// Source location, if available
357    #[educe(PartialEq(ignore))]
358    #[educe(Hash(ignore))]
359    pub loc: Option<Loc>,
360}
361
362#[cfg(test)]
363mod vars_test {
364    use super::*;
365    // Make sure the vars always parse correctly
366    #[test]
367    fn vars_correct() {
368        SlotId::principal();
369        SlotId::resource();
370    }
371
372    #[test]
373    fn display() {
374        assert_eq!(format!("{}", SlotId::principal()), "?principal")
375    }
376}
377
378/// A new type which indicates that the contained [`InternalName`] does not
379/// contain reserved `__cedar`, as specified by RFC 52.
380/// This represents names which are legal for end-users to _define_, while
381/// [`InternalName`] represents names which are legal for end-users to
382/// _reference_.
383#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, RefCast)]
384#[repr(transparent)]
385#[serde(transparent)]
386pub struct Name(pub(crate) InternalName);
387
388impl From<UnreservedId> for Name {
389    fn from(value: UnreservedId) -> Self {
390        Self::unqualified_name(value)
391    }
392}
393
394impl TryFrom<Name> for UnreservedId {
395    type Error = ();
396    fn try_from(value: Name) -> Result<Self, Self::Error> {
397        if value.0.is_unqualified() {
398            Ok(value.basename())
399        } else {
400            Err(())
401        }
402    }
403}
404
405impl Display for Name {
406    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407        self.0.fmt(f)
408    }
409}
410
411impl FromStr for Name {
412    type Err = ParseErrors;
413    fn from_str(s: &str) -> Result<Self, Self::Err> {
414        let n: InternalName = s.parse()?;
415        n.try_into().map_err(ParseErrors::singleton)
416    }
417}
418
419// PANIC SAFETY: this is a valid Regex pattern
420#[allow(clippy::unwrap_used)]
421static VALID_ANY_IDENT_REGEX: std::sync::LazyLock<Regex> =
422    std::sync::LazyLock::new(|| Regex::new("^[_a-zA-Z][_a-zA-Z0-9]*$").unwrap());
423
424// PANIC SAFETY: this is a valid Regex pattern
425#[allow(clippy::unwrap_used)]
426static VALID_NAME_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
427    Regex::new("^[_a-zA-Z][_a-zA-Z0-9]*(?:::[_a-zA-Z][_a-zA-Z0-9]*)*$").unwrap()
428});
429
430// All of Cedar's reserved keywords for identifiers.
431// Notice this is only a subset of all reserved keywords.
432static RESERVED_IDS: std::sync::LazyLock<HashSet<&'static str>> = std::sync::LazyLock::new(|| {
433    vec![
434        "true", "false", "if", "then", "else", "in", "is", "like", "has",
435        // Can only be used in [`InternalName`]
436        "__cedar",
437    ]
438    .into_iter()
439    .collect()
440});
441
442/**
443 * Return true if the given string is a valid normalized identifier
444 * ("normalized" in the sense of `FromNoramlizedStr`, i.e., no leading/trailing
445 * whitespace) that does not require quoting when used as an attribute.
446 */
447pub fn is_normalized_ident(s: &str) -> bool {
448    VALID_ANY_IDENT_REGEX.is_match(s) && !RESERVED_IDS.contains(s)
449}
450
451impl FromNormalizedStr for Name {
452    fn from_normalized_str(s: &str) -> Result<Self, ParseErrors> {
453        if !VALID_NAME_REGEX.is_match(s) {
454            return Err(Self::parse_err_from_str(s));
455        }
456
457        let path_parts: Vec<&str> = s.split("::").collect();
458        if path_parts.iter().any(|s| RESERVED_IDS.contains(s)) {
459            return Err(Self::parse_err_from_str(s));
460        }
461
462        if let Some((last, prefix)) = path_parts.split_last() {
463            Ok(Self(InternalName::new(
464                Id::new_unchecked(*last),
465                prefix.iter().map(|part| Id::new_unchecked(*part)),
466                Some(Loc::new(0..(s.len()), s.into())),
467            )))
468        } else {
469            Err(Self::parse_err_from_str(s))
470        }
471    }
472
473    fn describe_self() -> &'static str {
474        "Name"
475    }
476}
477
478/// Deserialize a [`Name`] using `from_normalized_str`
479/// This deserialization implementation is used in the JSON schema format.
480impl<'de> Deserialize<'de> for Name {
481    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
482    where
483        D: Deserializer<'de>,
484    {
485        deserializer
486            .deserialize_str(NameVisitor)
487            .and_then(|n| n.try_into().map_err(serde::de::Error::custom))
488    }
489}
490
491impl Name {
492    /// Create a [`Name`] with no path (no namespaces).
493    /// Returns an error if `s` is not a valid identifier.
494    pub fn parse_unqualified_name(s: &str) -> Result<Self, ParseErrors> {
495        InternalName::parse_unqualified_name(s)
496            .and_then(|n| n.try_into().map_err(ParseErrors::singleton))
497    }
498
499    /// Create a [`Name`] with no path (no namespaces).
500    pub fn unqualified_name(id: UnreservedId) -> Self {
501        // This is safe (upholds the `Name` invariant) because `id` must be an `UnreservedId`
502        Self(InternalName::unqualified_name(id.0, None))
503    }
504
505    /// Get the basename of the [`Name`] (ie, with namespaces stripped).
506    /// Return a reference to [`Id`]
507    pub fn basename_as_ref(&self) -> &Id {
508        self.0.basename()
509    }
510
511    /// Get the basename of the [`Name`] (ie, with namespaces stripped).
512    /// Return an [`UnreservedId`]
513    pub fn basename(&self) -> UnreservedId {
514        // PANIC SAFETY: Any component of a `Name` is a `UnreservedId`
515        #![allow(clippy::unwrap_used)]
516        self.0.basename().clone().try_into().unwrap()
517    }
518
519    /// Test if a [`Name`] is an [`UnreservedId`]
520    pub fn is_unqualified(&self) -> bool {
521        self.0.is_unqualified()
522    }
523
524    /// Qualify the name with an optional namespace
525    ///
526    /// This method has the same behavior as [`InternalName::qualify_with()`]
527    pub fn qualify_with(&self, namespace: Option<&InternalName>) -> InternalName {
528        self.0.qualify_with(namespace)
529    }
530
531    /// Qualify the name with an optional namespace
532    ///
533    /// This method has the same behavior as [`InternalName::qualify_with_name()`] except that
534    /// it's guaranteed to return [`Name`], not [`InternalName`]
535    pub fn qualify_with_name(&self, namespace: Option<&Self>) -> Self {
536        // This is safe (upholds the `Name` invariant) because both `self` and `namespace`
537        // cannot contain `__cedar` -- they were already `Name`s
538        Self(self.as_ref().qualify_with(namespace.map(|n| n.as_ref())))
539    }
540
541    /// Get the source location
542    pub fn loc(&self) -> Option<&Loc> {
543        self.0.loc()
544    }
545
546    fn parse_err_from_str(s: &str) -> ParseErrors {
547        match Self::from_str(s) {
548            Err(parse_err) => parse_err,
549            Ok(parsed) => {
550                let normalized_src = parsed.to_string();
551                let diff_byte = s
552                    .bytes()
553                    .zip(normalized_src.bytes())
554                    .enumerate()
555                    .find(|(_, (b0, b1))| b0 != b1)
556                    .map(|(idx, _)| idx)
557                    .unwrap_or_else(|| s.len().min(normalized_src.len()));
558
559                ParseErrors::singleton(ParseError::ToAST(ToASTError::new(
560                    ToASTErrorKind::NonNormalizedString {
561                        kind: Self::describe_self(),
562                        src: s.to_string(),
563                        normalized_src,
564                    },
565                    Some(Loc::new(diff_byte, s.into())),
566                )))
567            }
568        }
569    }
570}
571
572/// Error when a reserved name is used where it is not allowed
573#[derive(Debug, Clone, PartialEq, Eq, Error, Diagnostic, Hash)]
574#[error("The name `{0}` contains `__cedar`, which is reserved")]
575pub struct ReservedNameError(pub(crate) InternalName);
576
577impl ReservedNameError {
578    /// The [`InternalName`] which contained a reserved component
579    pub fn name(&self) -> &InternalName {
580        &self.0
581    }
582}
583
584impl From<ReservedNameError> for ParseError {
585    fn from(value: ReservedNameError) -> Self {
586        ParseError::ToAST(ToASTError::new(
587            value.clone().into(),
588            value.0.loc.clone().or_else(|| {
589                let name_str = value.0.to_string();
590                Some(Loc::new(0..(name_str.len()), name_str.into()))
591            }),
592        ))
593    }
594}
595
596impl TryFrom<InternalName> for Name {
597    type Error = ReservedNameError;
598    fn try_from(value: InternalName) -> Result<Self, Self::Error> {
599        if value.is_reserved() {
600            Err(ReservedNameError(value))
601        } else {
602            Ok(Self(value))
603        }
604    }
605}
606
607impl<'a> TryFrom<&'a InternalName> for &'a Name {
608    type Error = ReservedNameError;
609    fn try_from(value: &'a InternalName) -> Result<&'a Name, ReservedNameError> {
610        if value.is_reserved() {
611            Err(ReservedNameError(value.clone()))
612        } else {
613            Ok(<Name as RefCast>::ref_cast(value))
614        }
615    }
616}
617
618impl From<Name> for InternalName {
619    fn from(value: Name) -> Self {
620        value.0
621    }
622}
623
624impl AsRef<InternalName> for Name {
625    fn as_ref(&self) -> &InternalName {
626        &self.0
627    }
628}
629
630#[cfg(feature = "arbitrary")]
631impl<'a> arbitrary::Arbitrary<'a> for Name {
632    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
633        // Computing hash of long id strings can be expensive
634        // Hence we limit the size of `path` such that DRT does not report slow
635        // units
636        let path_size = u.int_in_range(0..=8)?;
637        let basename: UnreservedId = u.arbitrary()?;
638        let path: Vec<UnreservedId> = (0..path_size)
639            .map(|_| u.arbitrary())
640            .collect::<Result<Vec<_>, _>>()?;
641        let name = InternalName::new(basename.into(), path.into_iter().map(|id| id.into()), None);
642        // PANIC SAFETY: `name` is made of `UnreservedId`s and thus should be a valid `Name`
643        #[allow(clippy::unwrap_used)]
644        Ok(name.try_into().unwrap())
645    }
646
647    fn size_hint(depth: usize) -> (usize, Option<usize>) {
648        <InternalName as arbitrary::Arbitrary>::size_hint(depth)
649    }
650}
651
652#[cfg(test)]
653mod test {
654    use super::*;
655
656    #[test]
657    fn normalized_name() {
658        InternalName::from_normalized_str("foo").expect("should be OK");
659        InternalName::from_normalized_str("foo::bar").expect("should be OK");
660        InternalName::from_normalized_str(r#"foo::"bar""#).expect_err("shouldn't be OK");
661        InternalName::from_normalized_str(" foo").expect_err("shouldn't be OK");
662        InternalName::from_normalized_str("foo ").expect_err("shouldn't be OK");
663        InternalName::from_normalized_str("foo\n").expect_err("shouldn't be OK");
664        InternalName::from_normalized_str("foo//comment").expect_err("shouldn't be OK");
665    }
666
667    #[test]
668    fn qualify_with() {
669        assert_eq!(
670            "foo::bar::baz",
671            InternalName::from_normalized_str("baz")
672                .unwrap()
673                .qualify_with(Some(&"foo::bar".parse().unwrap()))
674                .to_smolstr()
675        );
676        assert_eq!(
677            "C::D",
678            InternalName::from_normalized_str("C::D")
679                .unwrap()
680                .qualify_with(Some(&"A::B".parse().unwrap()))
681                .to_smolstr()
682        );
683        assert_eq!(
684            "A::B::C::D",
685            InternalName::from_normalized_str("D")
686                .unwrap()
687                .qualify_with(Some(&"A::B::C".parse().unwrap()))
688                .to_smolstr()
689        );
690        assert_eq!(
691            "B::C::D",
692            InternalName::from_normalized_str("B::C::D")
693                .unwrap()
694                .qualify_with(Some(&"A".parse().unwrap()))
695                .to_smolstr()
696        );
697        assert_eq!(
698            "A",
699            InternalName::from_normalized_str("A")
700                .unwrap()
701                .qualify_with(None)
702                .to_smolstr()
703        )
704    }
705
706    #[test]
707    fn test_reserved() {
708        for n in [
709            "__cedar",
710            "__cedar::A",
711            "__cedar::A::B",
712            "A::__cedar",
713            "A::__cedar::B",
714        ] {
715            assert!(InternalName::from_normalized_str(n).unwrap().is_reserved());
716        }
717
718        for n in ["__cedarr", "A::_cedar", "A::___cedar::B"] {
719            assert!(!InternalName::from_normalized_str(n).unwrap().is_reserved());
720        }
721    }
722
723    #[test]
724    fn test_name_identifier_intersection() {
725        let not_reserved_for_ids = [
726            "permit",
727            "forbid",
728            "when",
729            "unless",
730            "principal",
731            "action",
732            "resource",
733            "context",
734        ];
735
736        for id in not_reserved_for_ids {
737            assert!(Name::from_normalized_str(&format!("A::{id}")).is_ok());
738        }
739
740        for id in RESERVED_IDS.iter() {
741            assert!(Name::from_normalized_str(&format!("A::{id}")).is_err());
742        }
743    }
744}