cedar_policy_core/ast/
request.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::entities::json::{
18    ContextJsonDeserializationError, ContextJsonParser, NullContextSchema,
19};
20use crate::evaluator::{EvaluationError, RestrictedEvaluator};
21use crate::extensions::Extensions;
22use crate::parser::Loc;
23use miette::Diagnostic;
24use smol_str::{SmolStr, ToSmolStr};
25use std::collections::{BTreeMap, HashMap};
26use std::sync::Arc;
27use thiserror::Error;
28
29use super::{
30    BorrowedRestrictedExpr, BoundedDisplay, EntityType, EntityUID, Expr, ExprKind,
31    ExpressionConstructionError, PartialValue, RestrictedExpr, Unknown, Value, ValueKind, Var,
32};
33
34/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
35#[derive(Debug, Clone)]
36pub struct Request {
37    /// Principal associated with the request
38    pub(crate) principal: EntityUIDEntry,
39
40    /// Action associated with the request
41    pub(crate) action: EntityUIDEntry,
42
43    /// Resource associated with the request
44    pub(crate) resource: EntityUIDEntry,
45
46    /// Context associated with the request.
47    /// `None` means that variable will result in a residual for partial evaluation.
48    pub(crate) context: Option<Context>,
49}
50
51/// Represents the principal type, resource type, and action UID.
52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53#[cfg_attr(
54    feature = "entity-manifest",
55    derive(serde::Serialize, serde::Deserialize)
56)]
57pub struct RequestType {
58    /// Principal type
59    pub principal: EntityType,
60    /// Action type
61    pub action: EntityUID,
62    /// Resource type
63    pub resource: EntityType,
64}
65
66/// An entry in a request for a Entity UID.
67/// It may either be a concrete EUID
68/// or an unknown in the case of partial evaluation
69#[derive(Debug, Clone)]
70pub enum EntityUIDEntry {
71    /// A concrete EntityUID
72    Known {
73        /// The concrete `EntityUID`
74        euid: Arc<EntityUID>,
75        /// Source location associated with the `EntityUIDEntry`, if any
76        loc: Option<Loc>,
77    },
78    /// An EntityUID left as unknown for partial evaluation
79    Unknown {
80        /// The type of the unknown EntityUID, if known.
81        ty: Option<EntityType>,
82
83        /// Source location associated with the `EntityUIDEntry`, if any
84        loc: Option<Loc>,
85    },
86}
87
88impl From<EntityUID> for EntityUIDEntry {
89    fn from(euid: EntityUID) -> Self {
90        Self::Known {
91            euid: Arc::new(euid.clone()),
92            loc: match &euid {
93                EntityUID::EntityUID(euid) => euid.loc(),
94                #[cfg(feature = "tolerant-ast")]
95                EntityUID::Error => None,
96            },
97        }
98    }
99}
100
101impl EntityUIDEntry {
102    /// Evaluate the entry to either:
103    /// A value, if the entry is concrete
104    /// An unknown corresponding to the passed `var`
105    pub fn evaluate(&self, var: Var) -> PartialValue {
106        match self {
107            EntityUIDEntry::Known { euid, loc } => {
108                Value::new(Arc::unwrap_or_clone(Arc::clone(euid)), loc.clone()).into()
109            }
110            EntityUIDEntry::Unknown { ty: None, loc } => {
111                Expr::unknown(Unknown::new_untyped(var.to_smolstr()))
112                    .with_maybe_source_loc(loc.clone())
113                    .into()
114            }
115            EntityUIDEntry::Unknown {
116                ty: Some(known_type),
117                loc,
118            } => Expr::unknown(Unknown::new_with_type(
119                var.to_smolstr(),
120                super::Type::Entity {
121                    ty: known_type.clone(),
122                },
123            ))
124            .with_maybe_source_loc(loc.clone())
125            .into(),
126        }
127    }
128
129    /// Create an entry with a concrete EntityUID and the given source location
130    pub fn known(euid: EntityUID, loc: Option<Loc>) -> Self {
131        Self::Known {
132            euid: Arc::new(euid),
133            loc,
134        }
135    }
136
137    /// Create an entry with an entirely unknown EntityUID
138    pub fn unknown() -> Self {
139        Self::Unknown {
140            ty: None,
141            loc: None,
142        }
143    }
144
145    /// Create an entry with an unknown EntityUID but known EntityType
146    pub fn unknown_with_type(ty: EntityType, loc: Option<Loc>) -> Self {
147        Self::Unknown { ty: Some(ty), loc }
148    }
149
150    /// Get the UID of the entry, or `None` if it is unknown (partial evaluation)
151    pub fn uid(&self) -> Option<&EntityUID> {
152        match self {
153            Self::Known { euid, .. } => Some(euid),
154            Self::Unknown { .. } => None,
155        }
156    }
157
158    /// Get the type of the entry, or `None` if it is unknown (partial evaluation with no type annotation)
159    pub fn get_type(&self) -> Option<&EntityType> {
160        match self {
161            Self::Known { euid, .. } => Some(euid.entity_type()),
162            Self::Unknown { ty, .. } => ty.as_ref(),
163        }
164    }
165}
166
167impl Request {
168    /// Default constructor.
169    ///
170    /// If `schema` is provided, this constructor validates that this `Request`
171    /// complies with the given `schema`.
172    pub fn new<S: RequestSchema>(
173        principal: (EntityUID, Option<Loc>),
174        action: (EntityUID, Option<Loc>),
175        resource: (EntityUID, Option<Loc>),
176        context: Context,
177        schema: Option<&S>,
178        extensions: &Extensions<'_>,
179    ) -> Result<Self, S::Error> {
180        let req = Self {
181            principal: EntityUIDEntry::known(principal.0, principal.1),
182            action: EntityUIDEntry::known(action.0, action.1),
183            resource: EntityUIDEntry::known(resource.0, resource.1),
184            context: Some(context),
185        };
186        if let Some(schema) = schema {
187            schema.validate_request(&req, extensions)?;
188        }
189        Ok(req)
190    }
191
192    /// Create a new `Request` with potentially unknown (for partial eval) variables.
193    ///
194    /// If `schema` is provided, this constructor validates that this `Request`
195    /// complies with the given `schema` (at least to the extent that we can
196    /// validate with the given information)
197    pub fn new_with_unknowns<S: RequestSchema>(
198        principal: EntityUIDEntry,
199        action: EntityUIDEntry,
200        resource: EntityUIDEntry,
201        context: Option<Context>,
202        schema: Option<&S>,
203        extensions: &Extensions<'_>,
204    ) -> Result<Self, S::Error> {
205        let req = Self {
206            principal,
207            action,
208            resource,
209            context,
210        };
211        if let Some(schema) = schema {
212            schema.validate_request(&req, extensions)?;
213        }
214        Ok(req)
215    }
216
217    /// Create a new `Request` with potentially unknown (for partial eval) variables/context
218    /// and without schema validation.
219    pub fn new_unchecked(
220        principal: EntityUIDEntry,
221        action: EntityUIDEntry,
222        resource: EntityUIDEntry,
223        context: Option<Context>,
224    ) -> Self {
225        Self {
226            principal,
227            action,
228            resource,
229            context,
230        }
231    }
232
233    /// Get the principal associated with the request
234    pub fn principal(&self) -> &EntityUIDEntry {
235        &self.principal
236    }
237
238    /// Get the action associated with the request
239    pub fn action(&self) -> &EntityUIDEntry {
240        &self.action
241    }
242
243    /// Get the resource associated with the request
244    pub fn resource(&self) -> &EntityUIDEntry {
245        &self.resource
246    }
247
248    /// Get the context associated with the request
249    /// Returning `None` means the variable is unknown, and will result in a residual expression
250    pub fn context(&self) -> Option<&Context> {
251        self.context.as_ref()
252    }
253
254    /// Get the request types that correspond to this request.
255    /// This includes the types of the principal, action, and resource.
256    /// [`RequestType`] is used by the entity manifest.
257    /// The context type is implied by the action's type.
258    /// Returns `None` if the request is not fully concrete.
259    pub fn to_request_type(&self) -> Option<RequestType> {
260        Some(RequestType {
261            principal: self.principal().uid()?.entity_type().clone(),
262            action: self.action().uid()?.clone(),
263            resource: self.resource().uid()?.entity_type().clone(),
264        })
265    }
266}
267
268impl std::fmt::Display for Request {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        let display_euid = |maybe_euid: &EntityUIDEntry| match maybe_euid {
271            EntityUIDEntry::Known { euid, .. } => format!("{euid}"),
272            EntityUIDEntry::Unknown { ty: None, .. } => "unknown".to_string(),
273            EntityUIDEntry::Unknown {
274                ty: Some(known_type),
275                ..
276            } => format!("unknown of type {known_type}"),
277        };
278        write!(
279            f,
280            "request with principal {}, action {}, resource {}, and context {}",
281            display_euid(&self.principal),
282            display_euid(&self.action),
283            display_euid(&self.resource),
284            match &self.context {
285                Some(x) => format!("{x}"),
286                None => "unknown".to_string(),
287            }
288        )
289    }
290}
291
292/// `Context` field of a `Request`
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum Context {
295    /// The context is a concrete value.
296    Value(Arc<BTreeMap<SmolStr, Value>>),
297    /// The context is a residual expression, containing some unknown value in
298    /// the record attributes.
299    /// INVARIANT(restricted): Each `Expr` in this map must be a `RestrictedExpr`.
300    /// INVARIANT(unknown): At least one `Expr` must contain an `unknown`.
301    RestrictedResidual(Arc<BTreeMap<SmolStr, Expr>>),
302}
303
304impl Context {
305    /// Create an empty `Context`
306    pub fn empty() -> Self {
307        Self::Value(Arc::new(BTreeMap::new()))
308    }
309
310    /// Create a `Context` from a `PartialValue` without checking that the
311    /// residual is a restricted expression.  This function does check that the
312    /// value or residual is a record and returns `Err` when it is not.
313    ///
314    /// INVARIANT: if `value` is a residual, then it must be a valid restricted expression.
315    fn from_restricted_partial_val_unchecked(
316        value: PartialValue,
317    ) -> Result<Self, ContextCreationError> {
318        match value {
319            PartialValue::Value(v) => {
320                if let ValueKind::Record(attrs) = v.value {
321                    Ok(Context::Value(attrs))
322                } else {
323                    Err(ContextCreationError::not_a_record(v.into()))
324                }
325            }
326            PartialValue::Residual(e) => {
327                if let ExprKind::Record(attrs) = e.expr_kind() {
328                    // From the invariant on `PartialValue::Residual`, there is
329                    // an unknown in `e`. It is a record, so there must be an
330                    // unknown in one of the attributes expressions, satisfying
331                    // INVARIANT(unknown). From the invariant on this function,
332                    // `e` is a valid restricted expression, satisfying
333                    // INVARIANT(restricted).
334                    Ok(Context::RestrictedResidual(attrs.clone()))
335                } else {
336                    Err(ContextCreationError::not_a_record(e))
337                }
338            }
339        }
340    }
341
342    /// Create a `Context` from a `RestrictedExpr`, which must be a `Record`.
343    ///
344    /// `extensions` provides the `Extensions` which should be active for
345    /// evaluating the `RestrictedExpr`.
346    pub fn from_expr(
347        expr: BorrowedRestrictedExpr<'_>,
348        extensions: &Extensions<'_>,
349    ) -> Result<Self, ContextCreationError> {
350        match expr.expr_kind() {
351            ExprKind::Record { .. } => {
352                let evaluator = RestrictedEvaluator::new(extensions);
353                let pval = evaluator.partial_interpret(expr)?;
354                // The invariant on `from_restricted_partial_val_unchecked`
355                // is satisfied because `expr` is a restricted expression,
356                // and must still be restricted after `partial_interpret`.
357                // The function call cannot return `Err` because `expr` is a
358                // record, and partially evaluating a record expression will
359                // yield a record expression or a record value.
360                // PANIC SAFETY: See above
361                #[allow(clippy::expect_used)]
362                Ok(Self::from_restricted_partial_val_unchecked(pval).expect(
363                    "`from_restricted_partial_val_unchecked` should succeed when called on a record.",
364                ))
365            }
366            _ => Err(ContextCreationError::not_a_record(expr.to_owned().into())),
367        }
368    }
369
370    /// Create a `Context` from a map of key to `RestrictedExpr`, or a Vec of
371    /// `(key, RestrictedExpr)` pairs, or any other iterator of `(key, RestrictedExpr)` pairs
372    ///
373    /// `extensions` provides the `Extensions` which should be active for
374    /// evaluating the `RestrictedExpr`.
375    pub fn from_pairs(
376        pairs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
377        extensions: &Extensions<'_>,
378    ) -> Result<Self, ContextCreationError> {
379        match RestrictedExpr::record(pairs) {
380            Ok(record) => Self::from_expr(record.as_borrowed(), extensions),
381            Err(ExpressionConstructionError::DuplicateKey(err)) => Err(
382                ExpressionConstructionError::DuplicateKey(err.with_context("in context")).into(),
383            ),
384        }
385    }
386
387    /// Create a `Context` from a string containing JSON (which must be a JSON
388    /// object, not any other JSON type, or you will get an error here).
389    /// JSON here must use the `__entity` and `__extn` escapes for entity
390    /// references, extension values, etc.
391    ///
392    /// For schema-based parsing, use `ContextJsonParser`.
393    pub fn from_json_str(json: &str) -> Result<Self, ContextJsonDeserializationError> {
394        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
395            .from_json_str(json)
396    }
397
398    /// Create a `Context` from a `serde_json::Value` (which must be a JSON
399    /// object, not any other JSON type, or you will get an error here).
400    /// JSON here must use the `__entity` and `__extn` escapes for entity
401    /// references, extension values, etc.
402    ///
403    /// For schema-based parsing, use `ContextJsonParser`.
404    pub fn from_json_value(
405        json: serde_json::Value,
406    ) -> Result<Self, ContextJsonDeserializationError> {
407        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
408            .from_json_value(json)
409    }
410
411    /// Create a `Context` from a JSON file.  The JSON file must contain a JSON
412    /// object, not any other JSON type, or you will get an error here.
413    /// JSON here must use the `__entity` and `__extn` escapes for entity
414    /// references, extension values, etc.
415    ///
416    /// For schema-based parsing, use `ContextJsonParser`.
417    pub fn from_json_file(
418        json: impl std::io::Read,
419    ) -> Result<Self, ContextJsonDeserializationError> {
420        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
421            .from_json_file(json)
422    }
423
424    /// Get the number of keys in this `Context`.
425    pub fn num_keys(&self) -> usize {
426        match self {
427            Context::Value(record) => record.len(),
428            Context::RestrictedResidual(record) => record.len(),
429        }
430    }
431
432    /// Private helper function to implement `into_iter()` for `Context`.
433    /// Gets an iterator over the (key, value) pairs in the `Context`, cloning
434    /// only if necessary.
435    ///
436    /// Note that some error messages rely on this function returning keys in
437    /// sorted order, or else the error message will not be fully deterministic.
438    fn into_pairs(self) -> Box<dyn Iterator<Item = (SmolStr, RestrictedExpr)>> {
439        match self {
440            Context::Value(record) => Box::new(
441                Arc::unwrap_or_clone(record)
442                    .into_iter()
443                    .map(|(k, v)| (k, RestrictedExpr::from(v))),
444            ),
445            Context::RestrictedResidual(record) => Box::new(
446                Arc::unwrap_or_clone(record)
447                    .into_iter()
448                    // By INVARIANT(restricted), all attributes expressions are
449                    // restricted expressions.
450                    .map(|(k, v)| (k, RestrictedExpr::new_unchecked(v))),
451            ),
452        }
453    }
454
455    /// Substitute unknowns with concrete values in this context. If this is
456    /// already a `Context::Value`, then this returns `self` unchanged and will
457    /// not error. Otherwise delegate to [`Expr::substitute`].
458    pub fn substitute(self, mapping: &HashMap<SmolStr, Value>) -> Result<Self, EvaluationError> {
459        match self {
460            Context::RestrictedResidual(residual_context) => {
461                // From Invariant(Restricted), `residual_context` contains only
462                // restricted expressions, so `Expr::record_arc` of the attributes
463                // will also be a restricted expression. This doesn't change after
464                // substitution, so we know `expr` must be a restricted expression.
465                let expr = Expr::record_arc(residual_context).substitute(mapping);
466                let expr = BorrowedRestrictedExpr::new_unchecked(&expr);
467
468                let extns = Extensions::all_available();
469                let eval = RestrictedEvaluator::new(extns);
470                let partial_value = eval.partial_interpret(expr)?;
471
472                // The invariant on `from_restricted_partial_val_unchecked`
473                // is satisfied because `expr` is restricted and must still be
474                // restricted after `partial_interpret`.
475                // The function call cannot fail because because `expr` was
476                // constructed as a record, and substitution and partial
477                // evaluation does not change this.
478                // PANIC SAFETY: See above
479                #[allow(clippy::expect_used)]
480                Ok(
481                    Self::from_restricted_partial_val_unchecked(partial_value).expect(
482                        "`from_restricted_partial_val_unchecked` should succeed when called on a record.",
483                    ),
484                )
485            }
486            Context::Value(_) => Ok(self),
487        }
488    }
489}
490
491/// Utilities for implementing `IntoIterator` for `Context`
492mod iter {
493    use super::*;
494
495    /// `IntoIter` iterator for `Context`
496    pub struct IntoIter(pub(super) Box<dyn Iterator<Item = (SmolStr, RestrictedExpr)>>);
497
498    impl std::fmt::Debug for IntoIter {
499        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500            write!(f, "IntoIter(<context>)")
501        }
502    }
503
504    impl Iterator for IntoIter {
505        type Item = (SmolStr, RestrictedExpr);
506
507        fn next(&mut self) -> Option<Self::Item> {
508            self.0.next()
509        }
510    }
511}
512
513impl IntoIterator for Context {
514    type Item = (SmolStr, RestrictedExpr);
515    type IntoIter = iter::IntoIter;
516
517    fn into_iter(self) -> Self::IntoIter {
518        iter::IntoIter(self.into_pairs())
519    }
520}
521
522impl From<Context> for RestrictedExpr {
523    fn from(value: Context) -> Self {
524        match value {
525            Context::Value(attrs) => Value::record_arc(attrs, None).into(),
526            Context::RestrictedResidual(attrs) => {
527                // By INVARIANT(restricted), all attributes expressions are
528                // restricted expressions, so the result of `record_arc` will be
529                // a restricted expression.
530                RestrictedExpr::new_unchecked(Expr::record_arc(attrs))
531            }
532        }
533    }
534}
535
536impl From<Context> for PartialValue {
537    fn from(ctx: Context) -> PartialValue {
538        match ctx {
539            Context::Value(attrs) => Value::record_arc(attrs, None).into(),
540            Context::RestrictedResidual(attrs) => {
541                // A `PartialValue::Residual` must contain an unknown in the
542                // expression. By INVARIANT(unknown), at least one expr in
543                // `attrs` contains an unknown, so the `record_arc` expression
544                // contains at least one unknown.
545                PartialValue::Residual(Expr::record_arc(attrs))
546            }
547        }
548    }
549}
550
551impl std::default::Default for Context {
552    fn default() -> Context {
553        Context::empty()
554    }
555}
556
557impl std::fmt::Display for Context {
558    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559        write!(f, "{}", PartialValue::from(self.clone()))
560    }
561}
562
563impl BoundedDisplay for Context {
564    fn fmt(&self, f: &mut impl std::fmt::Write, n: Option<usize>) -> std::fmt::Result {
565        BoundedDisplay::fmt(&PartialValue::from(self.clone()), f, n)
566    }
567}
568
569/// Errors while trying to create a `Context`
570#[derive(Debug, Diagnostic, Error)]
571pub enum ContextCreationError {
572    /// Tried to create a `Context` out of something other than a record
573    #[error(transparent)]
574    #[diagnostic(transparent)]
575    NotARecord(#[from] context_creation_errors::NotARecord),
576    /// Error evaluating the expression given for the `Context`
577    #[error(transparent)]
578    #[diagnostic(transparent)]
579    Evaluation(#[from] EvaluationError),
580    /// Error constructing a record for the `Context`.
581    /// Only returned by `Context::from_pairs()` and `Context::merge()`
582    #[error(transparent)]
583    #[diagnostic(transparent)]
584    ExpressionConstruction(#[from] ExpressionConstructionError),
585}
586
587impl ContextCreationError {
588    pub(crate) fn not_a_record(expr: Expr) -> Self {
589        Self::NotARecord(context_creation_errors::NotARecord {
590            expr: Box::new(expr),
591        })
592    }
593}
594
595/// Error subtypes for [`ContextCreationError`]
596pub mod context_creation_errors {
597    use super::Expr;
598    use crate::impl_diagnostic_from_method_on_field;
599    use miette::Diagnostic;
600    use thiserror::Error;
601
602    /// Error type for an expression that needed to be a record, but is not
603    //
604    // CAUTION: this type is publicly exported in `cedar-policy`.
605    // Don't make fields `pub`, don't make breaking changes, and use caution
606    // when adding public methods.
607    #[derive(Debug, Error)]
608    #[error("expression is not a record: {expr}")]
609    pub struct NotARecord {
610        /// Expression which is not a record
611        pub(super) expr: Box<Expr>,
612    }
613
614    // custom impl of `Diagnostic`: take source location from the `expr` field's `.source_loc()` method
615    impl Diagnostic for NotARecord {
616        impl_diagnostic_from_method_on_field!(expr, source_loc);
617    }
618}
619
620/// Trait for schemas capable of validating `Request`s
621pub trait RequestSchema {
622    /// Error type returned when a request fails validation
623    type Error: miette::Diagnostic;
624    /// Validate the given `request`, returning `Err` if it fails validation
625    fn validate_request(
626        &self,
627        request: &Request,
628        extensions: &Extensions<'_>,
629    ) -> Result<(), Self::Error>;
630
631    /// Validate the given `context`, returning `Err` if it fails validation
632    fn validate_context<'a>(
633        &self,
634        context: &Context,
635        action: &EntityUID,
636        extensions: &Extensions<'a>,
637    ) -> std::result::Result<(), Self::Error>;
638
639    /// Validate the scope variables, returning `Err` if it fails validation
640    fn validate_scope_variables(
641        &self,
642        principal: Option<&EntityUID>,
643        action: Option<&EntityUID>,
644        resource: Option<&EntityUID>,
645    ) -> std::result::Result<(), Self::Error>;
646}
647
648/// A `RequestSchema` that does no validation and always reports a passing result
649#[derive(Debug, Clone)]
650pub struct RequestSchemaAllPass;
651impl RequestSchema for RequestSchemaAllPass {
652    type Error = Infallible;
653    fn validate_request(
654        &self,
655        _request: &Request,
656        _extensions: &Extensions<'_>,
657    ) -> Result<(), Self::Error> {
658        Ok(())
659    }
660
661    fn validate_context<'a>(
662        &self,
663        _context: &Context,
664        _action: &EntityUID,
665        _extensions: &Extensions<'a>,
666    ) -> std::result::Result<(), Self::Error> {
667        Ok(())
668    }
669
670    fn validate_scope_variables(
671        &self,
672        _principal: Option<&EntityUID>,
673        _action: Option<&EntityUID>,
674        _resource: Option<&EntityUID>,
675    ) -> std::result::Result<(), Self::Error> {
676        Ok(())
677    }
678}
679
680/// Wrapper around `std::convert::Infallible` which also implements
681/// `miette::Diagnostic`
682#[derive(Debug, Diagnostic, Error)]
683#[error(transparent)]
684pub struct Infallible(pub std::convert::Infallible);
685
686#[cfg(test)]
687mod test {
688    use super::*;
689    use cool_asserts::assert_matches;
690
691    #[test]
692    fn test_json_from_str_non_record() {
693        assert_matches!(
694            Context::from_expr(RestrictedExpr::val("1").as_borrowed(), Extensions::none()),
695            Err(ContextCreationError::NotARecord { .. })
696        );
697        assert_matches!(
698            Context::from_json_str("1"),
699            Err(ContextJsonDeserializationError::ContextCreation(
700                ContextCreationError::NotARecord { .. }
701            ))
702        );
703    }
704}