use std::collections::BTreeMap;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
pub trait ServiceError {
fn to_envelope(&self, instance: String) -> ErrorEnvelope;
fn from_envelope(envelope: &ErrorEnvelope) -> Option<Self>
where
Self: Sized;
}
pub fn canonical_http_status(code: &str) -> u16 {
match code {
"UNAUTHENTICATED" => 401,
"PERMISSION_DENIED" => 403,
"INVALID_ARGUMENT" => 400,
"NOT_FOUND" => 404,
"CONFLICT" => 409,
"FAILED_PRECONDITION" => 412,
"REQUEST_ENTITY_TOO_LARGE" => 413,
"UNAVAILABLE" => 503,
"TIMEOUT" => 504,
"INTERNAL" => 500,
_ => 500,
}
}
pub fn new_error_instance_id() -> String {
Uuid::now_v7().to_string()
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorEnvelope {
pub error_code: String,
pub error_name: String,
pub error_instance_id: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub parameters: BTreeMap<String, String>,
#[serde(default, flatten)]
pub extra_fields: BTreeMap<String, Value>,
}
impl ErrorEnvelope {
pub fn new(
code: impl Into<String>,
name: impl Into<String>,
instance: impl Into<String>,
) -> Self {
Self {
error_code: code.into(),
error_name: name.into(),
error_instance_id: instance.into(),
parameters: BTreeMap::new(),
extra_fields: BTreeMap::new(),
}
}
}
impl Display for ErrorEnvelope {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{} ({}; instance {})",
self.error_name, self.error_code, self.error_instance_id
)
}
}
impl Error for ErrorEnvelope {}
#[cfg(test)]
mod tests {
use serde_json::json;
use uuid::Uuid;
use super::{ErrorEnvelope, new_error_instance_id};
#[test]
fn future_errors_preserve_parameters_and_envelope_fields() {
let value = json!({
"errorCode": "CONFLICT", "errorName": "Future:Conflict", "errorInstanceId": "instance",
"parameters": {"path": "input", "token": "secret"},
"futureField": {"reason": true}
});
let envelope: ErrorEnvelope = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(&envelope).unwrap(), value);
assert!(!envelope.to_string().contains("secret"));
}
#[test]
fn error_instance_ids_are_uuid_v7() {
let id = Uuid::parse_str(&new_error_instance_id()).unwrap();
assert_eq!(id.get_version_num(), 7);
}
}