Skip to main content

brokapi_finam_protocol/
error.rs

1//! Protocol-layer errors.
2//!
3//! [`FinamError`] is what this crate returns. The connector crate (`brokapi-finam`) maps it to the
4//! public `brokapi_core::Error`: a gRPC status that may have reached the broker becomes
5//! `Error::Unknown` for commands.
6
7use thiserror::Error;
8use tonic::Code;
9
10/// Error of the Finam protocol layer: a gRPC status, a configuration problem or a refused login.
11///
12/// The `Display` text never contains the API secret or a token.
13///
14/// # Examples
15///
16/// ```
17/// use brokapi_finam_protocol::FinamError;
18/// use tonic::Code;
19///
20/// let err = FinamError::from(tonic::Status::new(Code::ResourceExhausted, "too many requests"));
21/// assert_eq!(err.code(), Some(Code::ResourceExhausted));
22/// assert!(err.is_rate_limited());
23/// // The server said "no" before doing anything: a command was not applied.
24/// assert!(err.is_definite_rejection());
25///
26/// // A dropped connection may have happened after the request was written.
27/// let lost = FinamError::from(tonic::Status::new(Code::Unavailable, "connection reset"));
28/// assert!(!lost.is_definite_rejection());
29/// ```
30#[derive(Error, Debug, Clone)]
31pub enum FinamError {
32    /// gRPC status returned by the server (or synthesized by the transport).
33    #[error("grpc {code:?}: {message}")]
34    Status {
35        /// gRPC status code. A failed connection also arrives as a status, the code depends on the failure.
36        code: Code,
37        /// Text sent by the server (or made up by the transport layer).
38        message: String,
39    },
40    /// Bad endpoint, empty secret, malformed token payload.
41    #[error("config: {0}")]
42    Config(String),
43    /// Authentication failed (bad secret, token refused).
44    #[error("auth: {0}")]
45    Auth(String),
46}
47
48impl From<tonic::Status> for FinamError {
49    fn from(s: tonic::Status) -> Self {
50        Self::Status {
51            code: s.code(),
52            message: s.message().to_string(),
53        }
54    }
55}
56
57impl FinamError {
58    /// gRPC status code, or `None` for [`FinamError::Config`] and [`FinamError::Auth`].
59    pub fn code(&self) -> Option<Code> {
60        match self {
61            Self::Status { code, .. } => Some(*code),
62            _ => None,
63        }
64    }
65
66    /// `true` for status `UNAUTHENTICATED`: the JWT is missing, wrong or expired.
67    ///
68    /// A read may renew the token and repeat. A command must not be repeated automatically.
69    pub fn is_unauthenticated(&self) -> bool {
70        self.code() == Some(Code::Unauthenticated)
71    }
72
73    /// `true` for status `RESOURCE_EXHAUSTED`: the request quota is spent and the request was not run.
74    pub fn is_rate_limited(&self) -> bool {
75        self.code() == Some(Code::ResourceExhausted)
76    }
77
78    /// The server certainly did not process the request (so a command was NOT applied).
79    ///
80    /// Conservative: `Unavailable` is NOT here, because a connection can drop after the request
81    /// was written. For commands every other failure is an unknown outcome.
82    pub fn is_definite_rejection(&self) -> bool {
83        matches!(
84            self.code(),
85            Some(
86                Code::InvalidArgument
87                    | Code::NotFound
88                    | Code::PermissionDenied
89                    | Code::FailedPrecondition
90                    | Code::Unauthenticated
91                    | Code::ResourceExhausted
92                    | Code::OutOfRange
93                    | Code::Unimplemented
94                    | Code::AlreadyExists
95            )
96        )
97    }
98}