use crate::authz::AuthzError;
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_unavailable(self) -> bool {
matches!(
self,
ResultCode::Unavailable
| ResultCode::TargetUnavailable
| ResultCode::BackendUnavailable
)
}
pub const fn is_permission_denied(self) -> bool {
matches!(self, ResultCode::Forbidden | ResultCode::Unauthenticated)
}
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::Invalid(_) => ResultCode::InvalidArgument,
QueryError::Unauthorized(_) => ResultCode::Forbidden,
QueryError::IndexNotFound(_)
| QueryError::ForkNotFound(_)
| QueryError::SchemaNotFound(_) => ResultCode::NotFound,
QueryError::Backend(_) => ResultCode::Backend,
QueryError::Unavailable(_) => ResultCode::Unavailable,
QueryError::TooLarge { .. } => ResultCode::TooLarge,
QueryError::Version { .. } => ResultCode::VersionSkew,
QueryError::Stale { .. } => ResultCode::Stale,
QueryError::DeadlineExceeded { .. } => ResultCode::DeadlineExceeded,
QueryError::StaleGeneration { .. } => ResultCode::StaleGeneration,
}
}
}
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::LeaseHeld { .. } => ResultCode::Conflict,
KvError::BarrierUnreachable { .. } => ResultCode::StaleGeneration,
KvError::NotFound => ResultCode::NotFound,
KvError::NotLeader => ResultCode::Unavailable,
KvError::Stale { .. } => ResultCode::Stale,
KvError::Forbidden(_) => ResultCode::Forbidden,
KvError::InvalidExpiry(_) => ResultCode::InvalidArgument,
}
}
}
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::InvalidGrant(_)
| AuthzError::TenancyViolation(_) => ResultCode::InvalidArgument,
AuthzError::Conflict { .. } | AuthzError::MutationConflict(_) => ResultCode::Conflict,
AuthzError::Version { .. } => ResultCode::VersionSkew,
AuthzError::ResourceLimit(_) => ResultCode::ResourceLimit,
}
}
}
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,
}
}
}
#[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_one_condition_on_every_surface_when_classified_then_should_share_one_code() {
let text = || "x".to_owned();
let table: [(ResultCode, Vec<ResultCode>); 6] = [
(
ResultCode::InvalidArgument,
vec![
ResultCode::from(&KvError::InvalidKey(text())),
ResultCode::from(&KvError::InvalidNamespace(text())),
ResultCode::from(&ForkError::InvalidFork(text())),
ResultCode::from(&GraphError::InvalidName(text())),
ResultCode::from(&AuthzError::InvalidName(text())),
ResultCode::from(&AuthzError::InvalidGrant(text())),
ResultCode::from(&SessionError::Invalid(text())),
ResultCode::from(&QueryError::Invalid(text())),
],
),
(
ResultCode::Forbidden,
vec![
ResultCode::from(&QueryError::Unauthorized(text())),
ResultCode::from(&GraphError::Unauthorized(text())),
ResultCode::from(&AuthzError::Unauthorized),
ResultCode::from(&SessionError::Unauthorized(text())),
],
),
(
ResultCode::NotFound,
vec![
ResultCode::from(&QueryError::IndexNotFound(text())),
ResultCode::from(&KvError::NotFound),
ResultCode::from(&ForkError::NotFound(text())),
ResultCode::from(&GraphError::NotFound(text())),
ResultCode::from(&AuthzError::UnknownRole(text())),
ResultCode::from(&SessionError::NotFound(text())),
],
),
(
ResultCode::Unavailable,
vec![
ResultCode::from(&QueryError::Unavailable(text())),
ResultCode::from(&KvError::Unavailable(text())),
ResultCode::from(&ForkError::Unavailable(text())),
ResultCode::from(&GraphError::Unavailable(text())),
ResultCode::from(&SessionError::Unavailable(text())),
],
),
(
ResultCode::Unsupported,
vec![
ResultCode::from(&QueryError::Unsupported(text())),
ResultCode::from(&KvError::Unsupported(text())),
ResultCode::from(&ForkError::Unsupported(text())),
ResultCode::from(&GraphError::Unsupported(text())),
ResultCode::from(&AuthzError::Unsupported(text())),
ResultCode::from(&SessionError::Unsupported(text())),
],
),
(
ResultCode::Backend,
vec![
ResultCode::from(&QueryError::Backend(text())),
ResultCode::from(&KvError::Backend(text())),
ResultCode::from(&ForkError::Backend(text())),
ResultCode::from(&GraphError::Backend(text())),
ResultCode::from(&SessionError::Backend(text())),
],
),
];
for (expected, codes) in table {
for (surface, code) in codes.into_iter().enumerate() {
assert_eq!(code, expected, "surface {surface} of {expected:?}");
}
assert!(
!expected.is_retryable() || expected == ResultCode::Unavailable,
"only unavailable is retryable among these"
);
}
}
#[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);
}
}