use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
InvalidType,
InvalidName,
InvalidPayload,
NotFound,
Conflict,
SessionMismatch,
ResolveTimeout,
DaemonError,
IoError,
AlreadyDraining,
NotDraining,
AmbiguousId,
ParseError,
ShuttingDown,
Internal,
CaNotInitialized,
CaLocked,
InvalidAuth,
RateLimited,
EnrollmentClosed,
CapabilityDisabled,
NotStandby,
PromotionFailed,
RenewalFailed,
InvalidManifest,
ScopeViolation,
ApprovalDenied,
ApprovalTimeout,
ApprovalUnavailable,
Revoked,
}
impl ErrorCode {
pub fn http_status(&self) -> u16 {
match self {
Self::InvalidType
| Self::InvalidName
| Self::InvalidPayload
| Self::AmbiguousId
| Self::ParseError => 400,
Self::SessionMismatch => 403,
Self::NotFound => 404,
Self::Conflict | Self::AlreadyDraining | Self::NotDraining => 409,
Self::ResolveTimeout => 504,
Self::ShuttingDown
| Self::CaNotInitialized
| Self::CaLocked
| Self::CapabilityDisabled => 503,
Self::InvalidAuth => 401,
Self::RateLimited => 429,
Self::EnrollmentClosed
| Self::NotStandby
| Self::ScopeViolation
| Self::ApprovalDenied => 403,
Self::Revoked => 403,
Self::DaemonError
| Self::IoError
| Self::Internal
| Self::PromotionFailed
| Self::RenewalFailed => 500,
Self::InvalidManifest => 400,
Self::ApprovalTimeout => 504,
Self::ApprovalUnavailable => 503,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_code_serializes_to_snake_case() {
assert_eq!(
serde_json::to_value(ErrorCode::InvalidType).unwrap(),
"invalid_type"
);
assert_eq!(
serde_json::to_value(ErrorCode::NotFound).unwrap(),
"not_found"
);
assert_eq!(
serde_json::to_value(ErrorCode::AlreadyDraining).unwrap(),
"already_draining"
);
}
#[test]
fn all_error_code_variants_map_to_expected_http_status() {
let cases: Vec<(ErrorCode, u16)> = vec![
(ErrorCode::InvalidType, 400),
(ErrorCode::InvalidName, 400),
(ErrorCode::InvalidPayload, 400),
(ErrorCode::AmbiguousId, 400),
(ErrorCode::ParseError, 400),
(ErrorCode::InvalidAuth, 401),
(ErrorCode::SessionMismatch, 403),
(ErrorCode::EnrollmentClosed, 403),
(ErrorCode::NotFound, 404),
(ErrorCode::Conflict, 409),
(ErrorCode::AlreadyDraining, 409),
(ErrorCode::NotDraining, 409),
(ErrorCode::RateLimited, 429),
(ErrorCode::InvalidManifest, 400),
(ErrorCode::NotStandby, 403),
(ErrorCode::ScopeViolation, 403),
(ErrorCode::Revoked, 403),
(ErrorCode::ApprovalDenied, 403),
(ErrorCode::DaemonError, 500),
(ErrorCode::IoError, 500),
(ErrorCode::Internal, 500),
(ErrorCode::PromotionFailed, 500),
(ErrorCode::RenewalFailed, 500),
(ErrorCode::ShuttingDown, 503),
(ErrorCode::CaNotInitialized, 503),
(ErrorCode::CaLocked, 503),
(ErrorCode::CapabilityDisabled, 503),
(ErrorCode::ApprovalUnavailable, 503),
(ErrorCode::ResolveTimeout, 504),
(ErrorCode::ApprovalTimeout, 504),
];
for (code, expected_status) in &cases {
assert_eq!(
code.http_status(),
*expected_status,
"{code:?} should map to HTTP {expected_status}"
);
}
}
#[test]
fn all_error_code_variants_roundtrip_through_json() {
let variants: Vec<(ErrorCode, &str)> = vec![
(ErrorCode::InvalidType, "invalid_type"),
(ErrorCode::InvalidName, "invalid_name"),
(ErrorCode::InvalidPayload, "invalid_payload"),
(ErrorCode::NotFound, "not_found"),
(ErrorCode::Conflict, "conflict"),
(ErrorCode::SessionMismatch, "session_mismatch"),
(ErrorCode::ResolveTimeout, "resolve_timeout"),
(ErrorCode::DaemonError, "daemon_error"),
(ErrorCode::IoError, "io_error"),
(ErrorCode::AlreadyDraining, "already_draining"),
(ErrorCode::NotDraining, "not_draining"),
(ErrorCode::AmbiguousId, "ambiguous_id"),
(ErrorCode::ParseError, "parse_error"),
(ErrorCode::ShuttingDown, "shutting_down"),
(ErrorCode::Internal, "internal"),
(ErrorCode::CaNotInitialized, "ca_not_initialized"),
(ErrorCode::CaLocked, "ca_locked"),
(ErrorCode::InvalidAuth, "invalid_auth"),
(ErrorCode::RateLimited, "rate_limited"),
(ErrorCode::EnrollmentClosed, "enrollment_closed"),
(ErrorCode::CapabilityDisabled, "capability_disabled"),
(ErrorCode::NotStandby, "not_standby"),
(ErrorCode::PromotionFailed, "promotion_failed"),
(ErrorCode::RenewalFailed, "renewal_failed"),
(ErrorCode::InvalidManifest, "invalid_manifest"),
(ErrorCode::ScopeViolation, "scope_violation"),
(ErrorCode::Revoked, "revoked"),
(ErrorCode::ApprovalDenied, "approval_denied"),
(ErrorCode::ApprovalTimeout, "approval_timeout"),
(ErrorCode::ApprovalUnavailable, "approval_unavailable"),
];
for (code, expected_str) in &variants {
let serialized = serde_json::to_value(code).unwrap();
assert_eq!(
serialized, *expected_str,
"{code:?} should serialize to \"{expected_str}\""
);
let deserialized: ErrorCode = serde_json::from_value(serialized).unwrap();
assert_eq!(
&deserialized, code,
"\"{expected_str}\" should deserialize back to {code:?}"
);
}
}
}