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