made_api/api_error.rs
1use serde::{Deserialize, Serialize};
2
3/// How this contract fails.
4///
5/// Four shapes, because a consumer acts differently on each: waiting is a
6/// remedy for `Unavailable`, asking for something else is the remedy for
7/// `CeremonyNotFound`, reading the session again and trying once more is the
8/// remedy for `Conflict`, and `Refused` means the engine looked at the request
9/// and said no — retrying it unchanged will not change the answer.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
11pub enum ApiError {
12 #[error("the ceremony engine is unavailable: {reason}")]
13 Unavailable { reason: String },
14
15 #[error("no ceremony named `{ceremony_id}`")]
16 CeremonyNotFound { ceremony_id: String },
17
18 /// Somebody else wrote to what this call was writing to.
19 ///
20 /// Distinct from `Refused` because the remedies are opposites: a
21 /// refusal will answer the same way however often it is asked, while a
22 /// lost race is worth reading again and repeating. Folding the two
23 /// together makes a consumer either give up on races it would have won
24 /// or hammer a call that will never succeed.
25 #[error("the ceremony engine lost a race on {what}; read it again and retry")]
26 Conflict { what: String },
27
28 #[error("the ceremony engine refused: {reason}")]
29 Refused { reason: String },
30}
31
32impl ApiError {
33 /// Whether trying again, unchanged, could plausibly succeed.
34 ///
35 /// Published on the error rather than left to the consumer, because a
36 /// consumer keeping its own table of which errors are worth retrying goes
37 /// stale the first time this enum grows.
38 #[must_use]
39 pub fn is_transient(&self) -> bool {
40 matches!(self, Self::Unavailable { .. } | Self::Conflict { .. })
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn what_can_change_by_itself_invites_a_retry() {
50 assert!(ApiError::Unavailable {
51 reason: "starting".to_owned()
52 }
53 .is_transient());
54 assert!(
55 ApiError::Conflict {
56 what: "ceremony_instance".to_owned()
57 }
58 .is_transient(),
59 "a lost race is the one failure repeating unchanged can win"
60 );
61 assert!(!ApiError::CeremonyNotFound {
62 ceremony_id: "c-1".to_owned()
63 }
64 .is_transient());
65 assert!(
66 !ApiError::Refused {
67 reason: "unbound definition".to_owned()
68 }
69 .is_transient(),
70 "retrying a refusal unchanged asks the same question and earns the \
71 same answer"
72 );
73 }
74}