use crate::agent_workflow::AgentError;
use crate::fork::ForkError;
use crate::kv::KvError;
use crate::query::QueryError;
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,
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::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,
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::Unrecognized(_) => 500,
}
}
}
#[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::TooLarge { .. } => ResultCode::TooLarge,
QueryError::Version { .. } => ResultCode::VersionSkew,
QueryError::Stale { .. } => ResultCode::Stale,
}
}
}
impl From<&KvError> for ResultCode {
fn from(error: &KvError) -> Self {
match error {
KvError::Unsupported(_) => ResultCode::Unsupported,
KvError::InvalidKey(_) => ResultCode::InvalidArgument,
KvError::TooLarge { .. } => ResultCode::TooLarge,
KvError::Backend(_) => ResultCode::Backend,
KvError::Version { .. } => ResultCode::VersionSkew,
KvError::VersionConflict { .. } => ResultCode::Conflict,
KvError::LeaseLost => ResultCode::Conflict,
KvError::NotFound => ResultCode::NotFound,
}
}
}
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::Version { .. } => ResultCode::VersionSkew,
}
}
}
impl From<&AgentError> for ResultCode {
fn from(error: &AgentError) -> Self {
match error {
AgentError::Unsupported(_) => ResultCode::Unsupported,
AgentError::NotFound(_) => ResultCode::NotFound,
AgentError::Invalid(_) => ResultCode::InvalidArgument,
AgentError::Backend(_) => ResultCode::Backend,
AgentError::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,
] {
assert_eq!(ResultCode::from_code(code.code()), code);
}
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("orders".to_owned())),
ResultCode::NotFound
);
assert_eq!(
ResultCode::from(&QueryError::Stale {
what: "orders".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
);
}
#[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);
}
}
#[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);
}
}