cedar_policy_validator/schema/
raw_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 crate::schema::AllDefs;
18use crate::schema_errors::TypeNotDefinedError;
19use cedar_policy_core::ast::{Id, InternalName, Name, UnreservedId};
20use cedar_policy_core::parser::Loc;
21use itertools::Itertools;
22use nonempty::{nonempty, NonEmpty};
23use serde::{Deserialize, Serialize};
24
25/// A newtype which indicates that the contained [`InternalName`] may not yet be
26/// fully-qualified.
27///
28/// You can convert it to a fully-qualified [`InternalName`] using
29/// `.qualify_with()`, `.qualify_with_name()`, or `.conditionally_qualify_with()`.
30#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
31#[serde(transparent)]
32#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
33pub struct RawName(InternalName);
34
35impl RawName {
36    /// Create a new [`RawName`] from the given [`Id`]
37    pub fn new(id: Id) -> Self {
38        Self(InternalName::unqualified_name(id))
39    }
40
41    /// Create a new [`RawName`] from the given [`UnreservedId`]
42    pub fn new_from_unreserved(id: UnreservedId) -> Self {
43        Self::new(id.into())
44    }
45
46    /// Create a new [`RawName`] from the given [`InternalName`].
47    ///
48    /// Note that if `name` includes explicit namespaces, the result will be a
49    /// [`RawName`] that also includes those explicit namespaces, as if that
50    /// fully-qualified name appeared directly in the (JSON or Cedar) schema
51    /// format.
52    /// If `name` does not include explicit namespaces, the result will be a
53    /// [`RawName`] that also does not include explicit namespaces, which may or
54    /// may not translate back to the original input `name`, due to
55    /// namespace-qualification rules.
56    pub fn from_name(name: InternalName) -> Self {
57        Self(name)
58    }
59
60    /// Create a new [`RawName`] by parsing the provided string, which should contain
61    /// an unqualified `InternalName` (no explicit namespaces)
62    pub fn parse_unqualified_name(
63        s: &str,
64    ) -> Result<Self, cedar_policy_core::parser::err::ParseErrors> {
65        InternalName::parse_unqualified_name(s).map(RawName)
66    }
67
68    /// Create a new [`RawName`] by parsing the provided string, which should contain
69    /// an `InternalName` in normalized form.
70    ///
71    /// (See the [`cedar_policy_core::FromNormalizedStr`] trait.)
72    pub fn from_normalized_str(
73        s: &str,
74    ) -> Result<Self, cedar_policy_core::parser::err::ParseErrors> {
75        use cedar_policy_core::FromNormalizedStr;
76        InternalName::from_normalized_str(s).map(RawName)
77    }
78
79    /// Is this `RawName` unqualified, that is, written without any _explicit_
80    /// namespaces.
81    /// (This method returning `true` does not imply that the `RawName` will
82    /// _eventually resolve_ to an unqualified name.)
83    pub fn is_unqualified(&self) -> bool {
84        self.0.is_unqualified()
85    }
86
87    /// Get the source location of this `RawName`
88    pub fn loc(&self) -> Option<&Loc> {
89        self.0.loc()
90    }
91
92    /// Convert this [`RawName`] to an [`InternalName`] by adding the given `ns`
93    /// as its prefix, or by no-op if `ns` is `None`.
94    ///
95    /// Note that if the [`RawName`] already had a non-empty explicit namespace,
96    /// no additional prefixing will be done, even if `ns` is `Some`.
97    pub fn qualify_with(self, ns: Option<&InternalName>) -> InternalName {
98        self.0.qualify_with(ns)
99    }
100
101    /// Convert this [`RawName`] to an [`InternalName`] by adding the given `ns`
102    /// as its prefix, or by no-op if `ns` is `None`.
103    ///
104    /// Note that if the [`RawName`] already had a non-empty explicit namespace,
105    /// no additional prefixing will be done, even if `ns` is `Some`.
106    pub fn qualify_with_name(self, ns: Option<&Name>) -> InternalName {
107        self.0.qualify_with_name(ns)
108    }
109
110    /// Convert this [`RawName`] to a [`ConditionalName`].
111    /// This method is appropriate for when we encounter this [`RawName`] as a
112    /// type reference while the current/active namespace is `ns` (or `None` if
113    /// the current/active namespace is the empty namespace).
114    ///
115    /// This [`RawName`] will resolve as follows:
116    /// - If the [`RawName`] already has a non-empty explicit namespace, there
117    ///     is no ambiguity, and it will resolve always and only to itself
118    /// - Otherwise (if the [`RawName`] does not have an explicit namespace
119    ///     already), then it resolves to the following in priority order:
120    ///     1. The fully-qualified name resulting from prefixing `ns` to this
121    ///         [`RawName`], if that fully-qualified name is declared in the schema
122    ///         (in any schema fragment)
123    ///     2. Itself in the empty namespace, if that name is declared in the schema
124    ///         (in any schema fragment)
125    ///
126    /// Note that if the [`RawName`] is the name of a primitive or extension
127    /// type (without explicit `__cedar`), it will resolve via (2) above,
128    /// because the primitive/extension type names will be added as defined
129    /// common types in the empty namespace (aliasing to the real `__cedar`
130    /// definitions), assuming the user didn't themselves define those names
131    /// in the empty namespace.
132    pub fn conditionally_qualify_with(
133        self,
134        ns: Option<&InternalName>,
135        reference_type: ReferenceType,
136    ) -> ConditionalName {
137        let possibilities = if self.is_unqualified() {
138            match ns {
139                Some(ns) => {
140                    // the `RawName` does not have any namespace attached, so it refers
141                    // to something in the current namespace if available; otherwise, it
142                    // refers to something in the empty namespace
143                    nonempty![
144                        self.clone().qualify_with(Some(ns)),
145                        self.clone().qualify_with(None),
146                    ]
147                }
148                None => {
149                    // Same as the above case, but since the current/active
150                    // namespace is the empty namespace, the two possibilities
151                    // are the same; there is only one possibility
152                    nonempty![self.clone().qualify_with(None)]
153                }
154            }
155        } else {
156            // if the `RawName` already had an explicit namespace, there's no
157            // ambiguity
158            nonempty![self.clone().qualify_with(None)]
159        };
160        ConditionalName {
161            possibilities,
162            reference_type,
163            raw: self,
164        }
165    }
166}
167
168impl std::fmt::Display for RawName {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        write!(f, "{}", self.0)
171    }
172}
173
174impl std::str::FromStr for RawName {
175    type Err = <InternalName as std::str::FromStr>::Err;
176    fn from_str(s: &str) -> Result<Self, Self::Err> {
177        InternalName::from_str(s).map(RawName)
178    }
179}
180
181/// A name which may refer to many possible different fully-qualified names,
182/// depending on which of them are declared (in any schema fragment)
183///
184/// Caution using `==` on these: [`ConditionalName`]s are only equal if the have
185/// the same raw (source) _and_ the same list of possible resolution targets (in
186/// the same order), which in practice means they must be in the same
187/// current/active namespace. In particular:
188/// - two [`ConditionalName`]s which end up resolving to the same fully-qualified
189///   name may nonetheless not be `==` in their [`ConditionalName`] forms; and
190/// - two [`ConditionalName`]s which are written the same way in the original
191///   schema may nonetheless not be `==` in their [`ConditionalName`] forms
192///
193/// This type has only one (trivial) public constructor; it is normally
194/// constructed using [`RawName::conditionally_qualify_with()`].
195#[derive(Debug, Clone, PartialEq, Eq, Hash)]
196pub struct ConditionalName {
197    /// The [`ConditionalName`] may refer to any of these `possibilities`, depending
198    /// on which of them are declared (in any schema fragment).
199    ///
200    /// These are in descending priority order. If the first `InternalName` is
201    /// declared (in any schema fragment), then this `ConditionalName` refers to
202    /// the first `InternalName`. If that `InternalName` is not declared in any
203    /// schema fragment, then we check the second `InternalName`, etc.
204    ///
205    /// All of the contained `InternalName`s must be fully-qualified.
206    ///
207    /// Typical example: In
208    /// ```text
209    /// namespace NS { ... some reference to Foo ... }
210    /// ```
211    /// `Foo` is a `ConditionalName` with `possibilities = [NS::Foo, Foo]`.
212    /// That is, if `NS::Foo` exists, `Foo` refers to `NS::Foo`, but otherwise,
213    /// `Foo` refers to the `Foo` declared in the empty namespace.
214    possibilities: NonEmpty<InternalName>,
215    /// Whether the [`ConditionalName`] can resolve to a common-type name, an
216    /// entity-type name, or both
217    reference_type: ReferenceType,
218    /// Copy of the original/raw name found in the source; this field is
219    /// used only in error messages
220    raw: RawName,
221}
222
223impl ConditionalName {
224    /// Create a [`ConditionalName`] which unconditionally resolves to the given
225    /// fully-qualified [`InternalName`].
226    pub fn unconditional(name: InternalName, reference_type: ReferenceType) -> Self {
227        ConditionalName {
228            possibilities: nonempty!(name.clone()),
229            reference_type,
230            raw: RawName(name),
231        }
232    }
233
234    /// Get the (not-yet-necessarily-fully-qualified) [`RawName`] which was
235    /// encountered in the source, for the purposes of error messages
236    pub fn raw(&self) -> &RawName {
237        &self.raw
238    }
239
240    /// Get the possible fully-qualified [`InternalName`]s which this [`ConditionalName`]
241    /// might resolve to, in priority order (highest-priority first).
242    pub(crate) fn possibilities(&self) -> impl Iterator<Item = &InternalName> {
243        self.possibilities.iter()
244    }
245
246    /// Get the source location of this [`ConditionalName`]
247    pub fn loc(&self) -> Option<&Loc> {
248        self.raw.loc()
249    }
250
251    /// Resolve the [`ConditionalName`] into a fully-qualified [`InternalName`],
252    /// given that `all_defs` includes all fully-qualified [`InternalName`]s
253    /// defined in all schema fragments.
254    ///
255    /// Note that this returns [`InternalName`] (as opposed to [`Name`]),
256    /// because type references may resolve to an internal name like
257    /// `__cedar::String`.
258    /// In general, as noted on [`InternalName`], [`InternalName`]s are valid
259    /// to appear as type _references_, and we generally expect
260    /// [`ConditionalName`]s to also represent type _references_.
261    ///
262    /// `all_defs` also internally includes [`InternalName`]s, because some
263    /// names containing `__cedar` might be internally defined/valid, even
264    /// though it is not valid for _end-users_ to define those names.
265    pub fn resolve(self, all_defs: &AllDefs) -> Result<InternalName, TypeNotDefinedError> {
266        for possibility in &self.possibilities {
267            // Per RFC 24, we give priority to trying to resolve to a common
268            // type, before trying to resolve to an entity type.
269            // (However, we have an even stronger preference to resolve earlier
270            // in the `possibilities` list. So, in the hypothetical case where
271            // we could resolve to either an entity type first in the
272            // `possibilities` list, or a common type later in the
273            // `possibilities` list, we choose the former.)
274            // See also cedar#579.
275            if matches!(
276                self.reference_type,
277                ReferenceType::Common | ReferenceType::CommonOrEntity
278            ) && all_defs.is_defined_as_common(possibility)
279            {
280                return Ok(possibility.clone());
281            }
282            if matches!(
283                self.reference_type,
284                ReferenceType::Entity | ReferenceType::CommonOrEntity
285            ) && all_defs.is_defined_as_entity(possibility)
286            {
287                return Ok(possibility.clone());
288            }
289        }
290        Err(TypeNotDefinedError {
291            undefined_types: nonempty![self],
292        })
293    }
294
295    /// Provide a help message for the case where this [`ConditionalName`] failed to resolve
296    pub(crate) fn resolution_failure_help(&self) -> String {
297        let entity_or_common_text = match self.reference_type {
298            ReferenceType::Common => "as a common type",
299            ReferenceType::Entity => "as an entity type",
300            ReferenceType::CommonOrEntity => "as a common or entity type",
301        };
302        // PANIC SAFETY: indexing is safe because we first check the `.len()`
303        #[allow(clippy::indexing_slicing)]
304        match self.possibilities.len() {
305            1 => format!(
306                "`{}` has not been declared {}",
307                self.possibilities[0], entity_or_common_text
308            ),
309            2 => format!(
310                "neither `{}` nor `{}` refers to anything that has been declared {}",
311                self.possibilities[0], self.possibilities[1], entity_or_common_text,
312            ),
313            _ => format!(
314                "none of these have been declared {}: {}",
315                entity_or_common_text,
316                self.possibilities
317                    .iter()
318                    .map(|p| format!("`{p}`"))
319                    .join(", ")
320            ),
321        }
322    }
323}
324
325/// [`ConditionalName`] serializes as simply the raw name that was originally encountered in the schema
326impl Serialize for ConditionalName {
327    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
328    where
329        S: serde::Serializer,
330    {
331        self.raw().serialize(serializer)
332    }
333}
334
335/// Describes whether a reference can resolve to a common-type name, an
336/// entity-type name, or both
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
338pub enum ReferenceType {
339    /// The reference can only resolve to a common-type name
340    Common,
341    /// The reference can only resolve to an entity-type name
342    Entity,
343    /// The reference can resolve to either an entity or common type name
344    CommonOrEntity,
345}