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}