use std::fmt::Display;
use super::{HeterogeneousSetError, SchemaType};
use crate::ast::{
BorrowedRestrictedExpr, ContextCreationError, EntityAttrEvaluationError, EntityUID, Expr,
ExprKind, Name, PartialValue, PolicyID, RestrictedExpr, RestrictedExprError,
};
use crate::entities::conformance::EntitySchemaConformanceError;
use crate::extensions::ExtensionFunctionLookupError;
use crate::parser::err::ParseErrors;
use either::Either;
use itertools::Itertools;
use miette::Diagnostic;
use smol_str::SmolStr;
use thiserror::Error;
#[derive(Debug)]
pub enum EscapeKind {
Entity,
Extension,
}
impl Display for EscapeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Entity => write!(f, "__entity"),
Self::Extension => write!(f, "__extn"),
}
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum JsonDeserializationError {
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error("failed to parse escape `{kind}`: {value}, errors: {errs}")]
#[diagnostic(help("{}", match .kind {
EscapeKind::Entity => r#"an __entity escape should have a value like `{ "type": "SomeType", "id": "SomeId" }`"#,
EscapeKind::Extension => r#"an __extn escape should have a value like `{ "fn": "SomeFn", "arg": "SomeArg" }`"#,
}))]
ParseEscape {
kind: EscapeKind,
value: String,
#[diagnostic(transparent)]
errs: ParseErrors,
},
#[error(transparent)]
#[diagnostic(transparent)]
RestrictedExpressionError(#[from] RestrictedExprError),
#[error("{ctx}, expected a literal entity reference, but got `{}`", display_json_value(.got.as_ref()))]
#[diagnostic(help(
r#"literal entity references can be made with `{{ "type": "SomeType", "id": "SomeId" }}`"#
))]
ExpectedLiteralEntityRef {
ctx: Box<JsonDeserializationErrorContext>,
got: Box<Either<serde_json::Value, Expr>>,
},
#[error("{ctx}, expected an extension value, but got `{}`", display_json_value(.got.as_ref()))]
#[diagnostic(help(
r#"extension values can be made with `{{ "fn": "SomeFn", "id": "SomeId" }}`"#
))]
ExpectedExtnValue {
ctx: Box<JsonDeserializationErrorContext>,
got: Box<Either<serde_json::Value, Expr>>,
},
#[error("while parsing context, {0}")]
#[diagnostic(transparent)]
ContextCreation(#[from] ContextCreationError),
#[error("action `{uid}` has a non-action parent `{parent}`")]
#[diagnostic(help(
"parents of actions need to have type `Action` themselves, perhaps namespaced"
))]
ActionParentIsNotAction {
uid: EntityUID,
parent: EntityUID,
},
#[error("{ctx}, missing extension constructor for {arg_type} -> {return_type}")]
#[diagnostic(help("expected a value of type {return_type} because of the schema"))]
MissingImpliedConstructor {
ctx: Box<JsonDeserializationErrorContext>,
return_type: Box<SchemaType>,
arg_type: Box<SchemaType>,
},
#[error(transparent)]
#[diagnostic(transparent)]
DuplicateKey(DuplicateKey),
#[error(transparent)]
#[diagnostic(transparent)]
EntityAttributeEvaluation(#[from] EntityAttrEvaluationError),
#[error(transparent)]
#[diagnostic(transparent)]
EntitySchemaConformance(EntitySchemaConformanceError),
#[error("{ctx}, record attribute `{record_attr}` should not exist according to the schema")]
UnexpectedRecordAttr {
ctx: Box<JsonDeserializationErrorContext>,
record_attr: SmolStr,
},
#[error("{ctx}, expected the record to have an attribute `{record_attr}`, but it does not")]
MissingRequiredRecordAttr {
ctx: Box<JsonDeserializationErrorContext>,
record_attr: SmolStr,
},
#[error("{ctx}, {err}")]
TypeMismatch {
ctx: Box<JsonDeserializationErrorContext>,
#[diagnostic(transparent)]
err: TypeMismatchError,
},
#[error("{ctx}, {err}")]
HeterogeneousSet {
ctx: Box<JsonDeserializationErrorContext>,
#[diagnostic(transparent)]
err: HeterogeneousSetError,
},
#[error("{ctx}, {err}")]
ExtensionFunctionLookup {
ctx: Box<JsonDeserializationErrorContext>,
#[diagnostic(transparent)]
err: ExtensionFunctionLookupError,
},
#[error("{ctx}, argument `{arg}` to implicit constructor contains an unknown; this is not currently supported")]
#[diagnostic(help(
r#"expected an extension value here because of the schema. To pass an unknown to an extension function, use the explicit constructor form: `{{ "fn": "SomeFn", "arg": "SomeArg" }}`"#
))]
UnknownInImplicitConstructorArg {
ctx: Box<JsonDeserializationErrorContext>,
arg: Box<RestrictedExpr>,
},
#[error("{0}, the `__expr` escape is no longer supported")]
#[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))]
ExprTag(Box<JsonDeserializationErrorContext>),
#[error("{0}, found a `null`; JSON `null`s are not allowed in Cedar")]
Null(Box<JsonDeserializationErrorContext>),
}
impl JsonDeserializationError {
pub(crate) fn duplicate_key(
ctx: JsonDeserializationErrorContext,
key: impl Into<SmolStr>,
) -> Self {
Self::DuplicateKey(DuplicateKey {
ctx: Box::new(ctx),
key: key.into(),
})
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("{}, duplicate key `{}` in record", .ctx, .key)]
pub struct DuplicateKey {
ctx: Box<JsonDeserializationErrorContext>,
key: SmolStr,
}
#[derive(Debug, Diagnostic, Error)]
pub enum JsonSerializationError {
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error("unsupported call to `{func}` with 0 arguments")]
#[diagnostic(help(
"extension function calls with 0 arguments are not currently supported in our JSON format"
))]
ExtnCall0Arguments {
func: Name,
},
#[error("unsupported call to `{func}` with 2 or more arguments")]
#[diagnostic(help("extension function calls with 2 or more arguments are not currently supported in our JSON format"))]
ExtnCall2OrMoreArguments {
func: Name,
},
#[error("record uses reserved key `{key}`")]
ReservedKey {
key: SmolStr,
},
#[error("unexpected restricted expression `{kind:?}`")]
UnexpectedRestrictedExprKind {
kind: ExprKind,
},
#[error("cannot encode residual as JSON: {residual}")]
Residual {
residual: Expr,
},
}
#[derive(Debug, Clone)]
pub enum JsonDeserializationErrorContext {
EntityAttribute {
uid: EntityUID,
attr: SmolStr,
},
EntityParents {
uid: EntityUID,
},
EntityUid,
Context,
Policy {
id: PolicyID,
},
TemplateLink,
Unknown,
}
#[derive(Debug, Diagnostic, Error)]
#[error("type mismatch: value was expected to have type {expected}, but {}: `{}`",
match .actual_ty {
Some(actual_ty) => format!("actually has type {actual_ty}"),
None => "it does not".to_string(),
},
match .actual_val {
Either::Left(pval) => format!("{pval}"),
Either::Right(expr) => display_restricted_expr(expr.as_borrowed()),
}
)]
pub struct TypeMismatchError {
pub expected: Box<SchemaType>,
pub actual_ty: Option<Box<SchemaType>>,
pub actual_val: Either<PartialValue, Box<RestrictedExpr>>,
}
impl std::fmt::Display for JsonDeserializationErrorContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EntityAttribute { uid, attr } => write!(f, "in attribute `{attr}` on `{uid}`"),
Self::EntityParents { uid } => write!(f, "in parents field of `{uid}`"),
Self::EntityUid => write!(f, "in uid field of <unknown entity>"),
Self::Context => write!(f, "while parsing context"),
Self::Policy { id } => write!(f, "while parsing JSON policy `{id}`"),
Self::TemplateLink => write!(f, "while parsing a template link"),
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"),
}
}
}
fn display_json_value(v: &Either<serde_json::Value, Expr>) -> String {
match v {
Either::Left(json) => display_value(json),
Either::Right(e) => e.to_string(),
}
}
fn display_value(v: &serde_json::Value) -> String {
match v {
serde_json::Value::Array(contents) => {
format!("[{}]", contents.iter().map(display_value).join(", "))
}
serde_json::Value::Object(map) => {
let mut v: Vec<_> = map.iter().collect();
v.sort_by_key(|p| p.0);
let display_kv = |kv: &(&String, &serde_json::Value)| format!("\"{}\":{}", kv.0, kv.1);
format!("{{{}}}", v.iter().map(display_kv).join(","))
}
other => other.to_string(),
}
}
fn display_restricted_expr(expr: BorrowedRestrictedExpr<'_>) -> String {
match expr.expr_kind() {
ExprKind::Set(elements) => {
let restricted_exprs = elements.iter().map(BorrowedRestrictedExpr::new_unchecked); format!(
"[{}]",
restricted_exprs
.map(display_restricted_expr)
.sorted_unstable()
.join(", ")
)
}
ExprKind::Record(m) => {
format!(
"{{{}}}",
m.iter()
.sorted_unstable_by_key(|(k, _)| SmolStr::clone(k))
.map(|(k, v)| format!("\"{}\": {}", k.escape_debug(), v))
.join(", ")
)
}
_ => format!("{expr}"), }
}