use std::fmt;
use sc_observability_types::{ErrorCode, RecoverableSteps, Remediation};
use serde::de::{self, DeserializeOwned};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use crate::DISCRIMINANT_CODE;
use crate::exact::{require_exact_keys, required};
const JUSTIFICATION_KEY: &str = "justification";
const ALL_KINDS: &[ErrorKind] = &[
ErrorKind::InvalidInput,
ErrorKind::NotFound,
ErrorKind::Conflict,
ErrorKind::Unavailable,
ErrorKind::Internal,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum ErrorKind {
InvalidInput,
NotFound,
Conflict,
Unavailable,
Internal,
}
impl ErrorKind {
#[must_use]
pub const fn all() -> &'static [Self] {
ALL_KINDS
}
#[must_use]
pub const fn http_status_code(self) -> u16 {
match self {
Self::InvalidInput => 400,
Self::NotFound => 404,
Self::Conflict => 409,
Self::Unavailable => 503,
Self::Internal => 500,
}
}
#[must_use]
pub const fn is_caller_error(self) -> bool {
matches!(self, Self::InvalidInput | Self::NotFound | Self::Conflict)
}
const fn wire_name(self) -> &'static str {
match self {
Self::InvalidInput => "invalid_input",
Self::NotFound => "not_found",
Self::Conflict => "conflict",
Self::Unavailable => "unavailable",
Self::Internal => "internal",
}
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.wire_name())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OpError {
pub kind: ErrorKind,
pub code: ErrorCode,
pub message: String,
pub details: Option<Value>,
pub remediation: Option<Remediation>,
}
impl OpError {
#[must_use]
pub fn new(kind: ErrorKind, code: ErrorCode, message: impl Into<String>) -> Self {
Self {
kind,
code,
message: message.into(),
details: None,
remediation: None,
}
}
#[must_use]
pub fn from_code(kind: ErrorKind, code: &'static str, message: impl Into<String>) -> Self {
Self::new(kind, ErrorCode::new_static(code), message)
}
#[must_use]
pub fn with_details(mut self, details: Value) -> Self {
self.details = Some(details);
self
}
#[must_use]
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.remediation = Some(Remediation::recoverable(
suggestion,
std::iter::empty::<String>(),
));
self
}
#[must_use]
pub fn with_remediation(mut self, remediation: Remediation) -> Self {
self.remediation = Some(remediation);
self
}
#[must_use]
pub fn suggested_action(&self) -> Option<&str> {
self.remediation.as_ref().and_then(wire_suggestion)
}
#[must_use]
pub fn from_transport_strings(
code: &str,
endpoint: &str,
cause: &str,
suggested_action: Option<&str>,
) -> Self {
let daemon_down = code == "DAEMON.NOT_RUNNING";
let kind = if daemon_down {
ErrorKind::Unavailable
} else {
ErrorKind::Internal
};
let message = if daemon_down {
"the daemon is not running".to_owned()
} else {
format!("transport failure {code} at {endpoint}: {cause}")
};
Self {
kind,
code: ErrorCode::new_owned(code),
message,
details: Some(serde_json::json!({
"endpoint": endpoint,
"cause": cause,
})),
remediation: remediation_from_action(suggested_action),
}
}
}
impl fmt::Display for OpError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for OpError {}
impl Serialize for OpError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(5))?;
map.serialize_entry("kind", &self.kind)?;
map.serialize_entry("code", &self.code)?;
map.serialize_entry("message", &self.message)?;
map.serialize_entry(
"details",
&details_json(self.details.as_ref(), self.remediation.as_ref()),
)?;
map.serialize_entry(
"suggested_action",
&suggestion_json(self.remediation.as_ref()),
)?;
map.end()
}
}
impl<'de> Deserialize<'de> for OpError {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
op_error_from_value(value).map_err(de::Error::custom)
}
}
fn remediation_from_action(suggested_action: Option<&str>) -> Option<Remediation> {
let text = suggested_action?.trim();
if text.is_empty() {
None
} else {
Some(Remediation::recoverable(text, std::iter::empty::<String>()))
}
}
fn wire_suggestion(remediation: &Remediation) -> Option<&str> {
match remediation {
Remediation::Recoverable { steps } => {
let step = steps.first_step()?;
if step.is_empty() { None } else { Some(step) }
}
Remediation::NotRecoverable { .. } => None,
}
}
fn suggestion_json(remediation: Option<&Remediation>) -> Value {
match remediation.and_then(wire_suggestion) {
None => Value::Null,
Some(text) => Value::String(text.to_owned()),
}
}
fn details_json(details: Option<&Value>, remediation: Option<&Remediation>) -> Value {
let Some(Remediation::NotRecoverable { justification }) = remediation else {
return details.cloned().unwrap_or(Value::Null);
};
match details {
None => serde_json::json!({ JUSTIFICATION_KEY: justification }),
Some(Value::Object(map)) => {
let mut map = map.clone();
map.insert(
JUSTIFICATION_KEY.to_owned(),
Value::String(justification.clone()),
);
Value::Object(map)
}
Some(other) => serde_json::json!({
"value": other,
"justification": justification,
}),
}
}
fn op_error_from_value(value: Value) -> Result<OpError, String> {
let map = match value {
Value::Object(map) => map,
_ => {
return Err(format!("{DISCRIMINANT_CODE}: error must be a JSON object"));
}
};
require_exact_keys(
&map,
&["kind", "code", "message", "details", "suggested_action"],
)?;
let kind = from_json_value::<ErrorKind>(required(&map, "kind")?)?;
let code = from_json_value::<ErrorCode>(required(&map, "code")?)?;
let message = match required(&map, "message")? {
Value::String(message) => message.clone(),
_ => {
return Err(format!(
"{DISCRIMINANT_CODE}: message must be a JSON string"
));
}
};
let details = match required(&map, "details")? {
Value::Null => None,
other => Some(other.clone()),
};
let remediation = remediation_from_wire(required(&map, "suggested_action")?)?;
Ok(OpError {
kind,
code,
message,
details,
remediation,
})
}
fn remediation_from_wire(value: &Value) -> Result<Option<Remediation>, String> {
match value {
Value::Null => Ok(None),
Value::String(text) if text.is_empty() => Ok(None),
Value::String(text) => Ok(Some(Remediation::Recoverable {
steps: RecoverableSteps::first(text.clone()),
})),
_ => Err(format!(
"{DISCRIMINANT_CODE}: suggested_action must be a JSON string or null"
)),
}
}
fn from_json_value<T>(value: &Value) -> Result<T, String>
where
T: DeserializeOwned,
{
T::deserialize(value).map_err(|error| format!("{DISCRIMINANT_CODE}: {error}"))
}