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}