Skip to main content

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}