brokapi_alor/error.rs
1//! Maps the errors of the protocol layer ([`AlorApiError`]) to the public [`brokapi_core::Error`].
2//!
3//! There are two mappings, and the difference matters. [`map_err`] is for reads. [`map_command_err`] is for
4//! commands that change the account, where "no answer" must never look like "nothing happened".
5
6use std::time::Duration;
7
8use brokapi_alor_protocol::error::AlorApiError;
9use brokapi_core::{ConfigError, Error, TransportError};
10
11/// Maps a failed read (or a failed step before a command was sent) to a public error.
12///
13/// | Protocol error | Public error |
14/// |---|---|
15/// | `RateLimitExceeded`, or `Http` with status 429 | [`Error::RateLimited`] with `retry_after` of 1 second (the broker does not say how long: the value is a fixed guess) |
16/// | `Auth` (the token is refused) | [`Error::Config`] with the broker's message: fix the token, a repeat will not help |
17/// | `Subscription`, `Ws`, `Parse` | [`Error::Transport`] with the message |
18/// | `Http` (any other status or no status) | [`Error::Transport`] with the message |
19/// | `NotFound` (HTTP 404) | [`Error::Rejected`] with code `not_found` (an unknown instrument or order, as at the other brokers) |
20///
21/// All [`Error::Transport`] results are retryable for a read. Do not use this function for a command: it would
22/// turn a lost answer into a plain transport error. Use [`map_command_err`].
23///
24/// # Examples
25///
26/// ```
27/// use brokapi_alor_protocol::error::AlorApiError;
28/// use brokapi_alor::error::map_err;
29/// use brokapi_core::Error;
30///
31/// assert!(matches!(map_err(AlorApiError::RateLimitExceeded), Error::RateLimited { .. }));
32/// assert!(map_err(AlorApiError::NotFound { message: "no".into() }).is_not_found());
33/// assert!(matches!(map_err(AlorApiError::Auth { message: "bad token".into() }), Error::Config(_)));
34/// ```
35pub fn map_err(err: AlorApiError) -> Error {
36 match err {
37 AlorApiError::RateLimitExceeded => Error::RateLimited {
38 retry_after: Duration::from_secs(1),
39 },
40 AlorApiError::Auth { message } => Error::Config(ConfigError::Message(format!(
41 "authentication refused: {message}"
42 ))),
43 AlorApiError::Http {
44 status: Some(429), ..
45 } => Error::RateLimited {
46 retry_after: Duration::from_secs(1),
47 },
48 AlorApiError::Http { message, .. }
49 | AlorApiError::Ws { message, .. }
50 | AlorApiError::Subscription { message }
51 | AlorApiError::Parse(message) => Error::Transport(TransportError::Message(message)),
52 AlorApiError::NotFound { message } => Error::not_found(format!(
53 "the broker does not know this order or instrument: {message}"
54 )),
55 }
56}
57
58/// Maps a failure to build a client from the configuration to [`Error::Config`], with the text of the
59/// original error.
60pub fn map_config_err(err: AlorApiError) -> Error {
61 Error::Config(ConfigError::Message(err.to_string()))
62}
63
64/// A command that failed: only answers that prove the broker did not act become definite errors. No answer, a
65/// break, HTTP 5xx or an unreadable answer may have reached the broker: `Error::Unknown`, never retried by the
66/// library.
67///
68/// | Protocol error | Public error |
69/// |---|---|
70/// | `Http` without a status (no answer, a break, a timeout) or with status 500 and above | [`Error::Unknown`] |
71/// | `Parse` (the answer cannot be read), `Ws` | [`Error::Unknown`] |
72/// | `Http` with status 400 to 499, except 429 | [`Error::Rejected`] with code `http_<status>` and the broker's message |
73/// | everything else (429, `Auth`, `NotFound`, ...) | as in [`map_err`] |
74///
75/// # Examples
76///
77/// ```
78/// use brokapi_alor_protocol::error::AlorApiError;
79/// use brokapi_alor::error::map_command_err;
80/// use brokapi_core::Error;
81///
82/// // No answer at all: the order may exist.
83/// let lost = AlorApiError::Http { status: None, message: "timeout".into() };
84/// assert!(matches!(map_command_err(lost), Error::Unknown));
85/// // The broker answered 503: it may have acted.
86/// let busy = AlorApiError::Http { status: Some(503), message: "busy".into() };
87/// assert!(matches!(map_command_err(busy), Error::Unknown));
88/// // The broker answered 400: it did nothing.
89/// let bad = AlorApiError::Http { status: Some(400), message: "no".into() };
90/// match map_command_err(bad) {
91/// Error::Rejected { code, .. } => assert_eq!(code, "http_400"),
92/// other => panic!("{other:?}"),
93/// }
94/// ```
95pub fn map_command_err(err: AlorApiError) -> Error {
96 match &err {
97 AlorApiError::Http { status, .. } if status.is_none_or(|s| s >= 500) => Error::Unknown,
98 AlorApiError::Parse(_) | AlorApiError::Ws { .. } => Error::Unknown,
99 // The broker answered and said no (400, 401, 403, 409 ...): nothing was done.
100 AlorApiError::Http {
101 status: Some(s),
102 message,
103 } if (400..500).contains(s) && *s != 429 => Error::Rejected {
104 code: format!("http_{s}"),
105 message: message.clone(),
106 },
107 // A 404 on a command on an order: the order is not there (already gone, or never was).
108 AlorApiError::NotFound { message } => Error::Rejected {
109 code: "not_found".into(),
110 message: message.clone(),
111 },
112 _ => map_err(err),
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn lost_commands_are_unknown_refused_ones_are_not() {
122 let http = |status| AlorApiError::Http {
123 status,
124 message: "x".into(),
125 };
126 assert!(matches!(map_command_err(http(None)), Error::Unknown));
127 assert!(matches!(map_command_err(http(Some(503))), Error::Unknown));
128 assert!(matches!(
129 map_command_err(AlorApiError::Parse("x".into())),
130 Error::Unknown
131 ));
132 assert!(matches!(
133 map_command_err(http(Some(429))),
134 Error::RateLimited { .. }
135 ));
136 assert!(matches!(
137 map_command_err(http(Some(403))),
138 Error::Rejected { .. }
139 ));
140 }
141}