cedar_policy_core/entities/json/
err.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 std::fmt::Display;
18
19use super::SchemaType;
20use crate::ast::{
21    BorrowedRestrictedExpr, EntityAttrEvaluationError, EntityUID, Expr, ExprKind, PolicyID,
22    RestrictedExpr, RestrictedExpressionError, Type,
23};
24use crate::entities::conformance::err::EntitySchemaConformanceError;
25use crate::entities::{Name, ReservedNameError};
26use crate::extensions::ExtensionFunctionLookupError;
27use crate::parser::err::ParseErrors;
28use either::Either;
29use itertools::Itertools;
30use miette::Diagnostic;
31use smol_str::{SmolStr, ToSmolStr};
32use thiserror::Error;
33
34/// Escape kind
35#[derive(Debug)]
36pub enum EscapeKind {
37    /// Escape `__entity`
38    Entity,
39    /// Escape `__extn`
40    Extension,
41}
42
43impl Display for EscapeKind {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::Entity => write!(f, "__entity"),
47            Self::Extension => write!(f, "__extn"),
48        }
49    }
50}
51
52/// Errors thrown during deserialization from JSON
53//
54// CAUTION: this type is publicly exported in `cedar-policy`.
55// Don't make fields `pub`, don't make breaking changes, and use caution
56// when adding public methods.
57#[derive(Debug, Diagnostic, Error)]
58#[non_exhaustive]
59pub enum JsonDeserializationError {
60    /// Error thrown by the `serde_json` crate
61    #[error(transparent)]
62    #[diagnostic(transparent)]
63    Serde(#[from] JsonError),
64    /// Contents of an escape failed to parse.
65    #[error(transparent)]
66    #[diagnostic(transparent)]
67    ParseEscape(ParseEscape),
68    /// Restricted expression error
69    #[error(transparent)]
70    #[diagnostic(transparent)]
71    RestrictedExpressionError(#[from] RestrictedExpressionError),
72    /// A field that needs to be a literal entity reference, was some other JSON value
73    #[error(transparent)]
74    #[diagnostic(transparent)]
75    ExpectedLiteralEntityRef(ExpectedLiteralEntityRef),
76    /// A field that needs to be an extension value, was some other JSON value
77    #[error(transparent)]
78    #[diagnostic(transparent)]
79    ExpectedExtnValue(ExpectedExtnValue),
80    /// Parents of actions should be actions, but this action has a non-action parent
81    #[error(transparent)]
82    #[diagnostic(transparent)]
83    ActionParentIsNotAction(ActionParentIsNotAction),
84    /// Schema-based parsing needed an implicit extension constructor, but no suitable
85    /// constructor was found
86    #[error(transparent)]
87    #[diagnostic(transparent)]
88    MissingImpliedConstructor(MissingImpliedConstructor),
89    /// Incorrect number of arguments of an extension function are provided
90    /// during schema-based parsing
91    #[error(transparent)]
92    #[diagnostic(transparent)]
93    IncorrectNumOfArguments(IncorrectNumOfArguments),
94    /// Failed to look up an extension function
95    #[error(transparent)]
96    #[diagnostic(transparent)]
97    FailedExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
98    /// The same key appears two or more times in a single record
99    #[error(transparent)]
100    #[diagnostic(transparent)]
101    DuplicateKey(DuplicateKey),
102    /// Error when evaluating an entity attribute
103    #[error(transparent)]
104    #[diagnostic(transparent)]
105    EntityAttributeEvaluation(#[from] EntityAttrEvaluationError),
106    /// During schema-based parsing, encountered an entity which does not
107    /// conform to the schema.
108    ///
109    /// This error contains the `Entity` analogues some of the other errors
110    /// listed below, among other things.
111    #[error(transparent)]
112    #[diagnostic(transparent)]
113    EntitySchemaConformance(#[from] EntitySchemaConformanceError),
114    /// During schema-based parsing, encountered this attribute on a record, but
115    /// that attribute shouldn't exist on that record
116    #[error(transparent)]
117    #[diagnostic(transparent)]
118    UnexpectedRecordAttr(UnexpectedRecordAttr),
119    /// During schema-based parsing, didn't encounter this attribute of a
120    /// record, but that attribute should have existed
121    #[error(transparent)]
122    #[diagnostic(transparent)]
123    MissingRequiredRecordAttr(MissingRequiredRecordAttr),
124    /// During schema-based parsing, found a different type than the schema indicated.
125    ///
126    /// (This is used in all cases except inside entity attributes; type mismatches in
127    /// entity attributes are reported as `Self::EntitySchemaConformance`. As of
128    /// this writing, that means this should only be used for schema-based
129    /// parsing of the `Context`.)
130    #[error(transparent)]
131    #[diagnostic(transparent)]
132    TypeMismatch(TypeMismatch),
133    /// When trying to deserialize an AST with an error in it - this should fail
134    #[cfg(feature = "tolerant-ast")]
135    #[error("Unable to deserialize an AST Error node")]
136    #[diagnostic(help("AST error node indicates that the expression has failed to parse"))]
137    ASTErrorNode,
138    /// Raised when a JsonValue contains the no longer supported `__expr` escape
139    #[error("{0}, the `__expr` escape is no longer supported")]
140    #[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))]
141    ExprTag(Box<JsonDeserializationErrorContext>),
142    /// Raised when the input JSON contains a `null`
143    #[error("{0}, found a `null`; JSON `null`s are not allowed in Cedar")]
144    Null(Box<JsonDeserializationErrorContext>),
145    /// Returned when a name contains `__cedar`
146    #[error(transparent)]
147    #[diagnostic(transparent)]
148    ReservedName(#[from] ReservedNameError),
149    /// Never returned as of 4.2.0 (entity tags are now stable), but this error
150    /// variant not removed because that would be a breaking change on this
151    /// publicly-exported type.
152    #[deprecated(
153        since = "4.2.0",
154        note = "entity-tags is now stable and fully supported, so this error never occurs"
155    )]
156    #[error("entity tags are not supported in this build; to use entity tags, you must enable the `entity-tags` experimental feature")]
157    UnsupportedEntityTags,
158}
159
160impl JsonDeserializationError {
161    pub(crate) fn parse_escape(
162        kind: EscapeKind,
163        value: impl Into<String>,
164        errs: ParseErrors,
165    ) -> Self {
166        Self::ParseEscape(ParseEscape {
167            kind,
168            value: value.into(),
169            errs,
170        })
171    }
172
173    pub(crate) fn expected_entity_ref(
174        ctx: JsonDeserializationErrorContext,
175        got: Either<serde_json::Value, Expr>,
176    ) -> Self {
177        Self::ExpectedLiteralEntityRef(ExpectedLiteralEntityRef {
178            ctx: Box::new(ctx),
179            got: Box::new(got),
180        })
181    }
182
183    pub(crate) fn action_parent_is_not_action(uid: EntityUID, parent: EntityUID) -> Self {
184        Self::ActionParentIsNotAction(ActionParentIsNotAction { uid, parent })
185    }
186
187    pub(crate) fn missing_implied_constructor(
188        ctx: JsonDeserializationErrorContext,
189        return_type: SchemaType,
190    ) -> Self {
191        Self::MissingImpliedConstructor(MissingImpliedConstructor {
192            ctx: Box::new(ctx),
193            return_type: Box::new(return_type),
194        })
195    }
196
197    pub(crate) fn incorrect_num_of_arguments(
198        expected_arg_num: usize,
199        provided_arg_num: usize,
200        func_name: &str,
201    ) -> Self {
202        Self::IncorrectNumOfArguments(IncorrectNumOfArguments {
203            expected_arg_num,
204            provided_arg_num,
205            func_name: func_name.to_smolstr(),
206        })
207    }
208
209    pub(crate) fn duplicate_key(
210        ctx: JsonDeserializationErrorContext,
211        key: impl Into<SmolStr>,
212    ) -> Self {
213        Self::DuplicateKey(DuplicateKey {
214            ctx: Box::new(ctx),
215            key: key.into(),
216        })
217    }
218
219    pub(crate) fn unexpected_record_attr(
220        ctx: JsonDeserializationErrorContext,
221        record_attr: impl Into<SmolStr>,
222    ) -> Self {
223        Self::UnexpectedRecordAttr(UnexpectedRecordAttr {
224            ctx: Box::new(ctx),
225            record_attr: record_attr.into(),
226        })
227    }
228
229    pub(crate) fn missing_required_record_attr(
230        ctx: JsonDeserializationErrorContext,
231        record_attr: impl Into<SmolStr>,
232    ) -> Self {
233        Self::MissingRequiredRecordAttr(MissingRequiredRecordAttr {
234            ctx: Box::new(ctx),
235            record_attr: record_attr.into(),
236        })
237    }
238
239    pub(crate) fn type_mismatch(
240        ctx: JsonDeserializationErrorContext,
241        err: TypeMismatchError,
242    ) -> Self {
243        Self::TypeMismatch(TypeMismatch {
244            ctx: Box::new(ctx),
245            err,
246        })
247    }
248}
249
250#[derive(Debug, Error, Diagnostic)]
251#[error("{ctx}, {err}")]
252/// General error for type mismatches
253pub struct TypeMismatch {
254    /// Context of this error, which will be something other than `EntityAttribute`.
255    /// (Type mismatches in entity attributes are reported as
256    /// `Self::EntitySchemaConformance`.)
257    ctx: Box<JsonDeserializationErrorContext>,
258    /// Underlying error
259    #[diagnostic(transparent)]
260    err: TypeMismatchError,
261}
262
263#[derive(Debug, Error, Diagnostic)]
264#[error("{}, expected the record to have an attribute `{}`, but it does not", .ctx, .record_attr)]
265/// Error type for a record missing a required attr
266pub struct MissingRequiredRecordAttr {
267    /// Context of this error
268    ctx: Box<JsonDeserializationErrorContext>,
269    /// Name of the (Record) attribute which was expected
270    record_attr: SmolStr,
271}
272
273#[derive(Debug, Diagnostic, Error)]
274#[error("{}, record attribute `{}` should not exist according to the schema", .ctx, .record_attr)]
275/// Error type for record attributes that should not exist
276pub struct UnexpectedRecordAttr {
277    /// Context of this error
278    ctx: Box<JsonDeserializationErrorContext>,
279    /// Name of the (Record) attribute which was unexpected
280    record_attr: SmolStr,
281}
282
283#[derive(Debug, Error, Diagnostic)]
284#[error("{}, duplicate key `{}` in record", .ctx, .key)]
285/// Error type for records having duplicate keys
286pub struct DuplicateKey {
287    /// Context of this error
288    ctx: Box<JsonDeserializationErrorContext>,
289    /// The key that appeared two or more times
290    key: SmolStr,
291}
292
293#[derive(Debug, Error, Diagnostic)]
294#[error("{}, missing extension constructor for {}", .ctx, .return_type)]
295#[diagnostic(help("expected a value of type {} because of the schema", .return_type))]
296/// Error type for missing extension constructors
297pub struct MissingImpliedConstructor {
298    /// Context of this error
299    ctx: Box<JsonDeserializationErrorContext>,
300    /// return type of the constructor we were looking for
301    return_type: Box<SchemaType>,
302}
303
304#[derive(Debug, Error, Diagnostic)]
305#[error("expected {expected_arg_num} arguments for function {func_name} but {provided_arg_num} arguments are provided")]
306/// Error type for incorrect extension function argument number
307pub struct IncorrectNumOfArguments {
308    /// Expected number of arguments
309    expected_arg_num: usize,
310    /// Provided number of argument
311    provided_arg_num: usize,
312    /// Function name
313    func_name: SmolStr,
314}
315
316#[derive(Debug, Error, Diagnostic)]
317#[error("action `{}` has a non-action parent `{}`", .uid, .parent)]
318#[diagnostic(help("parents of actions need to have type `Action` themselves, perhaps namespaced"))]
319/// Error type for action  parents not having type `Action`
320pub struct ActionParentIsNotAction {
321    /// Action entity that had the invalid parent
322    uid: EntityUID,
323    /// Parent that is invalid
324    parent: EntityUID,
325}
326
327#[derive(Debug, Error, Diagnostic)]
328#[error("failed to parse escape `{kind}`: {value}, errors: {errs}")]
329#[diagnostic(help("{}", match .kind {
330        EscapeKind::Entity => r#"an __entity escape should have a value like `{ "type": "SomeType", "id": "SomeId" }`"#,
331        EscapeKind::Extension => r#"an __extn escape should have a value like `{ "fn": "SomeFn", "arg": "SomeArg" }`"#,
332    }))]
333/// Error type for incorrect escaping
334pub struct ParseEscape {
335    /// Escape kind
336    kind: EscapeKind,
337    /// Escape value at fault
338    value: String,
339    /// Parse errors
340    #[diagnostic(transparent)]
341    errs: ParseErrors,
342}
343
344#[derive(Debug, Error, Diagnostic)]
345#[error("{}, expected a literal entity reference, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
346#[diagnostic(help(
347    r#"literal entity references can be made with `{{ "type": "SomeType", "id": "SomeId" }}`"#
348))]
349/// Error type for getting any expression other than an entity reference
350pub struct ExpectedLiteralEntityRef {
351    /// Context of this error
352    ctx: Box<JsonDeserializationErrorContext>,
353    /// the expression we got instead
354    got: Box<Either<serde_json::Value, Expr>>,
355}
356
357#[derive(Debug, Error, Diagnostic)]
358#[error("{}, expected an extension value, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
359#[diagnostic(help(r#"extension values can be made with `{{ "fn": "SomeFn", "id": "SomeId" }}`"#))]
360/// Error type for getting any expression other than en extesion value
361pub struct ExpectedExtnValue {
362    /// Context of this error
363    ctx: Box<JsonDeserializationErrorContext>,
364    /// the expression we got instead
365    got: Box<Either<serde_json::Value, Expr>>,
366}
367
368#[derive(Debug, Error, Diagnostic)]
369#[error(transparent)]
370/// Wrapper type for errors from `serde_json`
371pub struct JsonError(#[from] serde_json::Error);
372
373impl From<serde_json::Error> for JsonDeserializationError {
374    fn from(value: serde_json::Error) -> Self {
375        Self::Serde(JsonError(value))
376    }
377}
378
379impl From<serde_json::Error> for JsonSerializationError {
380    fn from(value: serde_json::Error) -> Self {
381        Self::Serde(JsonError(value))
382    }
383}
384
385/// Errors thrown during serialization to JSON
386#[derive(Debug, Diagnostic, Error)]
387#[non_exhaustive]
388pub enum JsonSerializationError {
389    /// Error thrown by `serde_json`
390    #[error(transparent)]
391    #[diagnostic(transparent)]
392    Serde(#[from] JsonError),
393    /// Extension-function calls with 0 arguments are not currently supported in
394    /// our JSON format.
395    #[error(transparent)]
396    #[diagnostic(transparent)]
397    ExtnCall0Arguments(ExtnCall0Arguments),
398    /// Encountered a `Record` which can't be serialized to JSON because it
399    /// contains a key which is reserved as a JSON escape.
400    #[error(transparent)]
401    #[diagnostic(transparent)]
402    ReservedKey(ReservedKey),
403    /// Encountered an `ExprKind` which we didn't expect. Either a case is
404    /// missing in `CedarValueJson::from_expr()`, or an internal invariant was
405    /// violated and there is a non-restricted expression in `RestrictedExpr`
406    #[error(transparent)]
407    #[diagnostic(transparent)]
408    UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind),
409    /// Encountered a (partial-evaluation) residual which can't be encoded in
410    /// JSON
411    #[error(transparent)]
412    #[diagnostic(transparent)]
413    Residual(Residual),
414    /// Extension-function calls with 2 or more arguments are not currently
415    /// supported in our JSON format.
416    /// Cedar should not throw any error of this variant after #1697
417    #[error(transparent)]
418    #[diagnostic(transparent)]
419    ExtnCall2OrMoreArguments(ExtnCall2OrMoreArguments),
420}
421
422impl JsonSerializationError {
423    pub(crate) fn call_0_args(func: Name) -> Self {
424        Self::ExtnCall0Arguments(ExtnCall0Arguments { func })
425    }
426
427    pub(crate) fn reserved_key(key: impl Into<SmolStr>) -> Self {
428        Self::ReservedKey(ReservedKey { key: key.into() })
429    }
430
431    pub(crate) fn unexpected_restricted_expr_kind(kind: ExprKind) -> Self {
432        Self::UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind { kind })
433    }
434
435    pub(crate) fn residual(residual: Expr) -> Self {
436        Self::Residual(Residual { residual })
437    }
438}
439
440/// Error type for extension functions called w/ 0 arguments
441#[derive(Debug, Error, Diagnostic)]
442#[error("unsupported call to `{}` with 0 arguments", .func)]
443#[diagnostic(help(
444    "extension function calls with 0 arguments are not currently supported in our JSON format"
445))]
446pub struct ExtnCall0Arguments {
447    /// Name of the function which was called with 0 arguments
448    func: Name,
449}
450
451/// Error type for extension functions called w/ 2+ arguments
452/// Cedar should not throw this error after #1697
453#[derive(Debug, Error, Diagnostic)]
454#[error("unsupported call to `{}` with 2 or more arguments", .func)]
455#[diagnostic(help("extension function calls with 2 or more arguments are not currently supported in our JSON format"))]
456pub struct ExtnCall2OrMoreArguments {
457    /// Name of the function called w/ 2 or more arguments
458    func: Name,
459}
460
461/// Error type for using a reserved key in a record
462#[derive(Debug, Error, Diagnostic)]
463#[error("record uses reserved key `{}`", .key)]
464pub struct ReservedKey {
465    /// The reserved key used
466    key: SmolStr,
467}
468
469impl ReservedKey {
470    /// The reserved keyword used as a key
471    pub fn key(&self) -> impl AsRef<str> + '_ {
472        &self.key
473    }
474}
475
476/// Error type for a restricted expression containing a non-restricted expression
477#[derive(Debug, Error, Diagnostic)]
478#[error("unexpected restricted expression `{:?}`", .kind)]
479pub struct UnexpectedRestrictedExprKind {
480    /// The [`ExprKind`] we didn't expend to find
481    kind: ExprKind,
482}
483
484/// Error type for residuals that can't be serialized
485#[derive(Debug, Error, Diagnostic)]
486#[error("cannot encode residual as JSON: {}", .residual)]
487pub struct Residual {
488    /// The residual that can't be serialized
489    residual: Expr,
490}
491
492/// Gives information about the context of a JSON deserialization error (e.g.,
493/// where we were in the JSON document).
494#[derive(Debug, Clone)]
495pub enum JsonDeserializationErrorContext {
496    /// The error occurred while deserializing the attribute `attr` of an entity.
497    EntityAttribute {
498        /// Entity where the error occurred
499        uid: EntityUID,
500        /// Attribute where the error occurred
501        attr: SmolStr,
502    },
503    /// The error occurred while deserializing the tag `tag` of an entity.
504    EntityTag {
505        /// Entity where the error occurred
506        uid: EntityUID,
507        /// Tag where the error occurred
508        tag: SmolStr,
509    },
510    /// The error occurred while deserializing the `parents` field of an entity.
511    EntityParents {
512        /// Entity where the error occurred
513        uid: EntityUID,
514    },
515    /// The error occurred while deserializing the `uid` field of an entity.
516    EntityUid,
517    /// The error occurred while deserializing the `Context`.
518    Context,
519    /// The error occurred while deserializing a policy in JSON (EST) form.
520    Policy {
521        /// ID of the policy we were deserializing
522        id: PolicyID,
523    },
524    /// The error occured while deserializing a template link
525    TemplateLink,
526    /// The context was unknown, this shouldn't surface to users
527    Unknown,
528}
529
530/// Type mismatch error (in terms of `SchemaType`)
531#[derive(Debug, Diagnostic, Error)]
532#[error("type mismatch: value was expected to have type {expected}, but it {mismatch_reason}: `{}`",
533    display_restricted_expr(.actual_val.as_borrowed()),
534)]
535pub struct TypeMismatchError {
536    /// Type which was expected
537    expected: Box<SchemaType>,
538    /// Reason for the type mismatch
539    mismatch_reason: TypeMismatchReason,
540    /// Value which doesn't have the expected type
541    actual_val: Box<RestrictedExpr>,
542}
543
544#[derive(Debug, Error)]
545enum TypeMismatchReason {
546    /// We saw this dynamic type which is not compatible with the expected
547    /// schema type.
548    #[error("actually has type {0}")]
549    UnexpectedType(Type),
550    /// We saw a record type expression as expected, but it contains an
551    /// attribute we didn't expect.
552    #[error("contains an unexpected attribute `{0}`")]
553    UnexpectedAttr(SmolStr),
554    /// We saw a record type expression as expected, but it did not contain an
555    /// attribute we expected.
556    #[error("is missing the required attribute `{0}`")]
557    MissingRequiredAtr(SmolStr),
558    /// No further detail available.
559    #[error("does not")]
560    None,
561}
562
563impl TypeMismatchError {
564    pub(crate) fn type_mismatch(
565        expected: SchemaType,
566        actual_ty: Option<Type>,
567        actual_val: RestrictedExpr,
568    ) -> Self {
569        Self {
570            expected: Box::new(expected),
571            mismatch_reason: match actual_ty {
572                Some(ty) => TypeMismatchReason::UnexpectedType(ty),
573                None => TypeMismatchReason::None,
574            },
575            actual_val: Box::new(actual_val),
576        }
577    }
578
579    pub(crate) fn unexpected_attr(
580        expected: SchemaType,
581        unexpected_attr: SmolStr,
582        actual_val: RestrictedExpr,
583    ) -> Self {
584        Self {
585            expected: Box::new(expected),
586            mismatch_reason: TypeMismatchReason::UnexpectedAttr(unexpected_attr),
587            actual_val: Box::new(actual_val),
588        }
589    }
590
591    pub(crate) fn missing_required_attr(
592        expected: SchemaType,
593        missing_attr: SmolStr,
594        actual_val: RestrictedExpr,
595    ) -> Self {
596        Self {
597            expected: Box::new(expected),
598            mismatch_reason: TypeMismatchReason::MissingRequiredAtr(missing_attr),
599            actual_val: Box::new(actual_val),
600        }
601    }
602}
603
604impl std::fmt::Display for JsonDeserializationErrorContext {
605    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606        match self {
607            Self::EntityAttribute { uid, attr } => write!(f, "in attribute `{attr}` on `{uid}`"),
608            Self::EntityTag { uid, tag } => write!(f, "in tag `{tag}` on `{uid}`"),
609            Self::EntityParents { uid } => write!(f, "in parents field of `{uid}`"),
610            Self::EntityUid => write!(f, "in uid field of <unknown entity>"),
611            Self::Context => write!(f, "while parsing context"),
612            Self::Policy { id } => write!(f, "while parsing JSON policy `{id}`"),
613            Self::TemplateLink => write!(f, "while parsing a template link"),
614            Self::Unknown => write!(f, "parsing context was unknown, please file a bug report at https://github.com/cedar-policy/cedar so we can improve this error message"),
615        }
616    }
617}
618
619fn display_json_value(v: &Either<serde_json::Value, Expr>) -> String {
620    match v {
621        Either::Left(json) => display_value(json),
622        Either::Right(e) => e.to_string(),
623    }
624}
625
626/// Display a `serde_json::Value`, but sorting object attributes, so that the
627/// output is deterministic (important for tests that check equality of error
628/// messages).
629///
630/// Note that this doesn't sort array elements, because JSON arrays are ordered,
631/// so all JSON-handling functions naturally preserve order for arrays and thus
632/// provide a deterministic output.
633fn display_value(v: &serde_json::Value) -> String {
634    match v {
635        serde_json::Value::Array(contents) => {
636            format!("[{}]", contents.iter().map(display_value).join(", "))
637        }
638        serde_json::Value::Object(map) => {
639            let mut v: Vec<_> = map.iter().collect();
640            // We sort the keys here so that our error messages are consistent and defined
641            v.sort_by_key(|p| p.0);
642            let display_kv = |kv: &(&String, &serde_json::Value)| format!("\"{}\":{}", kv.0, kv.1);
643            format!("{{{}}}", v.iter().map(display_kv).join(","))
644        }
645        other => other.to_string(),
646    }
647}
648
649/// Display a `RestrictedExpr`, but sorting record attributes and set elements,
650/// so that the output is deterministic (important for tests that check equality
651/// of error messages).
652fn display_restricted_expr(expr: BorrowedRestrictedExpr<'_>) -> String {
653    match expr.expr_kind() {
654        ExprKind::Set(elements) => {
655            let restricted_exprs = elements.iter().map(BorrowedRestrictedExpr::new_unchecked); // since the RestrictedExpr invariant holds for the input, it holds for all set elements
656            format!(
657                "[{}]",
658                restricted_exprs
659                    .map(display_restricted_expr)
660                    .sorted_unstable()
661                    .join(", ")
662            )
663        }
664        ExprKind::Record(m) => {
665            format!(
666                "{{{}}}",
667                m.iter()
668                    .sorted_unstable_by_key(|(k, _)| SmolStr::clone(k))
669                    .map(|(k, v)| format!("\"{}\": {}", k.escape_debug(), v))
670                    .join(", ")
671            )
672        }
673        _ => format!("{expr}"), // all other cases: use the normal Display
674    }
675}