use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostReaction {
DoNotRetry,
NarrowOrSkip,
RaiseBudgetOrSkip,
RetryWithBackoff,
Respawn,
DropProvider,
ReportAndCount,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCode {
BadRequest,
UnsupportedKind,
UnsupportedRepresentation,
IncompatibleVersion,
BudgetUnsatisfiable,
Unavailable,
ShuttingDown,
Internal,
Unknown(String),
}
impl ErrorCode {
pub fn as_str(&self) -> &str {
match self {
Self::BadRequest => "bad_request",
Self::UnsupportedKind => "unsupported_kind",
Self::UnsupportedRepresentation => "unsupported_representation",
Self::IncompatibleVersion => "incompatible_version",
Self::BudgetUnsatisfiable => "budget_unsatisfiable",
Self::Unavailable => "unavailable",
Self::ShuttingDown => "shutting_down",
Self::Internal => "internal",
Self::Unknown(raw) => raw,
}
}
pub fn reaction(&self) -> HostReaction {
match self {
Self::BadRequest => HostReaction::DoNotRetry,
Self::UnsupportedKind => HostReaction::NarrowOrSkip,
Self::UnsupportedRepresentation => HostReaction::NarrowOrSkip,
Self::IncompatibleVersion => HostReaction::DropProvider,
Self::BudgetUnsatisfiable => HostReaction::RaiseBudgetOrSkip,
Self::Unavailable => HostReaction::RetryWithBackoff,
Self::ShuttingDown => HostReaction::Respawn,
Self::Internal | Self::Unknown(_) => HostReaction::ReportAndCount,
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self.reaction(),
HostReaction::RetryWithBackoff | HostReaction::Respawn
)
}
pub fn is_recognized(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<&str> for ErrorCode {
fn from(raw: &str) -> Self {
match raw {
"bad_request" => Self::BadRequest,
"unsupported_kind" => Self::UnsupportedKind,
"unsupported_representation" => Self::UnsupportedRepresentation,
"incompatible_version" => Self::IncompatibleVersion,
"budget_unsatisfiable" => Self::BudgetUnsatisfiable,
"unavailable" => Self::Unavailable,
"shutting_down" => Self::ShuttingDown,
"internal" => Self::Internal,
other => Self::Unknown(other.to_string()),
}
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for ErrorCode {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ErrorCode {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
Ok(ErrorCode::from(raw.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_known_code_roundtrips_through_its_wire_spelling() {
let codes = [
ErrorCode::BadRequest,
ErrorCode::UnsupportedKind,
ErrorCode::UnsupportedRepresentation,
ErrorCode::IncompatibleVersion,
ErrorCode::BudgetUnsatisfiable,
ErrorCode::Unavailable,
ErrorCode::ShuttingDown,
ErrorCode::Internal,
];
for code in codes {
let json = serde_json::to_string(&code).unwrap();
let back: ErrorCode = serde_json::from_str(&json).unwrap();
assert_eq!(back, code, "{code} did not survive a round-trip");
assert!(code.is_recognized());
}
}
#[test]
fn codes_serialize_as_bare_snake_case_strings() {
assert_eq!(
serde_json::to_string(&ErrorCode::UnsupportedKind).unwrap(),
"\"unsupported_kind\""
);
}
#[test]
fn an_unknown_code_survives_a_roundtrip_verbatim() {
let code: ErrorCode = serde_json::from_str("\"quota_exceeded\"").unwrap();
assert_eq!(code, ErrorCode::Unknown("quota_exceeded".into()));
assert_eq!(serde_json::to_string(&code).unwrap(), "\"quota_exceeded\"");
assert!(!code.is_recognized());
}
#[test]
fn an_unknown_code_reacts_as_internal_never_as_retryable() {
let code = ErrorCode::Unknown("something_new".into());
assert_eq!(code.reaction(), HostReaction::ReportAndCount);
assert!(!code.is_retryable());
}
#[test]
fn reactions_separate_retryable_faults_from_permanent_ones() {
assert!(ErrorCode::Unavailable.is_retryable());
assert!(ErrorCode::ShuttingDown.is_retryable());
assert!(!ErrorCode::BadRequest.is_retryable());
assert!(!ErrorCode::UnsupportedKind.is_retryable());
assert!(!ErrorCode::UnsupportedRepresentation.is_retryable());
assert!(!ErrorCode::BudgetUnsatisfiable.is_retryable());
assert!(!ErrorCode::Internal.is_retryable());
assert!(!ErrorCode::IncompatibleVersion.is_retryable());
assert_eq!(
ErrorCode::IncompatibleVersion.reaction(),
HostReaction::DropProvider
);
}
}