1use crate::authz::AuthzError;
11use crate::checkpoint::CheckpointError;
12use crate::fork::ForkError;
13use crate::graph::GraphError;
14use crate::kv::KvError;
15use crate::query::QueryError;
16use crate::session::SessionError;
17use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24#[non_exhaustive]
25pub enum ResultCode {
26 Ok,
28 Unsupported,
30 NotFound,
32 InvalidArgument,
34 TooLarge,
36 Conflict,
39 Stale,
42 VersionSkew,
44 Unauthenticated,
46 Backend,
48 Forbidden,
50 StepUpRequired,
53 Unavailable,
61 ResourceLimit,
63 Cancelled,
65 DeadlineExceeded,
67 ExpiredSnapshot,
69 StaleGeneration,
71 TargetUnavailable,
73 Unrecognized(u16),
79}
80
81impl ResultCode {
82 pub const fn code(self) -> u16 {
84 match self {
85 ResultCode::Ok => 0,
86 ResultCode::Unsupported => 1,
87 ResultCode::NotFound => 2,
88 ResultCode::InvalidArgument => 3,
89 ResultCode::TooLarge => 4,
90 ResultCode::Conflict => 5,
91 ResultCode::Stale => 6,
92 ResultCode::VersionSkew => 7,
93 ResultCode::Unauthenticated => 8,
94 ResultCode::Backend => 9,
95 ResultCode::Forbidden => 10,
96 ResultCode::StepUpRequired => 11,
97 ResultCode::Unavailable => 12,
98 ResultCode::ResourceLimit => 13,
99 ResultCode::Cancelled => 14,
100 ResultCode::DeadlineExceeded => 15,
101 ResultCode::ExpiredSnapshot => 16,
102 ResultCode::StaleGeneration => 17,
103 ResultCode::TargetUnavailable => 18,
104 ResultCode::Unrecognized(code) => code,
105 }
106 }
107
108 pub const fn from_code(code: u16) -> Self {
111 match code {
112 0 => ResultCode::Ok,
113 1 => ResultCode::Unsupported,
114 2 => ResultCode::NotFound,
115 3 => ResultCode::InvalidArgument,
116 4 => ResultCode::TooLarge,
117 5 => ResultCode::Conflict,
118 6 => ResultCode::Stale,
119 7 => ResultCode::VersionSkew,
120 8 => ResultCode::Unauthenticated,
121 9 => ResultCode::Backend,
122 10 => ResultCode::Forbidden,
123 11 => ResultCode::StepUpRequired,
124 12 => ResultCode::Unavailable,
125 13 => ResultCode::ResourceLimit,
126 14 => ResultCode::Cancelled,
127 15 => ResultCode::DeadlineExceeded,
128 16 => ResultCode::ExpiredSnapshot,
129 17 => ResultCode::StaleGeneration,
130 18 => ResultCode::TargetUnavailable,
131 other => ResultCode::Unrecognized(other),
132 }
133 }
134
135 pub const fn http_status(self) -> u16 {
138 match self {
139 ResultCode::Ok => 200,
140 ResultCode::Unsupported => 501,
141 ResultCode::NotFound => 404,
142 ResultCode::InvalidArgument => 400,
143 ResultCode::TooLarge => 413,
144 ResultCode::Conflict => 409,
145 ResultCode::Stale => 503,
146 ResultCode::VersionSkew => 400,
147 ResultCode::Unauthenticated => 401,
148 ResultCode::Backend => 502,
149 ResultCode::Forbidden => 403,
153 ResultCode::StepUpRequired => 403,
154 ResultCode::Unavailable => 503,
156 ResultCode::ResourceLimit => 429,
157 ResultCode::Cancelled => 409,
158 ResultCode::DeadlineExceeded => 408,
159 ResultCode::ExpiredSnapshot => 410,
160 ResultCode::StaleGeneration => 409,
161 ResultCode::TargetUnavailable => 503,
162 ResultCode::Unrecognized(_) => 500,
163 }
164 }
165
166 pub const fn is_retryable(self) -> bool {
172 matches!(
173 self,
174 ResultCode::Unavailable | ResultCode::Stale | ResultCode::TargetUnavailable
175 )
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub struct CommandError {
191 pub code: ResultCode,
192 pub message: String,
193}
194
195impl CommandError {
196 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
198 Self {
199 code,
200 message: message.into(),
201 }
202 }
203
204 pub fn unsupported(message: impl Into<String>) -> Self {
206 Self::new(ResultCode::Unsupported, message)
207 }
208}
209
210impl From<&QueryError> for ResultCode {
211 fn from(error: &QueryError) -> Self {
212 match error {
213 QueryError::Unsupported(_) => ResultCode::Unsupported,
214 QueryError::Unauthorized(_) => ResultCode::Forbidden,
217 QueryError::IndexNotFound(_) | QueryError::ForkNotFound(_) => ResultCode::NotFound,
218 QueryError::Backend(_) => ResultCode::Backend,
219 QueryError::Unavailable(_) => ResultCode::Unavailable,
220 QueryError::TooLarge { .. } => ResultCode::TooLarge,
221 QueryError::Version { .. } => ResultCode::VersionSkew,
222 QueryError::Stale { .. } => ResultCode::Stale,
223 QueryError::Cancelled { .. } => ResultCode::Cancelled,
224 QueryError::DeadlineExceeded { .. } => ResultCode::DeadlineExceeded,
225 QueryError::ExpiredSnapshot { .. } => ResultCode::ExpiredSnapshot,
226 QueryError::StaleGeneration { .. } => ResultCode::StaleGeneration,
227 QueryError::TargetUnavailable { .. } => ResultCode::TargetUnavailable,
228 QueryError::ResourceLimit { .. } => ResultCode::ResourceLimit,
229 }
230 }
231}
232
233impl From<&KvError> for ResultCode {
234 fn from(error: &KvError) -> Self {
235 match error {
236 KvError::Unsupported(_) => ResultCode::Unsupported,
237 KvError::InvalidKey(_) => ResultCode::InvalidArgument,
238 KvError::InvalidNamespace(_) => ResultCode::InvalidArgument,
239 KvError::TooLarge { .. } => ResultCode::TooLarge,
240 KvError::Backend(_) => ResultCode::Backend,
241 KvError::Unavailable(_) => ResultCode::Unavailable,
242 KvError::Version { .. } => ResultCode::VersionSkew,
243 KvError::VersionConflict { .. } => ResultCode::Conflict,
244 KvError::LeaseLost => ResultCode::Conflict,
245 KvError::NotFound => ResultCode::NotFound,
246 KvError::NotLeader => ResultCode::Unavailable,
247 KvError::Stale { .. } => ResultCode::Stale,
248 }
249 }
250}
251
252impl From<&ForkError> for ResultCode {
253 fn from(error: &ForkError) -> Self {
254 match error {
255 ForkError::Unsupported(_) => ResultCode::Unsupported,
256 ForkError::NotFound(_) => ResultCode::NotFound,
257 ForkError::InvalidFork(_) => ResultCode::InvalidArgument,
258 ForkError::Conflict(_) => ResultCode::Conflict,
259 ForkError::Backend(_) => ResultCode::Backend,
260 ForkError::Unavailable(_) => ResultCode::Unavailable,
261 ForkError::Version { .. } => ResultCode::VersionSkew,
262 ForkError::NotLeader => ResultCode::Unavailable,
263 }
264 }
265}
266
267impl From<&GraphError> for ResultCode {
268 fn from(error: &GraphError) -> Self {
269 match error {
270 GraphError::Unsupported(_) => ResultCode::Unsupported,
271 GraphError::Unauthorized(_) => ResultCode::Forbidden,
272 GraphError::InvalidName(_) => ResultCode::InvalidArgument,
273 GraphError::NotFound(_) => ResultCode::NotFound,
274 GraphError::TooLarge { .. } => ResultCode::TooLarge,
275 GraphError::Backend(_) => ResultCode::Backend,
276 GraphError::Unavailable(_) => ResultCode::Unavailable,
277 GraphError::Version { .. } => ResultCode::VersionSkew,
278 }
279 }
280}
281
282impl From<&AuthzError> for ResultCode {
283 fn from(error: &AuthzError) -> Self {
284 match error {
285 AuthzError::Unsupported(_) => ResultCode::Unsupported,
286 AuthzError::Unauthorized => ResultCode::Forbidden,
287 AuthzError::UnknownRole(_) => ResultCode::NotFound,
288 AuthzError::InvalidName(_) | AuthzError::TenancyViolation(_) => {
289 ResultCode::InvalidArgument
290 }
291 AuthzError::Conflict { .. } => ResultCode::Conflict,
292 AuthzError::Version { .. } => ResultCode::VersionSkew,
293 }
294 }
295}
296
297impl From<&SessionError> for ResultCode {
298 fn from(error: &SessionError) -> Self {
299 match error {
300 SessionError::Unsupported(_) => ResultCode::Unsupported,
301 SessionError::NotFound(_) | SessionError::NotRegistered(_) => ResultCode::NotFound,
302 SessionError::Invalid(_) => ResultCode::InvalidArgument,
303 SessionError::Unauthorized(_) => ResultCode::Forbidden,
304 SessionError::Stale(_) => ResultCode::StaleGeneration,
305 SessionError::Backend(_) => ResultCode::Backend,
306 SessionError::Unavailable(_) => ResultCode::Unavailable,
307 }
308 }
309}
310
311impl From<&CheckpointError> for ResultCode {
312 fn from(error: &CheckpointError) -> Self {
313 match error {
314 CheckpointError::Invalid(_) => ResultCode::InvalidArgument,
315 CheckpointError::NotFound => ResultCode::NotFound,
316 CheckpointError::Conflict { .. } | CheckpointError::LeaseLost => ResultCode::Conflict,
317 CheckpointError::Unauthorized => ResultCode::Forbidden,
318 CheckpointError::Unavailable(_) => ResultCode::Unavailable,
319 CheckpointError::Version { .. } => ResultCode::VersionSkew,
320 }
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn given_result_codes_when_mapped_then_should_round_trip_through_the_numeric_value() {
330 for code in [
331 ResultCode::Ok,
332 ResultCode::Unsupported,
333 ResultCode::NotFound,
334 ResultCode::InvalidArgument,
335 ResultCode::TooLarge,
336 ResultCode::Conflict,
337 ResultCode::Stale,
338 ResultCode::VersionSkew,
339 ResultCode::Unauthenticated,
340 ResultCode::Backend,
341 ResultCode::Forbidden,
342 ResultCode::StepUpRequired,
343 ResultCode::Unavailable,
344 ] {
345 assert_eq!(ResultCode::from_code(code.code()), code);
346 }
347 assert_eq!(ResultCode::from_code(900), ResultCode::Unrecognized(900));
349 assert_eq!(ResultCode::Unrecognized(900).code(), 900);
350 }
351
352 #[test]
353 fn given_surface_errors_when_classified_then_should_map_to_the_shared_code() {
354 assert_eq!(
355 ResultCode::from(&QueryError::IndexNotFound("readings".to_owned())),
356 ResultCode::NotFound
357 );
358 assert_eq!(
359 ResultCode::from(&QueryError::Stale {
360 what: "readings".to_owned(),
361 applied: 4,
362 required: 9,
363 }),
364 ResultCode::Stale
365 );
366 assert_eq!(
367 ResultCode::from(&KvError::VersionConflict { current: Some(3) }),
368 ResultCode::Conflict
369 );
370 assert_eq!(
371 ResultCode::from(&ForkError::Conflict("open".to_owned())),
372 ResultCode::Conflict
373 );
374 assert_eq!(
375 ResultCode::from(&KvError::NotLeader),
376 ResultCode::Unavailable
377 );
378 assert_eq!(
379 ResultCode::from(&ForkError::NotLeader),
380 ResultCode::Unavailable
381 );
382 assert_eq!(
383 ResultCode::from(&AuthzError::TenancyViolation("prefix acme".to_owned())),
384 ResultCode::InvalidArgument
385 );
386 assert_eq!(
387 ResultCode::from(&AuthzError::Unauthorized),
388 ResultCode::Forbidden
389 );
390 }
391
392 #[test]
395 fn given_transient_outcomes_when_classified_then_should_be_the_only_retryable_codes() {
396 for surface in [
397 ResultCode::from(&QueryError::Unavailable("dropped".to_owned())),
398 ResultCode::from(&KvError::Unavailable("dropped".to_owned())),
399 ResultCode::from(&ForkError::Unavailable("dropped".to_owned())),
400 ] {
401 assert_eq!(surface, ResultCode::Unavailable);
402 }
403 assert_eq!(ResultCode::Unavailable.http_status(), 503);
404 assert!(ResultCode::Unavailable.is_retryable());
405 assert!(ResultCode::Stale.is_retryable());
406 for code in [
407 ResultCode::Ok,
408 ResultCode::Unsupported,
409 ResultCode::NotFound,
410 ResultCode::InvalidArgument,
411 ResultCode::TooLarge,
412 ResultCode::Conflict,
413 ResultCode::VersionSkew,
414 ResultCode::Unauthenticated,
415 ResultCode::Backend,
416 ResultCode::Forbidden,
417 ResultCode::StepUpRequired,
418 ResultCode::Unrecognized(900),
419 ] {
420 assert!(
421 !code.is_retryable(),
422 "{code:?} must not invite a blind retry"
423 );
424 }
425 }
426
427 #[test]
428 fn given_result_codes_when_mapped_to_http_then_should_match_the_binding_table() {
429 assert_eq!(ResultCode::NotFound.http_status(), 404);
430 assert_eq!(ResultCode::Unsupported.http_status(), 501);
431 assert_eq!(ResultCode::TooLarge.http_status(), 413);
432 assert_eq!(ResultCode::Conflict.http_status(), 409);
433 assert_eq!(ResultCode::Stale.http_status(), 503);
434 assert_eq!(ResultCode::Unauthenticated.http_status(), 401);
435 assert_eq!(ResultCode::Forbidden.http_status(), 403);
436 assert_eq!(ResultCode::StepUpRequired.http_status(), 403);
437 assert_eq!(ResultCode::Backend.http_status(), 502);
438 assert_eq!(ResultCode::Unrecognized(777).http_status(), 500);
441 assert_eq!(ResultCode::Unrecognized(777).code(), 777);
442 }
443
444 #[cfg(feature = "cbor")]
445 #[test]
446 fn given_a_result_code_when_round_tripped_through_cbor_then_should_preserve_the_variant() {
447 use crate::framing::{decode_named, encode_named};
448 for code in [
449 ResultCode::Ok,
450 ResultCode::Conflict,
451 ResultCode::Stale,
452 ResultCode::Unrecognized(4242),
453 ] {
454 let bytes = encode_named(&code).expect("serializes");
455 let back: ResultCode = decode_named(&bytes).expect("deserializes");
456 assert_eq!(back, code);
457 }
458 }
459
460 #[cfg(feature = "cbor")]
461 #[test]
462 fn given_a_command_error_when_round_tripped_then_should_preserve_code_and_message() {
463 use crate::framing::{decode_named, encode_named};
464 let error = CommandError::unsupported("AGDX_KV_CAS not served on this build");
465 assert_eq!(error.code, ResultCode::Unsupported);
466 let bytes = encode_named(&error).expect("serializes");
467 let back: CommandError = decode_named(&bytes).expect("deserializes");
468 assert_eq!(back, error);
469 }
470}