brokapi_finam/error.rs
1//! Map protocol errors to public connector errors.
2//!
3//! Two mappings exist, and the difference matters:
4//!
5//! - [`map_protocol_err`] for queries and subscriptions. A failure can be retried safely.
6//! - [`map_command_err`] for `place` and `cancel`. Anything that may have reached the broker is
7//! `Error::Unknown`, because a retry could create a second order.
8//!
9//! | gRPC status | Query or stream | Command |
10//! |---|---|---|
11//! | `RESOURCE_EXHAUSTED` | `Error::RateLimited` | `Error::RateLimited` |
12//! | `UNAUTHENTICATED` (and a login the broker refused) | `Error::Config` | `Error::Config` |
13//! | `NOT_FOUND` | `Error::Rejected`, code `not_found` | `Error::Rejected`, code `not_found` |
14//! | `PERMISSION_DENIED`, `INVALID_ARGUMENT`, `FAILED_PRECONDITION`, `OUT_OF_RANGE`, `ALREADY_EXISTS`, `UNIMPLEMENTED` | `Error::Rejected` | `Error::Rejected` |
15//! | anything else (`UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INTERNAL`, ...) | `Error::Transport` | `Error::Unknown` |
16//!
17//! # Examples
18//!
19//! ```
20//! use brokapi_core::Error;
21//! use brokapi_finam::error::{map_command_err, map_protocol_err};
22//! use brokapi_finam_protocol::FinamError;
23//! use tonic::Code;
24//!
25//! let lost = FinamError::Status { code: Code::Unavailable, message: "connection reset".into() };
26//! // A read can simply be repeated.
27//! assert!(matches!(map_protocol_err(&lost), Error::Transport(_)));
28//! // A command may or may not have been applied.
29//! assert!(matches!(map_command_err(&lost), Error::Unknown));
30//!
31//! let refused = FinamError::Status { code: Code::InvalidArgument, message: "bad price".into() };
32//! assert!(matches!(map_command_err(&refused), Error::Rejected { .. }));
33//! ```
34
35use std::time::Duration;
36
37use brokapi_core::{ConfigError, Error, TransportError};
38use brokapi_finam_protocol::FinamError;
39use tonic::Code;
40
41/// Errors of queries and subscriptions. Commands use [`map_command_err`].
42///
43/// `RESOURCE_EXHAUSTED` becomes [`Error::RateLimited`] with a fixed `retry_after` of one second. Finam
44/// documents the quota (200 requests per minute) but not the wait, so the pause is a guess, not
45/// verified against the live API.
46pub fn map_protocol_err(err: &FinamError) -> Error {
47 match err {
48 FinamError::Config(m) => Error::Config(ConfigError::Message(m.clone())),
49 // A refused login is a problem of the secret, as at the other brokers: `Config`.
50 FinamError::Auth(m) => Error::Config(ConfigError::Message(format!("unauthenticated: {m}"))),
51 FinamError::Status { code, message } => match code {
52 // Finam's documented quota is 200 requests per minute; the wait is a guess.
53 Code::ResourceExhausted => Error::RateLimited {
54 retry_after: Duration::from_secs(1),
55 },
56 Code::Unauthenticated => {
57 Error::Config(ConfigError::Message(format!("unauthenticated: {message}")))
58 }
59 Code::PermissionDenied
60 | Code::NotFound
61 | Code::InvalidArgument
62 | Code::FailedPrecondition
63 | Code::OutOfRange
64 | Code::AlreadyExists
65 | Code::Unimplemented => Error::Rejected {
66 code: code_name(*code).into(),
67 message: message.clone(),
68 },
69 _ => Error::Transport(TransportError::Message(format!(
70 "{}: {message}",
71 code_name(*code)
72 ))),
73 },
74 }
75}
76
77/// Errors of `place` / `cancel`: anything that may have reached the broker is `Unknown`
78///, never `Transport` or `Rejected`.
79///
80/// Only a status where the server certainly did not process the request (see
81/// [`FinamError::is_definite_rejection`]) is mapped as in [`map_protocol_err`]. A dropped connection or
82/// a timeout is [`Error::Unknown`]: resolve it by asking for the order state, never by sending the
83/// command again.
84pub fn map_command_err(err: &FinamError) -> Error {
85 match err {
86 FinamError::Status { .. } if !err.is_definite_rejection() => Error::Unknown,
87 _ => map_protocol_err(err),
88 }
89}
90
91/// Lowercase name of a gRPC status code (`invalid_argument`), used as the `code` of `Error::Rejected`.
92pub fn code_name(code: Code) -> &'static str {
93 match code {
94 Code::Ok => "ok",
95 Code::Cancelled => "cancelled",
96 Code::Unknown => "unknown",
97 Code::InvalidArgument => "invalid_argument",
98 Code::DeadlineExceeded => "deadline_exceeded",
99 Code::NotFound => brokapi_core::NOT_FOUND,
100 Code::AlreadyExists => "already_exists",
101 Code::PermissionDenied => "permission_denied",
102 Code::ResourceExhausted => "resource_exhausted",
103 Code::FailedPrecondition => "failed_precondition",
104 Code::Aborted => "aborted",
105 Code::OutOfRange => "out_of_range",
106 Code::Unimplemented => "unimplemented",
107 Code::Internal => "internal",
108 Code::Unavailable => "unavailable",
109 Code::DataLoss => "data_loss",
110 Code::Unauthenticated => "unauthenticated",
111 }
112}
113
114/// A [`Error::Transport`] with a text. For failures of this process (a broken stream, an unknown
115/// subscription), not for answers of the broker.
116pub fn transport(msg: impl Into<String>) -> Error {
117 Error::Transport(TransportError::Message(msg.into()))
118}
119
120/// An [`Error::Unsupported`] naming the capability the broker does not offer (for example `replace_order`).
121pub fn unsupported(capability: &'static str) -> Error {
122 Error::Unsupported {
123 capability: capability.into(),
124 }
125}