Skip to main content

made_api/
api_error.rs

1use serde::{Deserialize, Serialize};
2
3/// How this contract fails.
4///
5/// Three 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`, and `Refused` means the engine looked at the request and
8/// said no — retrying it unchanged will not change the answer.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
10pub enum ApiError {
11    #[error("the ceremony engine is unavailable: {reason}")]
12    Unavailable { reason: String },
13
14    #[error("no ceremony named `{ceremony_id}`")]
15    CeremonyNotFound { ceremony_id: String },
16
17    #[error("the ceremony engine refused: {reason}")]
18    Refused { reason: String },
19}
20
21impl ApiError {
22    /// Whether trying again, unchanged, could plausibly succeed.
23    ///
24    /// Published on the error rather than left to the consumer, because a
25    /// consumer keeping its own table of which errors are worth retrying goes
26    /// stale the first time this enum grows.
27    #[must_use]
28    pub fn is_transient(&self) -> bool {
29        matches!(self, Self::Unavailable { .. })
30    }
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    #[test]
38    fn only_unavailability_invites_a_retry() {
39        assert!(ApiError::Unavailable {
40            reason: "starting".to_owned()
41        }
42        .is_transient());
43        assert!(!ApiError::CeremonyNotFound {
44            ceremony_id: "c-1".to_owned()
45        }
46        .is_transient());
47        assert!(
48            !ApiError::Refused {
49                reason: "unbound definition".to_owned()
50            }
51            .is_transient(),
52            "retrying a refusal unchanged asks the same question and earns the \
53             same answer"
54        );
55    }
56}