1use crate::authz::AuthzError;
11use crate::fork::ForkError;
12use crate::graph::GraphError;
13use crate::kv::KvError;
14use crate::query::QueryError;
15use crate::session::SessionError;
16use serde::{Deserialize, Serialize};
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum ResultCode {
25 Ok,
27 Unsupported,
29 NotFound,
31 InvalidArgument,
33 TooLarge,
35 Conflict,
38 Stale,
41 VersionSkew,
43 Unauthenticated,
45 Backend,
47 Forbidden,
49 StepUpRequired,
52 Unavailable,
60 ResourceLimit,
62 Cancelled,
64 DeadlineExceeded,
66 ExpiredSnapshot,
68 StaleGeneration,
70 TargetUnavailable,
72 Pending,
77 BackendUnavailable,
81 Unrecognized(u16),
87}
88
89impl ResultCode {
90 pub const fn code(self) -> u16 {
92 match self {
93 ResultCode::Ok => 0,
94 ResultCode::Unsupported => 1,
95 ResultCode::NotFound => 2,
96 ResultCode::InvalidArgument => 3,
97 ResultCode::TooLarge => 4,
98 ResultCode::Conflict => 5,
99 ResultCode::Stale => 6,
100 ResultCode::VersionSkew => 7,
101 ResultCode::Unauthenticated => 8,
102 ResultCode::Backend => 9,
103 ResultCode::Forbidden => 10,
104 ResultCode::StepUpRequired => 11,
105 ResultCode::Unavailable => 12,
106 ResultCode::ResourceLimit => 13,
107 ResultCode::Cancelled => 14,
108 ResultCode::DeadlineExceeded => 15,
109 ResultCode::ExpiredSnapshot => 16,
110 ResultCode::StaleGeneration => 17,
111 ResultCode::TargetUnavailable => 18,
112 ResultCode::Pending => 19,
113 ResultCode::BackendUnavailable => 20,
114 ResultCode::Unrecognized(code) => code,
115 }
116 }
117
118 pub const fn from_code(code: u16) -> Self {
121 match code {
122 0 => ResultCode::Ok,
123 1 => ResultCode::Unsupported,
124 2 => ResultCode::NotFound,
125 3 => ResultCode::InvalidArgument,
126 4 => ResultCode::TooLarge,
127 5 => ResultCode::Conflict,
128 6 => ResultCode::Stale,
129 7 => ResultCode::VersionSkew,
130 8 => ResultCode::Unauthenticated,
131 9 => ResultCode::Backend,
132 10 => ResultCode::Forbidden,
133 11 => ResultCode::StepUpRequired,
134 12 => ResultCode::Unavailable,
135 13 => ResultCode::ResourceLimit,
136 14 => ResultCode::Cancelled,
137 15 => ResultCode::DeadlineExceeded,
138 16 => ResultCode::ExpiredSnapshot,
139 17 => ResultCode::StaleGeneration,
140 18 => ResultCode::TargetUnavailable,
141 19 => ResultCode::Pending,
142 20 => ResultCode::BackendUnavailable,
143 other => ResultCode::Unrecognized(other),
144 }
145 }
146
147 pub const fn http_status(self) -> u16 {
150 match self {
151 ResultCode::Ok => 200,
152 ResultCode::Unsupported => 501,
153 ResultCode::NotFound => 404,
154 ResultCode::InvalidArgument => 400,
155 ResultCode::TooLarge => 413,
156 ResultCode::Conflict => 409,
157 ResultCode::Stale => 503,
158 ResultCode::VersionSkew => 400,
159 ResultCode::Unauthenticated => 401,
160 ResultCode::Backend => 502,
161 ResultCode::Forbidden => 403,
165 ResultCode::StepUpRequired => 403,
166 ResultCode::Unavailable => 503,
168 ResultCode::ResourceLimit => 429,
169 ResultCode::Cancelled => 409,
170 ResultCode::DeadlineExceeded => 408,
171 ResultCode::ExpiredSnapshot => 410,
172 ResultCode::StaleGeneration => 409,
173 ResultCode::TargetUnavailable => 503,
174 ResultCode::Pending => 503,
177 ResultCode::BackendUnavailable => 503,
178 ResultCode::Unrecognized(_) => 500,
179 }
180 }
181
182 pub const fn is_retryable(self) -> bool {
190 matches!(
191 self,
192 ResultCode::Unavailable
193 | ResultCode::Stale
194 | ResultCode::TargetUnavailable
195 | ResultCode::BackendUnavailable
196 )
197 }
198
199 pub const fn is_unavailable(self) -> bool {
203 matches!(
204 self,
205 ResultCode::Unavailable
206 | ResultCode::TargetUnavailable
207 | ResultCode::BackendUnavailable
208 )
209 }
210
211 pub const fn is_permission_denied(self) -> bool {
215 matches!(self, ResultCode::Forbidden | ResultCode::Unauthenticated)
216 }
217
218 pub const fn is_pending(self) -> bool {
221 matches!(self, ResultCode::Pending)
222 }
223
224 pub const fn is_backend_unavailable(self) -> bool {
231 matches!(self, ResultCode::BackendUnavailable)
232 }
233}
234
235#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
246pub struct CommandError {
247 pub code: ResultCode,
248 pub message: String,
249}
250
251impl CommandError {
252 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
254 Self {
255 code,
256 message: message.into(),
257 }
258 }
259
260 pub fn unsupported(message: impl Into<String>) -> Self {
262 Self::new(ResultCode::Unsupported, message)
263 }
264}
265
266impl From<&QueryError> for ResultCode {
267 fn from(error: &QueryError) -> Self {
268 match error {
269 QueryError::Unsupported(_) => ResultCode::Unsupported,
270 QueryError::Invalid(_) => ResultCode::InvalidArgument,
271 QueryError::Unauthorized(_) => ResultCode::Forbidden,
274 QueryError::IndexNotFound(_)
275 | QueryError::ForkNotFound(_)
276 | QueryError::SchemaNotFound(_) => ResultCode::NotFound,
277 QueryError::Backend(_) => ResultCode::Backend,
278 QueryError::Unavailable(_) => ResultCode::Unavailable,
279 QueryError::TooLarge { .. } => ResultCode::TooLarge,
280 QueryError::Version { .. } => ResultCode::VersionSkew,
281 QueryError::Stale { .. } => ResultCode::Stale,
282 QueryError::DeadlineExceeded { .. } => ResultCode::DeadlineExceeded,
283 QueryError::StaleGeneration { .. } => ResultCode::StaleGeneration,
284 }
285 }
286}
287
288impl From<&KvError> for ResultCode {
289 fn from(error: &KvError) -> Self {
290 match error {
291 KvError::Unsupported(_) => ResultCode::Unsupported,
292 KvError::InvalidKey(_) => ResultCode::InvalidArgument,
293 KvError::InvalidNamespace(_) => ResultCode::InvalidArgument,
294 KvError::TooLarge { .. } => ResultCode::TooLarge,
295 KvError::Backend(_) => ResultCode::Backend,
296 KvError::Unavailable(_) => ResultCode::Unavailable,
297 KvError::Version { .. } => ResultCode::VersionSkew,
298 KvError::VersionConflict { .. } => ResultCode::Conflict,
299 KvError::LeaseLost => ResultCode::Conflict,
300 KvError::LeaseHeld { .. } => ResultCode::Conflict,
301 KvError::BarrierUnreachable { .. } => ResultCode::StaleGeneration,
302 KvError::NotFound => ResultCode::NotFound,
303 KvError::NotLeader => ResultCode::Unavailable,
304 KvError::Stale { .. } => ResultCode::Stale,
305 KvError::Forbidden(_) => ResultCode::Forbidden,
306 KvError::InvalidExpiry(_) => ResultCode::InvalidArgument,
307 }
308 }
309}
310
311impl From<&ForkError> for ResultCode {
312 fn from(error: &ForkError) -> Self {
313 match error {
314 ForkError::Unsupported(_) => ResultCode::Unsupported,
315 ForkError::NotFound(_) => ResultCode::NotFound,
316 ForkError::InvalidFork(_) => ResultCode::InvalidArgument,
317 ForkError::Conflict(_) => ResultCode::Conflict,
318 ForkError::Backend(_) => ResultCode::Backend,
319 ForkError::Unavailable(_) => ResultCode::Unavailable,
320 ForkError::Version { .. } => ResultCode::VersionSkew,
321 ForkError::NotLeader => ResultCode::Unavailable,
322 }
323 }
324}
325
326impl From<&GraphError> for ResultCode {
327 fn from(error: &GraphError) -> Self {
328 match error {
329 GraphError::Unsupported(_) => ResultCode::Unsupported,
330 GraphError::Unauthorized(_) => ResultCode::Forbidden,
331 GraphError::InvalidName(_) => ResultCode::InvalidArgument,
332 GraphError::NotFound(_) => ResultCode::NotFound,
333 GraphError::TooLarge { .. } => ResultCode::TooLarge,
334 GraphError::Backend(_) => ResultCode::Backend,
335 GraphError::Unavailable(_) => ResultCode::Unavailable,
336 GraphError::Version { .. } => ResultCode::VersionSkew,
337 }
338 }
339}
340
341impl From<&AuthzError> for ResultCode {
342 fn from(error: &AuthzError) -> Self {
343 match error {
344 AuthzError::Unsupported(_) => ResultCode::Unsupported,
345 AuthzError::Unauthorized => ResultCode::Forbidden,
346 AuthzError::UnknownRole(_) => ResultCode::NotFound,
347 AuthzError::InvalidName(_)
348 | AuthzError::InvalidGrant(_)
349 | AuthzError::TenancyViolation(_) => ResultCode::InvalidArgument,
350 AuthzError::Conflict { .. } | AuthzError::MutationConflict(_) => ResultCode::Conflict,
351 AuthzError::Version { .. } => ResultCode::VersionSkew,
352 AuthzError::ResourceLimit(_) => ResultCode::ResourceLimit,
353 }
354 }
355}
356
357impl From<&SessionError> for ResultCode {
358 fn from(error: &SessionError) -> Self {
359 match error {
360 SessionError::Unsupported(_) => ResultCode::Unsupported,
361 SessionError::NotFound(_) | SessionError::NotRegistered(_) => ResultCode::NotFound,
362 SessionError::Invalid(_) => ResultCode::InvalidArgument,
363 SessionError::Unauthorized(_) => ResultCode::Forbidden,
364 SessionError::Stale(_) => ResultCode::StaleGeneration,
365 SessionError::Backend(_) => ResultCode::Backend,
366 SessionError::Unavailable(_) => ResultCode::Unavailable,
367 }
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn given_result_codes_when_mapped_then_should_round_trip_through_the_numeric_value() {
377 for code in [
378 ResultCode::Ok,
379 ResultCode::Unsupported,
380 ResultCode::NotFound,
381 ResultCode::InvalidArgument,
382 ResultCode::TooLarge,
383 ResultCode::Conflict,
384 ResultCode::Stale,
385 ResultCode::VersionSkew,
386 ResultCode::Unauthenticated,
387 ResultCode::Backend,
388 ResultCode::Forbidden,
389 ResultCode::StepUpRequired,
390 ResultCode::Unavailable,
391 ResultCode::TargetUnavailable,
392 ResultCode::Pending,
393 ResultCode::BackendUnavailable,
394 ] {
395 assert_eq!(ResultCode::from_code(code.code()), code);
396 }
397 assert_eq!(ResultCode::Pending.code(), 19);
398 assert_eq!(ResultCode::BackendUnavailable.code(), 20);
399 assert!(ResultCode::BackendUnavailable.is_retryable());
400 assert!(ResultCode::BackendUnavailable.is_backend_unavailable());
401 assert!(!ResultCode::Unavailable.is_backend_unavailable());
402 assert_eq!(ResultCode::from_code(21), ResultCode::Unrecognized(21));
403 assert_eq!(ResultCode::from_code(900), ResultCode::Unrecognized(900));
405 assert_eq!(ResultCode::Unrecognized(900).code(), 900);
406 }
407
408 #[test]
409 fn given_surface_errors_when_classified_then_should_map_to_the_shared_code() {
410 assert_eq!(
411 ResultCode::from(&QueryError::IndexNotFound("readings".to_owned())),
412 ResultCode::NotFound
413 );
414 assert_eq!(
415 ResultCode::from(&QueryError::Stale {
416 what: "readings".to_owned(),
417 applied: 4,
418 required: 9,
419 }),
420 ResultCode::Stale
421 );
422 assert_eq!(
423 ResultCode::from(&KvError::VersionConflict { current: Some(3) }),
424 ResultCode::Conflict
425 );
426 assert_eq!(
427 ResultCode::from(&ForkError::Conflict("open".to_owned())),
428 ResultCode::Conflict
429 );
430 assert_eq!(
431 ResultCode::from(&KvError::NotLeader),
432 ResultCode::Unavailable
433 );
434 assert_eq!(
435 ResultCode::from(&ForkError::NotLeader),
436 ResultCode::Unavailable
437 );
438 assert_eq!(
439 ResultCode::from(&AuthzError::TenancyViolation("prefix acme".to_owned())),
440 ResultCode::InvalidArgument
441 );
442 assert_eq!(
443 ResultCode::from(&AuthzError::Unauthorized),
444 ResultCode::Forbidden
445 );
446 }
447
448 #[test]
451 fn given_one_condition_on_every_surface_when_classified_then_should_share_one_code() {
452 let text = || "x".to_owned();
453 let table: [(ResultCode, Vec<ResultCode>); 6] = [
454 (
455 ResultCode::InvalidArgument,
456 vec![
457 ResultCode::from(&KvError::InvalidKey(text())),
458 ResultCode::from(&KvError::InvalidNamespace(text())),
459 ResultCode::from(&ForkError::InvalidFork(text())),
460 ResultCode::from(&GraphError::InvalidName(text())),
461 ResultCode::from(&AuthzError::InvalidName(text())),
462 ResultCode::from(&AuthzError::InvalidGrant(text())),
463 ResultCode::from(&SessionError::Invalid(text())),
464 ResultCode::from(&QueryError::Invalid(text())),
465 ],
466 ),
467 (
468 ResultCode::Forbidden,
469 vec![
470 ResultCode::from(&QueryError::Unauthorized(text())),
471 ResultCode::from(&GraphError::Unauthorized(text())),
472 ResultCode::from(&AuthzError::Unauthorized),
473 ResultCode::from(&SessionError::Unauthorized(text())),
474 ],
475 ),
476 (
477 ResultCode::NotFound,
478 vec![
479 ResultCode::from(&QueryError::IndexNotFound(text())),
480 ResultCode::from(&KvError::NotFound),
481 ResultCode::from(&ForkError::NotFound(text())),
482 ResultCode::from(&GraphError::NotFound(text())),
483 ResultCode::from(&AuthzError::UnknownRole(text())),
484 ResultCode::from(&SessionError::NotFound(text())),
485 ],
486 ),
487 (
488 ResultCode::Unavailable,
489 vec![
490 ResultCode::from(&QueryError::Unavailable(text())),
491 ResultCode::from(&KvError::Unavailable(text())),
492 ResultCode::from(&ForkError::Unavailable(text())),
493 ResultCode::from(&GraphError::Unavailable(text())),
494 ResultCode::from(&SessionError::Unavailable(text())),
495 ],
496 ),
497 (
498 ResultCode::Unsupported,
499 vec![
500 ResultCode::from(&QueryError::Unsupported(text())),
501 ResultCode::from(&KvError::Unsupported(text())),
502 ResultCode::from(&ForkError::Unsupported(text())),
503 ResultCode::from(&GraphError::Unsupported(text())),
504 ResultCode::from(&AuthzError::Unsupported(text())),
505 ResultCode::from(&SessionError::Unsupported(text())),
506 ],
507 ),
508 (
509 ResultCode::Backend,
510 vec![
511 ResultCode::from(&QueryError::Backend(text())),
512 ResultCode::from(&KvError::Backend(text())),
513 ResultCode::from(&ForkError::Backend(text())),
514 ResultCode::from(&GraphError::Backend(text())),
515 ResultCode::from(&SessionError::Backend(text())),
516 ],
517 ),
518 ];
519 for (expected, codes) in table {
520 for (surface, code) in codes.into_iter().enumerate() {
521 assert_eq!(code, expected, "surface {surface} of {expected:?}");
522 }
523 assert!(
524 !expected.is_retryable() || expected == ResultCode::Unavailable,
525 "only unavailable is retryable among these"
526 );
527 }
528 }
529
530 #[test]
533 fn given_transient_outcomes_when_classified_then_should_be_the_only_retryable_codes() {
534 for surface in [
535 ResultCode::from(&QueryError::Unavailable("dropped".to_owned())),
536 ResultCode::from(&KvError::Unavailable("dropped".to_owned())),
537 ResultCode::from(&ForkError::Unavailable("dropped".to_owned())),
538 ] {
539 assert_eq!(surface, ResultCode::Unavailable);
540 }
541 assert_eq!(ResultCode::Unavailable.http_status(), 503);
542 assert!(ResultCode::Unavailable.is_retryable());
543 assert!(ResultCode::Stale.is_retryable());
544 for code in [
545 ResultCode::Ok,
546 ResultCode::Unsupported,
547 ResultCode::NotFound,
548 ResultCode::InvalidArgument,
549 ResultCode::TooLarge,
550 ResultCode::Conflict,
551 ResultCode::VersionSkew,
552 ResultCode::Unauthenticated,
553 ResultCode::Backend,
554 ResultCode::Forbidden,
555 ResultCode::StepUpRequired,
556 ResultCode::Pending,
557 ResultCode::Unrecognized(900),
558 ] {
559 assert!(
560 !code.is_retryable(),
561 "{code:?} must not invite a blind retry"
562 );
563 }
564 }
565
566 #[test]
567 fn given_result_codes_when_mapped_to_http_then_should_match_the_binding_table() {
568 assert_eq!(ResultCode::NotFound.http_status(), 404);
569 assert_eq!(ResultCode::Unsupported.http_status(), 501);
570 assert_eq!(ResultCode::TooLarge.http_status(), 413);
571 assert_eq!(ResultCode::Conflict.http_status(), 409);
572 assert_eq!(ResultCode::Stale.http_status(), 503);
573 assert_eq!(ResultCode::Unauthenticated.http_status(), 401);
574 assert_eq!(ResultCode::Forbidden.http_status(), 403);
575 assert_eq!(ResultCode::StepUpRequired.http_status(), 403);
576 assert_eq!(ResultCode::Backend.http_status(), 502);
577 assert_eq!(ResultCode::Unrecognized(777).http_status(), 500);
580 assert_eq!(ResultCode::Unrecognized(777).code(), 777);
581 }
582
583 #[cfg(feature = "cbor")]
584 #[test]
585 fn given_a_result_code_when_round_tripped_through_cbor_then_should_preserve_the_variant() {
586 use crate::framing::{decode_named, encode_named};
587 for code in [
588 ResultCode::Ok,
589 ResultCode::Conflict,
590 ResultCode::Stale,
591 ResultCode::Unrecognized(4242),
592 ] {
593 let bytes = encode_named(&code).expect("serializes");
594 let back: ResultCode = decode_named(&bytes).expect("deserializes");
595 assert_eq!(back, code);
596 }
597 }
598
599 #[test]
600 fn given_backend_unavailable_when_serialized_then_should_use_the_backend_unavailable_string() {
601 let json = serde_json::to_string(&ResultCode::BackendUnavailable).expect("serializes");
602 assert_eq!(json, "\"backend_unavailable\"");
603 let back: ResultCode =
604 serde_json::from_str("\"backend_unavailable\"").expect("deserializes");
605 assert_eq!(back, ResultCode::BackendUnavailable);
606 }
607
608 #[cfg(feature = "cbor")]
609 #[test]
610 fn given_a_command_error_when_round_tripped_then_should_preserve_code_and_message() {
611 use crate::framing::{decode_named, encode_named};
612 let error = CommandError::unsupported("AGDX_KV_CAS not served on this build");
613 assert_eq!(error.code, ResultCode::Unsupported);
614 let bytes = encode_named(&error).expect("serializes");
615 let back: CommandError = decode_named(&bytes).expect("deserializes");
616 assert_eq!(back, error);
617 }
618}