cedar_policy_core/ast/
restricted_expr.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::{
18    EntityUID, Expr, ExprKind, ExpressionConstructionError, Literal, Name, PartialValue, Type,
19    Unknown, Value, ValueKind,
20};
21use crate::entities::json::err::JsonSerializationError;
22use crate::extensions::Extensions;
23use crate::parser::err::ParseErrors;
24use crate::parser::{self, Loc};
25use miette::Diagnostic;
26use serde::{Deserialize, Serialize};
27use smol_str::{SmolStr, ToSmolStr};
28use std::hash::{Hash, Hasher};
29use std::ops::Deref;
30use std::sync::Arc;
31use thiserror::Error;
32
33/// A few places in Core use these "restricted expressions" (for lack of a
34/// better term) which are in some sense the minimal subset of `Expr` required
35/// to express all possible `Value`s.
36///
37/// Specifically, "restricted" expressions are
38/// defined as expressions containing only the following:
39///   - bool, int, and string literals
40///   - literal EntityUIDs such as User::"alice"
41///   - extension function calls, where the arguments must be other things
42///       on this list
43///   - set and record literals, where the values must be other things on
44///       this list
45///
46/// That means the following are not allowed in "restricted" expressions:
47///   - `principal`, `action`, `resource`, `context`
48///   - builtin operators and functions, including `.`, `in`, `has`, `like`,
49///       `.contains()`
50///   - if-then-else expressions
51///
52/// These restrictions represent the expressions that are allowed to appear as
53/// attribute values in `Slice` and `Context`.
54#[derive(Deserialize, Serialize, Hash, Debug, Clone, PartialEq, Eq)]
55#[serde(transparent)]
56pub struct RestrictedExpr(Expr);
57
58impl RestrictedExpr {
59    /// Create a new `RestrictedExpr` from an `Expr`.
60    ///
61    /// This function is "safe" in the sense that it will verify that the
62    /// provided `expr` does indeed qualify as a "restricted" expression,
63    /// returning an error if not.
64    ///
65    /// Note this check requires recursively walking the AST. For a version of
66    /// this function that doesn't perform this check, see `new_unchecked()`
67    /// below.
68    pub fn new(expr: Expr) -> Result<Self, RestrictedExpressionError> {
69        is_restricted(&expr)?;
70        Ok(Self(expr))
71    }
72
73    /// Create a new `RestrictedExpr` from an `Expr`, where the caller is
74    /// responsible for ensuring that the `Expr` is a valid "restricted
75    /// expression". If it is not, internal invariants will be violated, which
76    /// may lead to other errors later, panics, or even incorrect results.
77    ///
78    /// For a "safer" version of this function that returns an error for invalid
79    /// inputs, see `new()` above.
80    pub fn new_unchecked(expr: Expr) -> Self {
81        // in debug builds, this does the check anyway, panicking if it fails
82        if cfg!(debug_assertions) {
83            // PANIC SAFETY: We're in debug mode and panicking intentionally
84            #[allow(clippy::unwrap_used)]
85            Self::new(expr).unwrap()
86        } else {
87            Self(expr)
88        }
89    }
90
91    /// Return the `RestrictedExpr`, but with the new `source_loc` (or `None`).
92    pub fn with_maybe_source_loc(self, source_loc: Option<Loc>) -> Self {
93        Self(self.0.with_maybe_source_loc(source_loc))
94    }
95
96    /// Create a `RestrictedExpr` that's just a single `Literal`.
97    ///
98    /// Note that you can pass this a `Literal`, an `Integer`, a `String`, etc.
99    pub fn val(v: impl Into<Literal>) -> Self {
100        // All literals are valid restricted-exprs
101        Self::new_unchecked(Expr::val(v))
102    }
103
104    /// Create a `RestrictedExpr` that's just a single `Unknown`.
105    pub fn unknown(u: Unknown) -> Self {
106        // All unknowns are valid restricted-exprs
107        Self::new_unchecked(Expr::unknown(u))
108    }
109
110    /// Create a `RestrictedExpr` which evaluates to a Set of the given `RestrictedExpr`s
111    pub fn set(exprs: impl IntoIterator<Item = RestrictedExpr>) -> Self {
112        // Set expressions are valid restricted-exprs if their elements are; and
113        // we know the elements are because we require `RestrictedExpr`s in the
114        // parameter
115        Self::new_unchecked(Expr::set(exprs.into_iter().map(Into::into)))
116    }
117
118    /// Create a `RestrictedExpr` which evaluates to a Record with the given
119    /// (key, value) pairs.
120    ///
121    /// Throws an error if any key occurs two or more times.
122    pub fn record(
123        pairs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
124    ) -> Result<Self, ExpressionConstructionError> {
125        // Record expressions are valid restricted-exprs if their elements are;
126        // and we know the elements are because we require `RestrictedExpr`s in
127        // the parameter
128        Ok(Self::new_unchecked(Expr::record(
129            pairs.into_iter().map(|(k, v)| (k, v.into())),
130        )?))
131    }
132
133    /// Create a `RestrictedExpr` which calls the given extension function
134    pub fn call_extension_fn(
135        function_name: Name,
136        args: impl IntoIterator<Item = RestrictedExpr>,
137    ) -> Self {
138        // Extension-function calls are valid restricted-exprs if their
139        // arguments are; and we know the arguments are because we require
140        // `RestrictedExpr`s in the parameter
141        Self::new_unchecked(Expr::call_extension_fn(
142            function_name,
143            args.into_iter().map(Into::into).collect(),
144        ))
145    }
146
147    /// Write a RestrictedExpr in "natural JSON" format.
148    ///
149    /// Used to output the context as a map from Strings to JSON Values
150    pub fn to_natural_json(&self) -> Result<serde_json::Value, JsonSerializationError> {
151        self.as_borrowed().to_natural_json()
152    }
153
154    /// Get the `bool` value of this `RestrictedExpr` if it's a boolean, or
155    /// `None` if it is not a boolean
156    pub fn as_bool(&self) -> Option<bool> {
157        // the only way a `RestrictedExpr` can be a boolean is if it's a literal
158        match self.expr_kind() {
159            ExprKind::Lit(Literal::Bool(b)) => Some(*b),
160            _ => None,
161        }
162    }
163
164    /// Get the `i64` value of this `RestrictedExpr` if it's a long, or `None`
165    /// if it is not a long
166    pub fn as_long(&self) -> Option<i64> {
167        // the only way a `RestrictedExpr` can be a long is if it's a literal
168        match self.expr_kind() {
169            ExprKind::Lit(Literal::Long(i)) => Some(*i),
170            _ => None,
171        }
172    }
173
174    /// Get the `SmolStr` value of this `RestrictedExpr` if it's a string, or
175    /// `None` if it is not a string
176    pub fn as_string(&self) -> Option<&SmolStr> {
177        // the only way a `RestrictedExpr` can be a string is if it's a literal
178        match self.expr_kind() {
179            ExprKind::Lit(Literal::String(s)) => Some(s),
180            _ => None,
181        }
182    }
183
184    /// Get the `EntityUID` value of this `RestrictedExpr` if it's an entity
185    /// reference, or `None` if it is not an entity reference
186    pub fn as_euid(&self) -> Option<&EntityUID> {
187        // the only way a `RestrictedExpr` can be an entity reference is if it's
188        // a literal
189        match self.expr_kind() {
190            ExprKind::Lit(Literal::EntityUID(e)) => Some(e),
191            _ => None,
192        }
193    }
194
195    /// Get `Unknown` value of this `RestrictedExpr` if it's an `Unknown`, or
196    /// `None` if it is not an `Unknown`
197    pub fn as_unknown(&self) -> Option<&Unknown> {
198        match self.expr_kind() {
199            ExprKind::Unknown(u) => Some(u),
200            _ => None,
201        }
202    }
203
204    /// Iterate over the elements of the set if this `RestrictedExpr` is a set,
205    /// or `None` if it is not a set
206    pub fn as_set_elements(&self) -> Option<impl Iterator<Item = BorrowedRestrictedExpr<'_>>> {
207        match self.expr_kind() {
208            ExprKind::Set(set) => Some(set.iter().map(BorrowedRestrictedExpr::new_unchecked)), // since the RestrictedExpr invariant holds for the input set, it will hold for each element as well
209            _ => None,
210        }
211    }
212
213    /// Iterate over the (key, value) pairs of the record if this
214    /// `RestrictedExpr` is a record, or `None` if it is not a record
215    pub fn as_record_pairs(
216        &self,
217    ) -> Option<impl Iterator<Item = (&SmolStr, BorrowedRestrictedExpr<'_>)>> {
218        match self.expr_kind() {
219            ExprKind::Record(map) => Some(
220                map.iter()
221                    .map(|(k, v)| (k, BorrowedRestrictedExpr::new_unchecked(v))),
222            ), // since the RestrictedExpr invariant holds for the input record, it will hold for each attr value as well
223            _ => None,
224        }
225    }
226
227    /// Get the name and args of the called extension function if this
228    /// `RestrictedExpr` is an extension function call, or `None` if it is not
229    /// an extension function call
230    pub fn as_extn_fn_call(
231        &self,
232    ) -> Option<(&Name, impl Iterator<Item = BorrowedRestrictedExpr<'_>>)> {
233        match self.expr_kind() {
234            ExprKind::ExtensionFunctionApp { fn_name, args } => Some((
235                fn_name,
236                args.iter().map(BorrowedRestrictedExpr::new_unchecked),
237            )), // since the RestrictedExpr invariant holds for the input call, it will hold for each argument as well
238            _ => None,
239        }
240    }
241}
242
243impl From<Value> for RestrictedExpr {
244    fn from(value: Value) -> RestrictedExpr {
245        RestrictedExpr::from(value.value).with_maybe_source_loc(value.loc)
246    }
247}
248
249impl From<ValueKind> for RestrictedExpr {
250    fn from(value: ValueKind) -> RestrictedExpr {
251        match value {
252            ValueKind::Lit(lit) => RestrictedExpr::val(lit),
253            ValueKind::Set(set) => {
254                RestrictedExpr::set(set.iter().map(|val| RestrictedExpr::from(val.clone())))
255            }
256            // PANIC SAFETY: cannot have duplicate key because the input was already a BTreeMap
257            #[allow(clippy::expect_used)]
258            ValueKind::Record(record) => RestrictedExpr::record(
259                Arc::unwrap_or_clone(record)
260                    .into_iter()
261                    .map(|(k, v)| (k, RestrictedExpr::from(v))),
262            )
263            .expect("can't have duplicate keys, because the input `map` was already a BTreeMap"),
264            ValueKind::ExtensionValue(ev) => {
265                let ev = Arc::unwrap_or_clone(ev);
266                ev.into()
267            }
268        }
269    }
270}
271
272impl TryFrom<PartialValue> for RestrictedExpr {
273    type Error = PartialValueToRestrictedExprError;
274    fn try_from(pvalue: PartialValue) -> Result<RestrictedExpr, PartialValueToRestrictedExprError> {
275        match pvalue {
276            PartialValue::Value(v) => Ok(RestrictedExpr::from(v)),
277            PartialValue::Residual(expr) => match RestrictedExpr::new(expr) {
278                Ok(e) => Ok(e),
279                Err(RestrictedExpressionError::InvalidRestrictedExpression(
280                    restricted_expr_errors::InvalidRestrictedExpressionError { expr, .. },
281                )) => Err(PartialValueToRestrictedExprError::NontrivialResidual {
282                    residual: Box::new(expr),
283                }),
284            },
285        }
286    }
287}
288
289/// Errors when converting `PartialValue` to `RestrictedExpr`
290#[derive(Debug, PartialEq, Eq, Diagnostic, Error)]
291pub enum PartialValueToRestrictedExprError {
292    /// The `PartialValue` contains a nontrivial residual that isn't a valid `RestrictedExpr`
293    #[error("residual is not a valid restricted expression: `{residual}`")]
294    NontrivialResidual {
295        /// Residual that isn't a valid `RestrictedExpr`
296        residual: Box<Expr>,
297    },
298}
299
300impl std::str::FromStr for RestrictedExpr {
301    type Err = RestrictedExpressionParseError;
302
303    fn from_str(s: &str) -> Result<RestrictedExpr, Self::Err> {
304        parser::parse_restrictedexpr(s)
305    }
306}
307
308/// While `RestrictedExpr` wraps an _owned_ `Expr`, `BorrowedRestrictedExpr`
309/// wraps a _borrowed_ `Expr`, with the same invariants.
310///
311/// We derive `Copy` for this type because it's just a single reference, and
312/// `&T` is `Copy` for all `T`.
313#[derive(Serialize, Hash, Debug, Clone, PartialEq, Eq, Copy)]
314pub struct BorrowedRestrictedExpr<'a>(&'a Expr);
315
316impl<'a> BorrowedRestrictedExpr<'a> {
317    /// Create a new `BorrowedRestrictedExpr` from an `&Expr`.
318    ///
319    /// This function is "safe" in the sense that it will verify that the
320    /// provided `expr` does indeed qualify as a "restricted" expression,
321    /// returning an error if not.
322    ///
323    /// Note this check requires recursively walking the AST. For a version of
324    /// this function that doesn't perform this check, see `new_unchecked()`
325    /// below.
326    pub fn new(expr: &'a Expr) -> Result<Self, RestrictedExpressionError> {
327        is_restricted(expr)?;
328        Ok(Self(expr))
329    }
330
331    /// Create a new `BorrowedRestrictedExpr` from an `&Expr`, where the caller
332    /// is responsible for ensuring that the `Expr` is a valid "restricted
333    /// expression". If it is not, internal invariants will be violated, which
334    /// may lead to other errors later, panics, or even incorrect results.
335    ///
336    /// For a "safer" version of this function that returns an error for invalid
337    /// inputs, see `new()` above.
338    pub fn new_unchecked(expr: &'a Expr) -> Self {
339        // in debug builds, this does the check anyway, panicking if it fails
340        if cfg!(debug_assertions) {
341            // PANIC SAFETY: We're in debug mode and panicking intentionally
342            #[allow(clippy::unwrap_used)]
343            Self::new(expr).unwrap()
344        } else {
345            Self(expr)
346        }
347    }
348
349    /// Write a BorrowedRestrictedExpr in "natural JSON" format.
350    ///
351    /// Used to output the context as a map from Strings to JSON Values
352    pub fn to_natural_json(self) -> Result<serde_json::Value, JsonSerializationError> {
353        Ok(serde_json::to_value(
354            crate::entities::json::CedarValueJson::from_expr(self)?,
355        )?)
356    }
357
358    /// Convert `BorrowedRestrictedExpr` to `RestrictedExpr`.
359    /// This has approximately the cost of cloning the `Expr`.
360    pub fn to_owned(self) -> RestrictedExpr {
361        RestrictedExpr::new_unchecked(self.0.clone())
362    }
363
364    /// Get the `bool` value of this `RestrictedExpr` if it's a boolean, or
365    /// `None` if it is not a boolean
366    pub fn as_bool(&self) -> Option<bool> {
367        // the only way a `RestrictedExpr` can be a boolean is if it's a literal
368        match self.expr_kind() {
369            ExprKind::Lit(Literal::Bool(b)) => Some(*b),
370            _ => None,
371        }
372    }
373
374    /// Get the `i64` value of this `RestrictedExpr` if it's a long, or `None`
375    /// if it is not a long
376    pub fn as_long(&self) -> Option<i64> {
377        // the only way a `RestrictedExpr` can be a long is if it's a literal
378        match self.expr_kind() {
379            ExprKind::Lit(Literal::Long(i)) => Some(*i),
380            _ => None,
381        }
382    }
383
384    /// Get the `SmolStr` value of this `RestrictedExpr` if it's a string, or
385    /// `None` if it is not a string
386    pub fn as_string(&self) -> Option<&SmolStr> {
387        // the only way a `RestrictedExpr` can be a string is if it's a literal
388        match self.expr_kind() {
389            ExprKind::Lit(Literal::String(s)) => Some(s),
390            _ => None,
391        }
392    }
393
394    /// Get the `EntityUID` value of this `RestrictedExpr` if it's an entity
395    /// reference, or `None` if it is not an entity reference
396    pub fn as_euid(&self) -> Option<&EntityUID> {
397        // the only way a `RestrictedExpr` can be an entity reference is if it's
398        // a literal
399        match self.expr_kind() {
400            ExprKind::Lit(Literal::EntityUID(e)) => Some(e),
401            _ => None,
402        }
403    }
404
405    /// Get `Unknown` value of this `RestrictedExpr` if it's an `Unknown`, or
406    /// `None` if it is not an `Unknown`
407    pub fn as_unknown(&self) -> Option<&Unknown> {
408        match self.expr_kind() {
409            ExprKind::Unknown(u) => Some(u),
410            _ => None,
411        }
412    }
413
414    /// Iterate over the elements of the set if this `RestrictedExpr` is a set,
415    /// or `None` if it is not a set
416    pub fn as_set_elements(&self) -> Option<impl Iterator<Item = BorrowedRestrictedExpr<'_>>> {
417        match self.expr_kind() {
418            ExprKind::Set(set) => Some(set.iter().map(BorrowedRestrictedExpr::new_unchecked)), // since the RestrictedExpr invariant holds for the input set, it will hold for each element as well
419            _ => None,
420        }
421    }
422
423    /// Iterate over the (key, value) pairs of the record if this
424    /// `RestrictedExpr` is a record, or `None` if it is not a record
425    pub fn as_record_pairs(
426        &self,
427    ) -> Option<impl Iterator<Item = (&'_ SmolStr, BorrowedRestrictedExpr<'_>)>> {
428        match self.expr_kind() {
429            ExprKind::Record(map) => Some(
430                map.iter()
431                    .map(|(k, v)| (k, BorrowedRestrictedExpr::new_unchecked(v))),
432            ), // since the RestrictedExpr invariant holds for the input record, it will hold for each attr value as well
433            _ => None,
434        }
435    }
436
437    /// Get the name and args of the called extension function if this
438    /// `RestrictedExpr` is an extension function call, or `None` if it is not
439    /// an extension function call
440    pub fn as_extn_fn_call(
441        &self,
442    ) -> Option<(&Name, impl Iterator<Item = BorrowedRestrictedExpr<'_>>)> {
443        match self.expr_kind() {
444            ExprKind::ExtensionFunctionApp { fn_name, args } => Some((
445                fn_name,
446                args.iter().map(BorrowedRestrictedExpr::new_unchecked),
447            )), // since the RestrictedExpr invariant holds for the input call, it will hold for each argument as well
448            _ => None,
449        }
450    }
451
452    /// Try to compute the runtime type of this expression. See
453    /// [`Expr::try_type_of`] for exactly what this computes.
454    ///
455    /// On a restricted expression, there are fewer cases where we might fail to
456    /// compute the type, but there are still `unknown`s and extension function
457    /// calls which may cause this function to return `None` .
458    pub fn try_type_of(&self, extensions: &Extensions<'_>) -> Option<Type> {
459        self.0.try_type_of(extensions)
460    }
461}
462
463/// Helper function: does the given `Expr` qualify as a "restricted" expression.
464///
465/// Returns `Ok(())` if yes, or a `RestrictedExpressionError` if no.
466fn is_restricted(expr: &Expr) -> Result<(), RestrictedExpressionError> {
467    match expr.expr_kind() {
468        ExprKind::Lit(_) => Ok(()),
469        ExprKind::Unknown(_) => Ok(()),
470        ExprKind::Var(_) => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
471            feature: "variables".into(),
472            expr: expr.clone(),
473        }
474        .into()),
475        ExprKind::Slot(_) => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
476            feature: "template slots".into(),
477            expr: expr.clone(),
478        }
479        .into()),
480        ExprKind::If { .. } => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
481            feature: "if-then-else".into(),
482            expr: expr.clone(),
483        }
484        .into()),
485        ExprKind::And { .. } => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
486            feature: "&&".into(),
487            expr: expr.clone(),
488        }
489        .into()),
490        ExprKind::Or { .. } => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
491            feature: "||".into(),
492            expr: expr.clone(),
493        }
494        .into()),
495        ExprKind::UnaryApp { op, .. } => {
496            Err(restricted_expr_errors::InvalidRestrictedExpressionError {
497                feature: op.to_smolstr(),
498                expr: expr.clone(),
499            }
500            .into())
501        }
502        ExprKind::BinaryApp { op, .. } => {
503            Err(restricted_expr_errors::InvalidRestrictedExpressionError {
504                feature: op.to_smolstr(),
505                expr: expr.clone(),
506            }
507            .into())
508        }
509        ExprKind::GetAttr { .. } => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
510            feature: "attribute accesses".into(),
511            expr: expr.clone(),
512        }
513        .into()),
514        ExprKind::HasAttr { .. } => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
515            feature: "'has'".into(),
516            expr: expr.clone(),
517        }
518        .into()),
519        ExprKind::Like { .. } => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
520            feature: "'like'".into(),
521            expr: expr.clone(),
522        }
523        .into()),
524        ExprKind::Is { .. } => Err(restricted_expr_errors::InvalidRestrictedExpressionError {
525            feature: "'is'".into(),
526            expr: expr.clone(),
527        }
528        .into()),
529        ExprKind::ExtensionFunctionApp { args, .. } => args.iter().try_for_each(is_restricted),
530        ExprKind::Set(exprs) => exprs.iter().try_for_each(is_restricted),
531        ExprKind::Record(map) => map.values().try_for_each(is_restricted),
532    }
533}
534
535// converting into Expr is always safe; restricted exprs are always valid Exprs
536impl From<RestrictedExpr> for Expr {
537    fn from(r: RestrictedExpr) -> Expr {
538        r.0
539    }
540}
541
542impl AsRef<Expr> for RestrictedExpr {
543    fn as_ref(&self) -> &Expr {
544        &self.0
545    }
546}
547
548impl Deref for RestrictedExpr {
549    type Target = Expr;
550    fn deref(&self) -> &Expr {
551        self.as_ref()
552    }
553}
554
555impl std::fmt::Display for RestrictedExpr {
556    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557        write!(f, "{}", &self.0)
558    }
559}
560
561// converting into Expr is always safe; restricted exprs are always valid Exprs
562impl<'a> From<BorrowedRestrictedExpr<'a>> for &'a Expr {
563    fn from(r: BorrowedRestrictedExpr<'a>) -> &'a Expr {
564        r.0
565    }
566}
567
568impl<'a> AsRef<Expr> for BorrowedRestrictedExpr<'a> {
569    fn as_ref(&self) -> &'a Expr {
570        self.0
571    }
572}
573
574impl RestrictedExpr {
575    /// Turn an `&RestrictedExpr` into a `BorrowedRestrictedExpr`
576    pub fn as_borrowed(&self) -> BorrowedRestrictedExpr<'_> {
577        BorrowedRestrictedExpr::new_unchecked(self.as_ref())
578    }
579}
580
581impl<'a> Deref for BorrowedRestrictedExpr<'a> {
582    type Target = Expr;
583    fn deref(&self) -> &'a Expr {
584        self.0
585    }
586}
587
588impl std::fmt::Display for BorrowedRestrictedExpr<'_> {
589    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
590        write!(f, "{}", &self.0)
591    }
592}
593
594/// Like `ExprShapeOnly`, but for restricted expressions.
595///
596/// A newtype wrapper around (borrowed) restricted expressions that provides
597/// `Eq` and `Hash` implementations that ignore any source information or other
598/// generic data used to annotate the expression.
599#[derive(Eq, Debug, Clone)]
600pub struct RestrictedExprShapeOnly<'a>(BorrowedRestrictedExpr<'a>);
601
602impl<'a> RestrictedExprShapeOnly<'a> {
603    /// Construct a `RestrictedExprShapeOnly` from a `BorrowedRestrictedExpr`.
604    /// The `BorrowedRestrictedExpr` is not modified, but any comparisons on the
605    /// resulting `RestrictedExprShapeOnly` will ignore source information and
606    /// generic data.
607    pub fn new(e: BorrowedRestrictedExpr<'a>) -> RestrictedExprShapeOnly<'a> {
608        RestrictedExprShapeOnly(e)
609    }
610}
611
612impl PartialEq for RestrictedExprShapeOnly<'_> {
613    fn eq(&self, other: &Self) -> bool {
614        self.0.eq_shape(&other.0)
615    }
616}
617
618impl Hash for RestrictedExprShapeOnly<'_> {
619    fn hash<H: Hasher>(&self, state: &mut H) {
620        self.0.hash_shape(state);
621    }
622}
623
624/// Error when constructing a restricted expression from unrestricted
625/// expression
626//
627// CAUTION: this type is publicly exported in `cedar-policy`.
628// Don't make fields `pub`, don't make breaking changes, and use caution
629// when adding public methods.
630#[derive(Debug, Clone, PartialEq, Eq, Error, Diagnostic)]
631pub enum RestrictedExpressionError {
632    /// An expression was expected to be a "restricted" expression, but contained
633    /// a feature that is not allowed in restricted expressions.
634    #[error(transparent)]
635    #[diagnostic(transparent)]
636    InvalidRestrictedExpression(#[from] restricted_expr_errors::InvalidRestrictedExpressionError),
637}
638
639/// Error subtypes for [`RestrictedExpressionError`]
640pub mod restricted_expr_errors {
641    use super::Expr;
642    use crate::impl_diagnostic_from_method_on_field;
643    use miette::Diagnostic;
644    use smol_str::SmolStr;
645    use thiserror::Error;
646
647    /// An expression was expected to be a "restricted" expression, but contained
648    /// a feature that is not allowed in restricted expressions.
649    //
650    // CAUTION: this type is publicly exported in `cedar-policy`.
651    // Don't make fields `pub`, don't make breaking changes, and use caution
652    // when adding public methods.
653    #[derive(Debug, Clone, PartialEq, Eq, Error)]
654    #[error("not allowed to use {feature} in a restricted expression: `{expr}`")]
655    pub struct InvalidRestrictedExpressionError {
656        /// String description of what disallowed feature appeared in the expression
657        pub(crate) feature: SmolStr,
658        /// the (sub-)expression that uses the disallowed feature. This may be a
659        /// sub-expression of a larger expression.
660        pub(crate) expr: Expr,
661    }
662
663    // custom impl of `Diagnostic`: take source location from the `expr` field's `.source_loc()` method
664    impl Diagnostic for InvalidRestrictedExpressionError {
665        impl_diagnostic_from_method_on_field!(expr, source_loc);
666    }
667}
668
669/// Errors possible from `RestrictedExpr::from_str()`
670//
671// This is NOT a publicly exported error type.
672#[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
673pub enum RestrictedExpressionParseError {
674    /// Failed to parse the expression
675    #[error(transparent)]
676    #[diagnostic(transparent)]
677    Parse(#[from] ParseErrors),
678    /// Parsed successfully as an expression, but failed to construct a
679    /// restricted expression, for the reason indicated in the underlying error
680    #[error(transparent)]
681    #[diagnostic(transparent)]
682    InvalidRestrictedExpression(#[from] RestrictedExpressionError),
683}
684
685#[cfg(test)]
686mod test {
687    use super::*;
688    use crate::ast::expression_construction_errors;
689    use crate::parser::err::{ParseError, ToASTError, ToASTErrorKind};
690    use crate::parser::Loc;
691    use std::str::FromStr;
692    use std::sync::Arc;
693
694    #[test]
695    fn duplicate_key() {
696        // duplicate key is an error when mapped to values of different types
697        assert_eq!(
698            RestrictedExpr::record([
699                ("foo".into(), RestrictedExpr::val(37),),
700                ("foo".into(), RestrictedExpr::val("hello"),),
701            ]),
702            Err(expression_construction_errors::DuplicateKeyError {
703                key: "foo".into(),
704                context: "in record literal",
705            }
706            .into())
707        );
708
709        // duplicate key is an error when mapped to different values of same type
710        assert_eq!(
711            RestrictedExpr::record([
712                ("foo".into(), RestrictedExpr::val(37),),
713                ("foo".into(), RestrictedExpr::val(101),),
714            ]),
715            Err(expression_construction_errors::DuplicateKeyError {
716                key: "foo".into(),
717                context: "in record literal",
718            }
719            .into())
720        );
721
722        // duplicate key is an error when mapped to the same value multiple times
723        assert_eq!(
724            RestrictedExpr::record([
725                ("foo".into(), RestrictedExpr::val(37),),
726                ("foo".into(), RestrictedExpr::val(37),),
727            ]),
728            Err(expression_construction_errors::DuplicateKeyError {
729                key: "foo".into(),
730                context: "in record literal",
731            }
732            .into())
733        );
734
735        // duplicate key is an error even when other keys appear in between
736        assert_eq!(
737            RestrictedExpr::record([
738                ("bar".into(), RestrictedExpr::val(-3),),
739                ("foo".into(), RestrictedExpr::val(37),),
740                ("spam".into(), RestrictedExpr::val("eggs"),),
741                ("foo".into(), RestrictedExpr::val(37),),
742                ("eggs".into(), RestrictedExpr::val("spam"),),
743            ]),
744            Err(expression_construction_errors::DuplicateKeyError {
745                key: "foo".into(),
746                context: "in record literal",
747            }
748            .into())
749        );
750
751        // duplicate key is also an error when parsing from string
752        let str = r#"{ foo: 37, bar: "hi", foo: 101 }"#;
753        assert_eq!(
754            RestrictedExpr::from_str(str),
755            Err(RestrictedExpressionParseError::Parse(
756                ParseErrors::singleton(ParseError::ToAST(ToASTError::new(
757                    ToASTErrorKind::ExpressionConstructionError(
758                        expression_construction_errors::DuplicateKeyError {
759                            key: "foo".into(),
760                            context: "in record literal",
761                        }
762                        .into()
763                    ),
764                    Loc::new(0..32, Arc::from(str))
765                )))
766            )),
767        )
768    }
769}