use crate::authz::AuthzError;
use crate::checkpoint::CheckpointError;
use crate::fork::ForkError;
use crate::graph::GraphError;
use crate::kv::KvError;
use crate::query::QueryError;
use crate::session::SessionError;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResultCode {
Ok,
Unsupported,
NotFound,
InvalidArgument,
TooLarge,
Conflict,
Stale,
VersionSkew,
Unauthenticated,
Backend,
Forbidden,
StepUpRequired,
Unavailable,
ResourceLimit,
Cancelled,
DeadlineExceeded,
ExpiredSnapshot,
StaleGeneration,
TargetUnavailable,
Pending,
BackendUnavailable,
Unrecognized(u16),
}
impl ResultCode {
pub const fn code(self) -> u16 {
match self {
ResultCode::Ok => 0,
ResultCode::Unsupported => 1,
ResultCode::NotFound => 2,
ResultCode::InvalidArgument => 3,
ResultCode::TooLarge => 4,
ResultCode::Conflict => 5,
ResultCode::Stale => 6,
ResultCode::VersionSkew => 7,
ResultCode::Unauthenticated => 8,
ResultCode::Backend => 9,
ResultCode::Forbidden => 10,
ResultCode::StepUpRequired => 11,
ResultCode::Unavailable => 12,
ResultCode::ResourceLimit => 13,
ResultCode::Cancelled => 14,
ResultCode::DeadlineExceeded => 15,
ResultCode::ExpiredSnapshot => 16,
ResultCode::StaleGeneration => 17,
ResultCode::TargetUnavailable => 18,
ResultCode::Pending => 19,
ResultCode::BackendUnavailable => 20,
ResultCode::Unrecognized(code) => code,
}
}
pub const fn from_code(code: u16) -> Self {
match code {
0 => ResultCode::Ok,
1 => ResultCode::Unsupported,
2 => ResultCode::NotFound,
3 => ResultCode::InvalidArgument,
4 => ResultCode::TooLarge,
5 => ResultCode::Conflict,
6 => ResultCode::Stale,
7 => ResultCode::VersionSkew,
8 => ResultCode::Unauthenticated,
9 => ResultCode::Backend,
10 => ResultCode::Forbidden,
11 => ResultCode::StepUpRequired,
12 => ResultCode::Unavailable,
13 => ResultCode::ResourceLimit,
14 => ResultCode::Cancelled,
15 => ResultCode::DeadlineExceeded,
16 => ResultCode::ExpiredSnapshot,
17 => ResultCode::StaleGeneration,
18 => ResultCode::TargetUnavailable,
19 => ResultCode::Pending,
20 => ResultCode::BackendUnavailable,
other => ResultCode::Unrecognized(other),
}
}
pub const fn http_status(self) -> u16 {
match self {
ResultCode::Ok => 200,
ResultCode::Unsupported => 501,
ResultCode::NotFound => 404,
ResultCode::InvalidArgument => 400,
ResultCode::TooLarge => 413,
ResultCode::Conflict => 409,
ResultCode::Stale => 503,
ResultCode::VersionSkew => 400,
ResultCode::Unauthenticated => 401,
ResultCode::Backend => 502,
ResultCode::Forbidden => 403,
ResultCode::StepUpRequired => 403,
ResultCode::Unavailable => 503,
ResultCode::ResourceLimit => 429,
ResultCode::Cancelled => 409,
ResultCode::DeadlineExceeded => 408,
ResultCode::ExpiredSnapshot => 410,
ResultCode::StaleGeneration => 409,
ResultCode::TargetUnavailable => 503,
ResultCode::Pending => 503,
ResultCode::BackendUnavailable => 503,
ResultCode::Unrecognized(_) => 500,
}
}
pub const fn is_retryable(self) -> bool {
matches!(
self,
ResultCode::Unavailable
| ResultCode::Stale
| ResultCode::TargetUnavailable
| ResultCode::BackendUnavailable
)
}
pub const fn is_pending(self) -> bool {
matches!(self, ResultCode::Pending)
}
pub const fn is_backend_unavailable(self) -> bool {
matches!(self, ResultCode::BackendUnavailable)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandError {
pub code: ResultCode,
pub message: String,
}
impl CommandError {
pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn unsupported(message: impl Into<String>) -> Self {
Self::new(ResultCode::Unsupported, message)
}
}
impl From<&QueryError> for ResultCode {
fn from(error: &QueryError) -> Self {
match error {
QueryError::Unsupported(_) => ResultCode::Unsupported,
QueryError::Unauthorized(_) => ResultCode::Forbidden,
QueryError::IndexNotFound(_) | QueryError::ForkNotFound(_) => ResultCode::NotFound,
QueryError::Backend(_) => ResultCode::Backend,
QueryError::Unavailable(_) => ResultCode::Unavailable,
QueryError::TooLarge { .. } => ResultCode::TooLarge,
QueryError::Version { .. } => ResultCode::VersionSkew,
QueryError::Stale { .. } => ResultCode::Stale,
QueryError::Cancelled { .. } => ResultCode::Cancelled,
QueryError::DeadlineExceeded { .. } => ResultCode::DeadlineExceeded,
QueryError::ExpiredSnapshot { .. } => ResultCode::ExpiredSnapshot,
QueryError::StaleGeneration { .. } => ResultCode::StaleGeneration,
QueryError::TargetUnavailable { .. } => ResultCode::TargetUnavailable,
QueryError::ResourceLimit { .. } => ResultCode::ResourceLimit,
}
}
}
impl From<&KvError> for ResultCode {
fn from(error: &KvError) -> Self {
match error {
KvError::Unsupported(_) => ResultCode::Unsupported,
KvError::InvalidKey(_) => ResultCode::InvalidArgument,
KvError::InvalidNamespace(_) => ResultCode::InvalidArgument,
KvError::TooLarge { .. } => ResultCode::TooLarge,
KvError::Backend(_) => ResultCode::Backend,
KvError::Unavailable(_) => ResultCode::Unavailable,
KvError::Version { .. } => ResultCode::VersionSkew,
KvError::VersionConflict { .. } => ResultCode::Conflict,
KvError::LeaseLost => ResultCode::Conflict,
KvError::NotFound => ResultCode::NotFound,
KvError::NotLeader => ResultCode::Unavailable,
KvError::Stale { .. } => ResultCode::Stale,
}
}
}
impl From<&ForkError> for ResultCode {
fn from(error: &ForkError) -> Self {
match error {
ForkError::Unsupported(_) => ResultCode::Unsupported,
ForkError::NotFound(_) => ResultCode::NotFound,
ForkError::InvalidFork(_) => ResultCode::InvalidArgument,
ForkError::Conflict(_) => ResultCode::Conflict,
ForkError::Backend(_) => ResultCode::Backend,
ForkError::Unavailable(_) => ResultCode::Unavailable,
ForkError::Version { .. } => ResultCode::VersionSkew,
ForkError::NotLeader => ResultCode::Unavailable,
}
}
}
impl From<&GraphError> for ResultCode {
fn from(error: &GraphError) -> Self {
match error {
GraphError::Unsupported(_) => ResultCode::Unsupported,
GraphError::Unauthorized(_) => ResultCode::Forbidden,
GraphError::InvalidName(_) => ResultCode::InvalidArgument,
GraphError::NotFound(_) => ResultCode::NotFound,
GraphError::TooLarge { .. } => ResultCode::TooLarge,
GraphError::Backend(_) => ResultCode::Backend,
GraphError::Unavailable(_) => ResultCode::Unavailable,
GraphError::Version { .. } => ResultCode::VersionSkew,
}
}
}
impl From<&AuthzError> for ResultCode {
fn from(error: &AuthzError) -> Self {
match error {
AuthzError::Unsupported(_) => ResultCode::Unsupported,
AuthzError::Unauthorized => ResultCode::Forbidden,
AuthzError::UnknownRole(_) => ResultCode::NotFound,
AuthzError::InvalidName(_) | AuthzError::TenancyViolation(_) => {
ResultCode::InvalidArgument
}
AuthzError::Conflict { .. } => ResultCode::Conflict,
AuthzError::Version { .. } => ResultCode::VersionSkew,
}
}
}
impl From<&SessionError> for ResultCode {
fn from(error: &SessionError) -> Self {
match error {
SessionError::Unsupported(_) => ResultCode::Unsupported,
SessionError::NotFound(_) | SessionError::NotRegistered(_) => ResultCode::NotFound,
SessionError::Invalid(_) => ResultCode::InvalidArgument,
SessionError::Unauthorized(_) => ResultCode::Forbidden,
SessionError::Stale(_) => ResultCode::StaleGeneration,
SessionError::Backend(_) => ResultCode::Backend,
SessionError::Unavailable(_) => ResultCode::Unavailable,
}
}
}
impl From<&CheckpointError> for ResultCode {
fn from(error: &CheckpointError) -> Self {
match error {
CheckpointError::Invalid(_) => ResultCode::InvalidArgument,
CheckpointError::NotFound => ResultCode::NotFound,
CheckpointError::Conflict { .. } | CheckpointError::LeaseLost => ResultCode::Conflict,
CheckpointError::Unauthorized => ResultCode::Forbidden,
CheckpointError::Unavailable(_) => ResultCode::Unavailable,
CheckpointError::Version { .. } => ResultCode::VersionSkew,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn given_result_codes_when_mapped_then_should_round_trip_through_the_numeric_value() {
for code in [
ResultCode::Ok,
ResultCode::Unsupported,
ResultCode::NotFound,
ResultCode::InvalidArgument,
ResultCode::TooLarge,
ResultCode::Conflict,
ResultCode::Stale,
ResultCode::VersionSkew,
ResultCode::Unauthenticated,
ResultCode::Backend,
ResultCode::Forbidden,
ResultCode::StepUpRequired,
ResultCode::Unavailable,
ResultCode::TargetUnavailable,
ResultCode::Pending,
ResultCode::BackendUnavailable,
] {
assert_eq!(ResultCode::from_code(code.code()), code);
}
assert_eq!(ResultCode::Pending.code(), 19);
assert_eq!(ResultCode::BackendUnavailable.code(), 20);
assert!(ResultCode::BackendUnavailable.is_retryable());
assert!(ResultCode::BackendUnavailable.is_backend_unavailable());
assert!(!ResultCode::Unavailable.is_backend_unavailable());
assert_eq!(ResultCode::from_code(21), ResultCode::Unrecognized(21));
assert_eq!(ResultCode::from_code(900), ResultCode::Unrecognized(900));
assert_eq!(ResultCode::Unrecognized(900).code(), 900);
}
#[test]
fn given_surface_errors_when_classified_then_should_map_to_the_shared_code() {
assert_eq!(
ResultCode::from(&QueryError::IndexNotFound("readings".to_owned())),
ResultCode::NotFound
);
assert_eq!(
ResultCode::from(&QueryError::Stale {
what: "readings".to_owned(),
applied: 4,
required: 9,
}),
ResultCode::Stale
);
assert_eq!(
ResultCode::from(&KvError::VersionConflict { current: Some(3) }),
ResultCode::Conflict
);
assert_eq!(
ResultCode::from(&ForkError::Conflict("open".to_owned())),
ResultCode::Conflict
);
assert_eq!(
ResultCode::from(&KvError::NotLeader),
ResultCode::Unavailable
);
assert_eq!(
ResultCode::from(&ForkError::NotLeader),
ResultCode::Unavailable
);
assert_eq!(
ResultCode::from(&AuthzError::TenancyViolation("prefix acme".to_owned())),
ResultCode::InvalidArgument
);
assert_eq!(
ResultCode::from(&AuthzError::Unauthorized),
ResultCode::Forbidden
);
}
#[test]
fn given_transient_outcomes_when_classified_then_should_be_the_only_retryable_codes() {
for surface in [
ResultCode::from(&QueryError::Unavailable("dropped".to_owned())),
ResultCode::from(&KvError::Unavailable("dropped".to_owned())),
ResultCode::from(&ForkError::Unavailable("dropped".to_owned())),
] {
assert_eq!(surface, ResultCode::Unavailable);
}
assert_eq!(ResultCode::Unavailable.http_status(), 503);
assert!(ResultCode::Unavailable.is_retryable());
assert!(ResultCode::Stale.is_retryable());
for code in [
ResultCode::Ok,
ResultCode::Unsupported,
ResultCode::NotFound,
ResultCode::InvalidArgument,
ResultCode::TooLarge,
ResultCode::Conflict,
ResultCode::VersionSkew,
ResultCode::Unauthenticated,
ResultCode::Backend,
ResultCode::Forbidden,
ResultCode::StepUpRequired,
ResultCode::Pending,
ResultCode::Unrecognized(900),
] {
assert!(
!code.is_retryable(),
"{code:?} must not invite a blind retry"
);
}
}
#[test]
fn given_result_codes_when_mapped_to_http_then_should_match_the_binding_table() {
assert_eq!(ResultCode::NotFound.http_status(), 404);
assert_eq!(ResultCode::Unsupported.http_status(), 501);
assert_eq!(ResultCode::TooLarge.http_status(), 413);
assert_eq!(ResultCode::Conflict.http_status(), 409);
assert_eq!(ResultCode::Stale.http_status(), 503);
assert_eq!(ResultCode::Unauthenticated.http_status(), 401);
assert_eq!(ResultCode::Forbidden.http_status(), 403);
assert_eq!(ResultCode::StepUpRequired.http_status(), 403);
assert_eq!(ResultCode::Backend.http_status(), 502);
assert_eq!(ResultCode::Unrecognized(777).http_status(), 500);
assert_eq!(ResultCode::Unrecognized(777).code(), 777);
}
#[cfg(feature = "cbor")]
#[test]
fn given_a_result_code_when_round_tripped_through_cbor_then_should_preserve_the_variant() {
use crate::framing::{decode_named, encode_named};
for code in [
ResultCode::Ok,
ResultCode::Conflict,
ResultCode::Stale,
ResultCode::Unrecognized(4242),
] {
let bytes = encode_named(&code).expect("serializes");
let back: ResultCode = decode_named(&bytes).expect("deserializes");
assert_eq!(back, code);
}
}
#[test]
fn given_backend_unavailable_when_serialized_then_should_use_the_backend_unavailable_string() {
let json = serde_json::to_string(&ResultCode::BackendUnavailable).expect("serializes");
assert_eq!(json, "\"backend_unavailable\"");
let back: ResultCode =
serde_json::from_str("\"backend_unavailable\"").expect("deserializes");
assert_eq!(back, ResultCode::BackendUnavailable);
}
#[cfg(feature = "cbor")]
#[test]
fn given_a_command_error_when_round_tripped_then_should_preserve_code_and_message() {
use crate::framing::{decode_named, encode_named};
let error = CommandError::unsupported("AGDX_KV_CAS not served on this build");
assert_eq!(error.code, ResultCode::Unsupported);
let bytes = encode_named(&error).expect("serializes");
let back: CommandError = decode_named(&bytes).expect("deserializes");
assert_eq!(back, error);
}
}