1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
7#[serde(rename_all = "snake_case")]
8pub enum ErrorCode {
9 InvalidType,
10 InvalidName,
11 InvalidPayload,
12 NotFound,
13 Conflict,
14 SessionMismatch,
15 ResolveTimeout,
16 DaemonError,
17 IoError,
18 AlreadyDraining,
19 NotDraining,
20 AmbiguousId,
21 ParseError,
22 ShuttingDown,
23 Internal,
24 CaNotInitialized,
26 CaLocked,
27 InvalidAuth,
28 RateLimited,
29 EnrollmentClosed,
30 CapabilityDisabled,
31 NotStandby,
33 PromotionFailed,
34 RenewalFailed,
35 InvalidManifest,
36 ScopeViolation,
38 ApprovalDenied,
39 ApprovalTimeout,
40 ApprovalUnavailable,
41 Revoked,
43}
44
45impl ErrorCode {
46 pub fn http_status(&self) -> u16 {
49 match self {
50 Self::InvalidType
51 | Self::InvalidName
52 | Self::InvalidPayload
53 | Self::AmbiguousId
54 | Self::ParseError => 400,
55 Self::SessionMismatch => 403,
56 Self::NotFound => 404,
57 Self::Conflict | Self::AlreadyDraining | Self::NotDraining => 409,
58 Self::ResolveTimeout => 504,
59 Self::ShuttingDown
60 | Self::CaNotInitialized
61 | Self::CaLocked
62 | Self::CapabilityDisabled => 503,
63 Self::InvalidAuth => 401,
64 Self::RateLimited => 429,
65 Self::EnrollmentClosed
66 | Self::NotStandby
67 | Self::ScopeViolation
68 | Self::ApprovalDenied => 403,
69 Self::Revoked => 403,
70 Self::DaemonError
71 | Self::IoError
72 | Self::Internal
73 | Self::PromotionFailed
74 | Self::RenewalFailed => 500,
75 Self::InvalidManifest => 400,
76 Self::ApprovalTimeout => 504,
77 Self::ApprovalUnavailable => 503,
78 }
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn error_code_serializes_to_snake_case() {
88 assert_eq!(
89 serde_json::to_value(ErrorCode::InvalidType).unwrap(),
90 "invalid_type"
91 );
92 assert_eq!(
93 serde_json::to_value(ErrorCode::NotFound).unwrap(),
94 "not_found"
95 );
96 assert_eq!(
97 serde_json::to_value(ErrorCode::AlreadyDraining).unwrap(),
98 "already_draining"
99 );
100 }
101
102 #[test]
106 fn all_error_code_variants_map_to_expected_http_status() {
107 let cases: Vec<(ErrorCode, u16)> = vec![
108 (ErrorCode::InvalidType, 400),
110 (ErrorCode::InvalidName, 400),
111 (ErrorCode::InvalidPayload, 400),
112 (ErrorCode::AmbiguousId, 400),
113 (ErrorCode::ParseError, 400),
114 (ErrorCode::InvalidAuth, 401),
116 (ErrorCode::SessionMismatch, 403),
118 (ErrorCode::EnrollmentClosed, 403),
119 (ErrorCode::NotFound, 404),
121 (ErrorCode::Conflict, 409),
123 (ErrorCode::AlreadyDraining, 409),
124 (ErrorCode::NotDraining, 409),
125 (ErrorCode::RateLimited, 429),
127 (ErrorCode::InvalidManifest, 400),
129 (ErrorCode::NotStandby, 403),
131 (ErrorCode::ScopeViolation, 403),
133 (ErrorCode::Revoked, 403),
134 (ErrorCode::ApprovalDenied, 403),
135 (ErrorCode::DaemonError, 500),
137 (ErrorCode::IoError, 500),
138 (ErrorCode::Internal, 500),
139 (ErrorCode::PromotionFailed, 500),
140 (ErrorCode::RenewalFailed, 500),
141 (ErrorCode::ShuttingDown, 503),
143 (ErrorCode::CaNotInitialized, 503),
144 (ErrorCode::CaLocked, 503),
145 (ErrorCode::CapabilityDisabled, 503),
146 (ErrorCode::ApprovalUnavailable, 503),
147 (ErrorCode::ResolveTimeout, 504),
149 (ErrorCode::ApprovalTimeout, 504),
150 ];
151 for (code, expected_status) in &cases {
152 assert_eq!(
153 code.http_status(),
154 *expected_status,
155 "{code:?} should map to HTTP {expected_status}"
156 );
157 }
158 }
159
160 #[test]
162 fn all_error_code_variants_roundtrip_through_json() {
163 let variants: Vec<(ErrorCode, &str)> = vec![
164 (ErrorCode::InvalidType, "invalid_type"),
165 (ErrorCode::InvalidName, "invalid_name"),
166 (ErrorCode::InvalidPayload, "invalid_payload"),
167 (ErrorCode::NotFound, "not_found"),
168 (ErrorCode::Conflict, "conflict"),
169 (ErrorCode::SessionMismatch, "session_mismatch"),
170 (ErrorCode::ResolveTimeout, "resolve_timeout"),
171 (ErrorCode::DaemonError, "daemon_error"),
172 (ErrorCode::IoError, "io_error"),
173 (ErrorCode::AlreadyDraining, "already_draining"),
174 (ErrorCode::NotDraining, "not_draining"),
175 (ErrorCode::AmbiguousId, "ambiguous_id"),
176 (ErrorCode::ParseError, "parse_error"),
177 (ErrorCode::ShuttingDown, "shutting_down"),
178 (ErrorCode::Internal, "internal"),
179 (ErrorCode::CaNotInitialized, "ca_not_initialized"),
180 (ErrorCode::CaLocked, "ca_locked"),
181 (ErrorCode::InvalidAuth, "invalid_auth"),
182 (ErrorCode::RateLimited, "rate_limited"),
183 (ErrorCode::EnrollmentClosed, "enrollment_closed"),
184 (ErrorCode::CapabilityDisabled, "capability_disabled"),
185 (ErrorCode::NotStandby, "not_standby"),
186 (ErrorCode::PromotionFailed, "promotion_failed"),
187 (ErrorCode::RenewalFailed, "renewal_failed"),
188 (ErrorCode::InvalidManifest, "invalid_manifest"),
189 (ErrorCode::ScopeViolation, "scope_violation"),
190 (ErrorCode::Revoked, "revoked"),
191 (ErrorCode::ApprovalDenied, "approval_denied"),
192 (ErrorCode::ApprovalTimeout, "approval_timeout"),
193 (ErrorCode::ApprovalUnavailable, "approval_unavailable"),
194 ];
195 for (code, expected_str) in &variants {
196 let serialized = serde_json::to_value(code).unwrap();
197 assert_eq!(
198 serialized, *expected_str,
199 "{code:?} should serialize to \"{expected_str}\""
200 );
201
202 let deserialized: ErrorCode = serde_json::from_value(serialized).unwrap();
203 assert_eq!(
204 &deserialized, code,
205 "\"{expected_str}\" should deserialize back to {code:?}"
206 );
207 }
208 }
209}