Skip to main content

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}