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