use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
InvalidRequest,
Unauthorized,
Forbidden,
ContentTooLarge,
StoragePermissionDenied,
NotSupported,
NotFound,
MethodNotAllowed,
Gone,
AlreadyExists,
Conflict,
DeadlineExceeded,
Unavailable,
OutcomeUnknown,
DataCorruption,
Internal,
}
macro_rules! error_codes {
(@count) => { 0 };
(@count $head:ident $($tail:ident)*) => { 1 + error_codes!(@count $($tail)*) };
($($variant:ident => $wire:literal),+ $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorCode {
$(
#[doc = concat!("Carries the stable wire code `", $wire, "`.")]
$variant,
)+
}
impl ErrorCode {
pub const ALL: [ErrorCode; error_codes!(@count $($variant)+)] =
[$(ErrorCode::$variant,)+];
pub fn as_str(self) -> &'static str {
match self {
$(ErrorCode::$variant => $wire,)+
}
}
pub fn parse(value: &str) -> Option<ErrorCode> {
match value {
$($wire => Some(ErrorCode::$variant),)+
_ => None,
}
}
}
impl serde::Serialize for ErrorCode {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> serde::Deserialize<'de> for ErrorCode {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
ErrorCode::parse(&value).ok_or_else(|| {
serde::de::Error::custom(format_args!("unknown error code `{value}`"))
})
}
}
};
}
error_codes! {
InvalidRequest => "invalid_request",
Unauthorized => "unauthorized",
Forbidden => "forbidden",
StoragePermissionDenied => "storage_permission_denied",
ContentTooLarge => "content_too_large",
NotSupported => "not_supported",
RouteNotFound => "route_not_found",
MethodNotAllowed => "method_not_allowed",
NamespaceNotFound => "namespace_not_found",
NamespaceDeleted => "namespace_deleted",
NamespaceExists => "namespace_exists",
CheckpointNotFound => "checkpoint_not_found",
SnapshotNotFound => "snapshot_not_found",
SnapshotGone => "snapshot_gone",
SnapshotQuotaExceeded => "snapshot_quota_exceeded",
ContentNotPrepared => "content_not_prepared",
PathNotFound => "path_not_found",
InodeNotFound => "inode_not_found",
RevisionNotFound => "revision_not_found",
PathConflict => "path_conflict",
DirectoryNotEmpty => "directory_not_empty",
StaleHead => "stale_head",
StaleRevision => "stale_revision",
StaleAttributes => "stale_attributes",
StaleAccess => "stale_access",
NamespaceUnrestricted => "namespace_unrestricted",
BindingGenerationMismatch => "binding_generation_mismatch",
NotDeleted => "not_deleted",
WriterFenced => "writer_fenced",
WouldCycle => "would_cycle",
CommitIdReuseConflict => "commit_id_reuse_conflict",
CommitOutcomeUnknown => "commit_outcome_unknown",
CommitQueueFull => "commit_queue_full",
WriterSessionClosed => "writer_session_closed",
WriterCapacityExceeded => "writer_capacity_exceeded",
ServerBusy => "server_busy",
ShuttingDown => "shutting_down",
DeadlineExceeded => "deadline_exceeded",
CheckpointUnavailable => "checkpoint_unavailable",
ContentNotMaterialized => "content_not_materialized",
MaintenanceRequired => "maintenance_required",
UploadNotFound => "upload_not_found",
UploadAlreadyCompleted => "upload_already_completed",
UploadContentConflict => "upload_content_conflict",
RebootstrapRequired => "rebootstrap_required",
QueryUnindexable => "query_unindexable",
IndexLagging => "index_lagging",
IndexCorrupt => "index_corrupt",
NamespaceCorrupt => "namespace_corrupt",
ServerError => "server_error",
}
impl ErrorCode {
pub fn kind(self) -> ErrorKind {
match self {
ErrorCode::InvalidRequest | ErrorCode::QueryUnindexable => ErrorKind::InvalidRequest,
ErrorCode::Unauthorized => ErrorKind::Unauthorized,
ErrorCode::Forbidden => ErrorKind::Forbidden,
ErrorCode::StoragePermissionDenied => ErrorKind::StoragePermissionDenied,
ErrorCode::ContentTooLarge => ErrorKind::ContentTooLarge,
ErrorCode::NotSupported => ErrorKind::NotSupported,
ErrorCode::NamespaceNotFound
| ErrorCode::CheckpointNotFound
| ErrorCode::SnapshotNotFound
| ErrorCode::PathNotFound
| ErrorCode::InodeNotFound
| ErrorCode::RevisionNotFound
| ErrorCode::UploadNotFound
| ErrorCode::RouteNotFound => ErrorKind::NotFound,
ErrorCode::MethodNotAllowed => ErrorKind::MethodNotAllowed,
ErrorCode::NamespaceDeleted | ErrorCode::SnapshotGone => ErrorKind::Gone,
ErrorCode::NamespaceExists => ErrorKind::AlreadyExists,
ErrorCode::DeadlineExceeded => ErrorKind::DeadlineExceeded,
ErrorCode::CommitQueueFull
| ErrorCode::WriterSessionClosed
| ErrorCode::WriterCapacityExceeded
| ErrorCode::ServerBusy
| ErrorCode::ShuttingDown
| ErrorCode::CheckpointUnavailable
| ErrorCode::ContentNotMaterialized
| ErrorCode::IndexLagging
| ErrorCode::MaintenanceRequired => ErrorKind::Unavailable,
ErrorCode::CommitOutcomeUnknown => ErrorKind::OutcomeUnknown,
ErrorCode::IndexCorrupt | ErrorCode::NamespaceCorrupt => ErrorKind::DataCorruption,
ErrorCode::ServerError => ErrorKind::Internal,
ErrorCode::ContentNotPrepared
| ErrorCode::PathConflict
| ErrorCode::DirectoryNotEmpty
| ErrorCode::StaleHead
| ErrorCode::StaleRevision
| ErrorCode::StaleAttributes
| ErrorCode::StaleAccess
| ErrorCode::NamespaceUnrestricted
| ErrorCode::BindingGenerationMismatch
| ErrorCode::NotDeleted
| ErrorCode::WriterFenced
| ErrorCode::WouldCycle
| ErrorCode::CommitIdReuseConflict
| ErrorCode::UploadAlreadyCompleted
| ErrorCode::UploadContentConflict
| ErrorCode::RebootstrapRequired
| ErrorCode::SnapshotQuotaExceeded => ErrorKind::Conflict,
}
}
pub fn retryable_without_operator_action(self) -> bool {
match self {
ErrorCode::CommitQueueFull | ErrorCode::ServerBusy | ErrorCode::ShuttingDown => true,
ErrorCode::InvalidRequest
| ErrorCode::Unauthorized
| ErrorCode::Forbidden
| ErrorCode::StoragePermissionDenied
| ErrorCode::ContentTooLarge
| ErrorCode::NotSupported
| ErrorCode::RouteNotFound
| ErrorCode::MethodNotAllowed
| ErrorCode::NamespaceNotFound
| ErrorCode::NamespaceDeleted
| ErrorCode::NamespaceExists
| ErrorCode::CheckpointNotFound
| ErrorCode::SnapshotNotFound
| ErrorCode::SnapshotGone
| ErrorCode::SnapshotQuotaExceeded
| ErrorCode::ContentNotPrepared
| ErrorCode::PathNotFound
| ErrorCode::InodeNotFound
| ErrorCode::RevisionNotFound
| ErrorCode::PathConflict
| ErrorCode::DirectoryNotEmpty
| ErrorCode::StaleHead
| ErrorCode::StaleRevision
| ErrorCode::StaleAttributes
| ErrorCode::StaleAccess
| ErrorCode::NamespaceUnrestricted
| ErrorCode::BindingGenerationMismatch
| ErrorCode::NotDeleted
| ErrorCode::WriterFenced
| ErrorCode::WouldCycle
| ErrorCode::CommitIdReuseConflict
| ErrorCode::CommitOutcomeUnknown
| ErrorCode::WriterSessionClosed
| ErrorCode::WriterCapacityExceeded
| ErrorCode::DeadlineExceeded
| ErrorCode::CheckpointUnavailable
| ErrorCode::ContentNotMaterialized
| ErrorCode::MaintenanceRequired
| ErrorCode::UploadNotFound
| ErrorCode::UploadAlreadyCompleted
| ErrorCode::UploadContentConflict
| ErrorCode::RebootstrapRequired
| ErrorCode::QueryUnindexable
| ErrorCode::IndexLagging
| ErrorCode::IndexCorrupt
| ErrorCode::NamespaceCorrupt
| ErrorCode::ServerError => false,
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::ErrorCode;
#[test]
fn error_codes_serde_uses_the_wire_strings() {
for code in ErrorCode::ALL {
let value = serde_json::to_value(code).expect("serialize error code");
assert_eq!(value, serde_json::Value::String(code.as_str().to_owned()));
let parsed: ErrorCode = serde_json::from_value(value).expect("deserialize error code");
assert_eq!(parsed, code);
}
assert!(serde_json::from_str::<ErrorCode>("\"not_a_code\"").is_err());
}
#[test]
fn retryability_is_limited_to_self_clearing_admission_conditions() {
let retryable: Vec<_> = ErrorCode::ALL
.into_iter()
.filter(|code| code.retryable_without_operator_action())
.collect();
assert_eq!(
retryable,
[
ErrorCode::CommitQueueFull,
ErrorCode::ServerBusy,
ErrorCode::ShuttingDown,
]
);
}
}