Skip to main content

trading_ig/
error.rs

1//! Crate-wide error type.
2//!
3//! Domain modules **must not** define their own error enum — extend [`Error`]
4//! instead. Variants are intentionally coarse-grained; the `Api` variant
5//! carries IG's machine-readable `errorCode` for fine matching by callers.
6
7use http::StatusCode;
8use serde::Deserialize;
9
10/// Convenience alias used throughout the crate.
11pub type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    #[error("HTTP transport error: {0}")]
16    Http(#[from] reqwest::Error),
17
18    #[error("IG API error ({status}): {0}", .source.error_code)]
19    Api {
20        status: StatusCode,
21        #[source]
22        source: ApiError,
23    },
24
25    #[error("authentication error: {0}")]
26    Auth(String),
27
28    #[error("rate limited by IG ({0})")]
29    RateLimited(String),
30
31    #[error("failed to deserialise response: {0}")]
32    Deserialization(#[from] serde_json::Error),
33
34    #[error("invalid configuration: {0}")]
35    Config(String),
36
37    #[error("invalid input: {0}")]
38    InvalidInput(String),
39
40    #[error("URL error: {0}")]
41    Url(#[from] url::ParseError),
42
43    #[error("invalid HTTP header value: {0}")]
44    HeaderValue(#[from] http::header::InvalidHeaderValue),
45}
46
47/// Wire-level error payload returned by IG.
48///
49/// Most endpoints return `{ "errorCode": "…" }` on failure. Some include
50/// additional context fields, surfaced via the `extra` map.
51#[derive(Debug, Clone, Deserialize, thiserror::Error)]
52#[error("{error_code}")]
53pub struct ApiError {
54    #[serde(rename = "errorCode")]
55    pub error_code: String,
56    #[serde(flatten, default)]
57    pub extra: serde_json::Map<String, serde_json::Value>,
58}
59
60impl Error {
61    /// True if the error is a `Auth` variant or an `Api` error whose code
62    /// indicates a token issue (e.g. token expired / invalid).
63    pub fn is_auth(&self) -> bool {
64        match self {
65            Error::Auth(_) => true,
66            Error::Api { source, .. } => {
67                let c = source.error_code.as_str();
68                c.contains("oauth-token-invalid")
69                    || c.contains("client-token-missing")
70                    || c.contains("security-token")
71            }
72            _ => false,
73        }
74    }
75
76    /// True if the error indicates the rate limit has been hit.
77    pub fn is_rate_limited(&self) -> bool {
78        match self {
79            Error::RateLimited(_) => true,
80            Error::Api { source, .. } => source.error_code.contains("api-rate-exceeded"),
81            _ => false,
82        }
83    }
84}