use crate::host_identity::HostIdentityError;
use serde_json::{json, Value};
use std::fmt;
pub const ERROR_HOST_HANDOFF: &str = "host_handoff";
pub const ERROR_HOST_IDENTITY: &str = "host_identity";
pub const ERROR_SUITE_UNRESOLVED: &str = "suite_unresolved";
pub const ERROR_NOT_ALLOWED: &str = "request_not_allowed";
pub const ERROR_RESPONSE_TOO_LARGE: &str = "response_too_large";
pub const ERROR_REQUEST_TOO_LARGE: &str = "request_too_large";
pub const ERROR_BAD_CHUNK: &str = "bad_chunk";
pub const ERROR_CONNECTION_CLOSED: &str = "connection_closed";
pub const ERROR_TIMEOUT: &str = "request_timeout";
pub const ERROR_UNAVAILABLE: &str = "service_unavailable";
pub const ERROR_UNAUTHENTICATED: &str = "unauthenticated";
pub const ERROR_FORBIDDEN: &str = "forbidden";
pub const ERROR_REVISION_CONFLICT: &str = "revision_conflict";
pub const ERROR_RATE_LIMITED: &str = "rate_limited";
#[derive(Debug, Clone, PartialEq)]
pub struct ServiceError {
pub code: String,
pub message: String,
pub retryable: bool,
pub details: Option<Value>,
}
impl Eq for ServiceError {}
impl ServiceError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
retryable: false,
details: None,
}
}
pub fn retryable(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
retryable: true,
details: None,
}
}
pub fn with_details(mut self, details: Value) -> Self {
self.details = if details.is_null() {
None
} else {
Some(details)
};
self
}
pub fn details(&self) -> Option<&Value> {
self.details.as_ref()
}
pub fn handoff() -> Self {
Self::retryable(
ERROR_HOST_HANDOFF,
"host is handing the service to a higher-ranked host; retry",
)
}
pub fn to_value(&self) -> Value {
let mut v =
json!({"code": self.code, "message": self.message, "retryable": self.retryable});
if let (Some(d), Some(o)) = (&self.details, v.as_object_mut()) {
o.insert("details".to_string(), d.clone());
}
v
}
pub fn from_value(v: &Value) -> Self {
Self {
code: v["code"].as_str().unwrap_or("error").into(),
message: v["message"].as_str().unwrap_or("").into(),
retryable: v["retryable"].as_bool().unwrap_or(false),
details: v.get("details").filter(|d| !d.is_null()).cloned(),
}
}
}
impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for ServiceError {}
impl From<std::io::Error> for ServiceError {
fn from(e: std::io::Error) -> Self {
Self::new("io", e.to_string())
}
}
impl From<HostIdentityError> for ServiceError {
fn from(e: HostIdentityError) -> Self {
Self::new(ERROR_HOST_IDENTITY, e.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConnectError {
Service(ServiceError),
HostIdentity(HostIdentityError),
}
impl ConnectError {
pub fn into_service_error(self) -> ServiceError {
match self {
ConnectError::Service(e) => e,
ConnectError::HostIdentity(e) => e.into(),
}
}
}
impl fmt::Display for ConnectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConnectError::Service(e) => fmt::Display::fmt(e, f),
ConnectError::HostIdentity(e) => fmt::Display::fmt(e, f),
}
}
}
impl std::error::Error for ConnectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConnectError::Service(e) => Some(e),
ConnectError::HostIdentity(e) => Some(e),
}
}
}
impl From<ServiceError> for ConnectError {
fn from(e: ServiceError) -> Self {
ConnectError::Service(e)
}
}
impl From<HostIdentityError> for ConnectError {
fn from(e: HostIdentityError) -> Self {
ConnectError::HostIdentity(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn details_roundtrip_and_absent_or_null_read_as_none() {
let plain = ServiceError::new("nope", "x");
assert!(plain.details().is_none());
assert!(plain.to_value().get("details").is_none());
assert_eq!(ServiceError::from_value(&plain.to_value()), plain);
let nulled = json!({"code": "nope", "message": "x", "retryable": false, "details": null});
assert_eq!(ServiceError::from_value(&nulled), plain);
let detailed = ServiceError::retryable("needs_auth", "sign in")
.with_details(json!({"platform": "x", "nested": {"a": [1, 2]}}));
assert_eq!(detailed.to_value()["details"]["nested"]["a"], json!([1, 2]));
assert_eq!(ServiceError::from_value(&detailed.to_value()), detailed);
assert!(detailed.with_details(Value::Null).details().is_none());
}
}