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#[diagnostic(forward(err))]
253/// General error for type mismatches
254pub struct TypeMismatch {
255    /// Context of this error, which will be something other than `EntityAttribute`.
256    /// (Type mismatches in entity attributes are reported as
257    /// `Self::EntitySchemaConformance`.)
258    ctx: Box<JsonDeserializationErrorContext>,
259    /// Underlying error
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(
330    help("{}", match .kind {
331        EscapeKind::Entity => r#"an __entity escape should have a value like `{ "type": "SomeType", "id": "SomeId" }`"#,
332        EscapeKind::Extension => r#"an __extn escape should have a value like `{ "fn": "SomeFn", "arg": "SomeArg" }`"#,
333    }),
334    forward(errs)
335)]
336/// Error type for incorrect escaping
337pub struct ParseEscape {
338    /// Escape kind
339    kind: EscapeKind,
340    /// Escape value at fault
341    value: String,
342    /// Parse errors
343    errs: ParseErrors,
344}
345
346#[derive(Debug, Error, Diagnostic)]
347#[error("{}, expected a literal entity reference, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
348#[diagnostic(help(
349    r#"literal entity references can be made with `{{ "type": "SomeType", "id": "SomeId" }}`"#
350))]
351/// Error type for getting any expression other than an entity reference
352pub struct ExpectedLiteralEntityRef {
353    /// Context of this error
354    ctx: Box<JsonDeserializationErrorContext>,
355    /// the expression we got instead
356    got: Box<Either<serde_json::Value, Expr>>,
357}
358
359#[derive(Debug, Error, Diagnostic)]
360#[error("{}, expected an extension value, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
361#[diagnostic(help(r#"extension values can be made with `{{ "fn": "SomeFn", "id": "SomeId" }}`"#))]
362/// Error type for getting any expression other than en extesion value
363pub struct ExpectedExtnValue {
364    /// Context of this error
365    ctx: Box<JsonDeserializationErrorContext>,
366    /// the expression we got instead
367    got: Box<Either<serde_json::Value, Expr>>,
368}
369
370#[derive(Debug, Error, Diagnostic)]
371#[error(transparent)]
372/// Wrapper type for errors from `serde_json`
373pub struct JsonError(#[from] serde_json::Error);
374
375impl From<serde_json::Error> for JsonDeserializationError {
376    fn from(value: serde_json::Error) -> Self {
377        Self::Serde(JsonError(value))
378    }
379}
380
381impl From<serde_json::Error> for JsonSerializationError {
382    fn from(value: serde_json::Error) -> Self {
383        Self::Serde(JsonError(value))
384    }
385}
386
387/// Errors thrown during serialization to JSON
388#[derive(Debug, Diagnostic, Error)]
389#[non_exhaustive]
390pub enum JsonSerializationError {
391    /// Error thrown by `serde_json`
392    #[error(transparent)]
393    #[diagnostic(transparent)]
394    Serde(#[from] JsonError),
395    /// Extension-function calls with 0 arguments are not currently supported in
396    /// our JSON format.
397    #[error(transparent)]
398    #[diagnostic(transparent)]
399    ExtnCall0Arguments(ExtnCall0Arguments),
400    /// Encountered a `Record` which can't be serialized to JSON because it
401    /// contains a key which is reserved as a JSON escape.
402    #[error(transparent)]
403    #[diagnostic(transparent)]
404    ReservedKey(ReservedKey),
405    /// Encountered an `ExprKind` which we didn't expect. Either a case is
406    /// missing in `CedarValueJson::from_expr()`, or an internal invariant was
407    /// violated and there is a non-restricted expression in `RestrictedExpr`
408    #[error(transparent)]
409    #[diagnostic(transparent)]
410    UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind),
411    /// Encountered a (partial-evaluation) residual which can't be encoded in
412    /// JSON
413    #[error(transparent)]
414    #[diagnostic(transparent)]
415    Residual(Residual),
416    /// Extension-function calls with 2 or more arguments are not currently
417    /// supported in our JSON format.
418    /// Cedar should not throw any error of this variant after #1697
419    #[error(transparent)]
420    #[diagnostic(transparent)]
421    ExtnCall2OrMoreArguments(ExtnCall2OrMoreArguments),
422}
423
424impl JsonSerializationError {
425    pub(crate) fn call_0_args(func: Name) -> Self {
426        Self::ExtnCall0Arguments(ExtnCall0Arguments { func })
427    }
428
429    pub(crate) fn reserved_key(key: impl Into<SmolStr>) -> Self {
430        Self::ReservedKey(ReservedKey { key: key.into() })
431    }
432
433    pub(crate) fn unexpected_restricted_expr_kind(kind: ExprKind) -> Self {
434        Self::UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind { kind })
435    }
436
437    pub(crate) fn residual(residual: Expr) -> Self {
438        Self::Residual(Residual { residual })
439    }
440}
441
442/// Error type for extension functions called w/ 0 arguments
443#[derive(Debug, Error, Diagnostic)]
444#[error("unsupported call to `{}` with 0 arguments", .func)]
445#[diagnostic(help(
446    "extension function calls with 0 arguments are not currently supported in our JSON format"
447))]
448pub struct ExtnCall0Arguments {
449    /// Name of the function which was called with 0 arguments
450    func: Name,
451}
452
453/// Error type for extension functions called w/ 2+ arguments
454/// Cedar should not throw this error after #1697
455#[derive(Debug, Error, Diagnostic)]
456#[error("unsupported call to `{}` with 2 or more arguments", .func)]
457#[diagnostic(help("extension function calls with 2 or more arguments are not currently supported in our JSON format"))]
458pub struct ExtnCall2OrMoreArguments {
459    /// Name of the function called w/ 2 or more arguments
460    func: Name,
461}
462
463/// Error type for using a reserved key in a record
464#[derive(Debug, Error, Diagnostic)]
465#[error("record uses reserved key `{}`", .key)]
466pub struct ReservedKey {
467    /// The reserved key used
468    key: SmolStr,
469}
470
471impl ReservedKey {
472    /// The reserved keyword used as a key
473    pub fn key(&self) -> impl AsRef<str> + '_ {
474        &self.key
475    }
476}
477
478/// Error type for a restricted expression containing a non-restricted expression
479#[derive(Debug, Error, Diagnostic)]
480#[error("unexpected restricted expression `{:?}`", .kind)]
481pub struct UnexpectedRestrictedExprKind {
482    /// The [`ExprKind`] we didn't expend to find
483    kind: ExprKind,
484}
485
486/// Error type for residuals that can't be serialized
487#[derive(Debug, Error, Diagnostic)]
488#[error("cannot encode residual as JSON: {}", .residual)]
489pub struct Residual {
490    /// The residual that can't be serialized
491    residual: Expr,
492}
493
494/// Gives information about the context of a JSON deserialization error (e.g.,
495/// where we were in the JSON document).
496#[derive(Debug, Clone)]
497pub enum JsonDeserializationErrorContext {
498    /// The error occurred while deserializing the attribute `attr` of an entity.
499    EntityAttribute {
500        /// Entity where the error occurred
501        uid: EntityUID,
502        /// Attribute where the error occurred
503        attr: SmolStr,
504    },
505    /// The error occurred while deserializing the tag `tag` of an entity.
506    EntityTag {
507        /// Entity where the error occurred
508        uid: EntityUID,
509        /// Tag where the error occurred
510        tag: SmolStr,
511    },
512    /// The error occurred while deserializing the `parents` field of an entity.
513    EntityParents {
514        /// Entity where the error occurred
515        uid: EntityUID,
516    },
517    /// The error occurred while deserializing the `uid` field of an entity.
518    EntityUid,
519    /// The error occurred while deserializing the `Context`.
520    Context,
521    /// The error occurred while deserializing a policy in JSON (EST) form.
522    Policy {
523        /// ID of the policy we were deserializing
524        id: PolicyID,
525    },
526    /// The error occured while deserializing a template link
527    TemplateLink,
528    /// The context was unknown, this shouldn't surface to users
529    Unknown,
530}
531
532/// Type mismatch error (in terms of `SchemaType`)
533#[derive(Debug, Diagnostic, Error)]
534#[error("type mismatch: value was expected to have type {expected}, but it {mismatch_reason}: `{}`",
535    display_restricted_expr(.actual_val.as_borrowed()),
536)]
537pub struct TypeMismatchError {
538    /// Type which was expected
539    expected: Box<SchemaType>,
540    /// Reason for the type mismatch
541    mismatch_reason: TypeMismatchReason,
542    /// Value which doesn't have the expected type
543    actual_val: Box<RestrictedExpr>,
544}
545
546#[derive(Debug, Error)]
547enum TypeMismatchReason {
548    /// We saw this dynamic type which is not compatible with the expected
549    /// schema type.
550    #[error("actually has type {0}")]
551    UnexpectedType(Type),
552    /// We saw a record type expression as expected, but it contains an
553    /// attribute we didn't expect.
554    #[error("contains an unexpected attribute `{0}`")]
555    UnexpectedAttr(SmolStr),
556    /// We saw a record type expression as expected, but it did not contain an
557    /// attribute we expected.
558    #[error("is missing the required attribute `{0}`")]
559    MissingRequiredAtr(SmolStr),
560    /// No further detail available.
561    #[error("does not")]
562    None,
563}
564
565impl TypeMismatchError {
566    pub(crate) fn type_mismatch(
567        expected: SchemaType,
568        actual_ty: Option<Type>,
569        actual_val: RestrictedExpr,
570    ) -> Self {
571        Self {
572            expected: Box::new(expected),
573            mismatch_reason: match actual_ty {
574                Some(ty) => TypeMismatchReason::UnexpectedType(ty),
575                None => TypeMismatchReason::None,
576            },
577            actual_val: Box::new(actual_val),
578        }
579    }
580
581    pub(crate) fn unexpected_attr(
582        expected: SchemaType,
583        unexpected_attr: SmolStr,
584        actual_val: RestrictedExpr,
585    ) -> Self {
586        Self {
587            expected: Box::new(expected),
588            mismatch_reason: TypeMismatchReason::UnexpectedAttr(unexpected_attr),
589            actual_val: Box::new(actual_val),
590        }
591    }
592
593    pub(crate) fn missing_required_attr(
594        expected: SchemaType,
595        missing_attr: SmolStr,
596        actual_val: RestrictedExpr,
597    ) -> Self {
598        Self {
599            expected: Box::new(expected),
600            mismatch_reason: TypeMismatchReason::MissingRequiredAtr(missing_attr),
601            actual_val: Box::new(actual_val),
602        }
603    }
604}
605
606impl std::fmt::Display for JsonDeserializationErrorContext {
607    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608        match self {
609            Self::EntityAttribute { uid, attr } => write!(f, "in attribute `{attr}` on `{uid}`"),
610            Self::EntityTag { uid, tag } => write!(f, "in tag `{tag}` on `{uid}`"),
611            Self::EntityParents { uid } => write!(f, "in parents field of `{uid}`"),
612            Self::EntityUid => write!(f, "in uid field of <unknown entity>"),
613            Self::Context => write!(f, "while parsing context"),
614            Self::Policy { id } => write!(f, "while parsing JSON policy `{id}`"),
615            Self::TemplateLink => write!(f, "while parsing a template link"),
616            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"),
617        }
618    }
619}
620
621fn display_json_value(v: &Either<serde_json::Value, Expr>) -> String {
622    match v {
623        Either::Left(json) => display_value(json),
624        Either::Right(e) => e.to_string(),
625    }
626}
627
628/// Display a `serde_json::Value`, but sorting object attributes, so that the
629/// output is deterministic (important for tests that check equality of error
630/// messages).
631///
632/// Note that this doesn't sort array elements, because JSON arrays are ordered,
633/// so all JSON-handling functions naturally preserve order for arrays and thus
634/// provide a deterministic output.
635fn display_value(v: &serde_json::Value) -> String {
636    match v {
637        serde_json::Value::Array(contents) => {
638            format!("[{}]", contents.iter().map(display_value).join(", "))
639        }
640        serde_json::Value::Object(map) => {
641            let mut v: Vec<_> = map.iter().collect();
642            // We sort the keys here so that our error messages are consistent and defined
643            v.sort_by_key(|p| p.0);
644            let display_kv = |kv: &(&String, &serde_json::Value)| format!("\"{}\":{}", kv.0, kv.1);
645            format!("{{{}}}", v.iter().map(display_kv).join(","))
646        }
647        other => other.to_string(),
648    }
649}
650
651/// Display a `RestrictedExpr`, but sorting record attributes and set elements,
652/// so that the output is deterministic (important for tests that check equality
653/// of error messages).
654fn display_restricted_expr(expr: BorrowedRestrictedExpr<'_>) -> String {
655    match expr.expr_kind() {
656        ExprKind::Set(elements) => {
657            let restricted_exprs = elements.iter().map(BorrowedRestrictedExpr::new_unchecked); // since the RestrictedExpr invariant holds for the input, it holds for all set elements
658            format!(
659                "[{}]",
660                restricted_exprs
661                    .map(display_restricted_expr)
662                    .sorted_unstable()
663                    .join(", ")
664            )
665        }
666        ExprKind::Record(m) => {
667            format!(
668                "{{{}}}",
669                m.iter()
670                    .sorted_unstable_by_key(|(k, _)| SmolStr::clone(k))
671                    .map(|(k, v)| format!("\"{}\": {}", k.escape_debug(), v))
672                    .join(", ")
673            )
674        }
675        _ => format!("{expr}"), // all other cases: use the normal Display
676    }
677}