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}