Skip to main content

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}