Skip to main content

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