Skip to main content

openmeteo_rs/
error.rs

1use std::time::Duration;
2
3/// Crate-wide result type.
4pub type Result<T> = std::result::Result<T, Error>;
5
6/// HTTP transport error hidden behind the crate error type.
7#[derive(Debug)]
8pub struct HttpError {
9    source: reqwest::Error,
10}
11
12impl HttpError {
13    pub(crate) fn new(source: reqwest::Error) -> Self {
14        Self { source }
15    }
16}
17
18impl std::fmt::Display for HttpError {
19    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        self.source.fmt(formatter)
21    }
22}
23
24impl std::error::Error for HttpError {
25    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
26        Some(&self.source)
27    }
28}
29
30/// Errors returned by the Open-Meteo client.
31#[derive(Debug, thiserror::Error)]
32#[non_exhaustive]
33pub enum Error {
34    /// The HTTP request failed before a response could be decoded.
35    #[error("HTTP request failed: {0}")]
36    Http(#[source] HttpError),
37
38    /// The HTTP request timed out.
39    #[error("request timed out after {0:?}")]
40    Timeout(Duration),
41
42    /// The Open-Meteo API returned an error response.
43    #[error("Open-Meteo API error ({status}): {reason}")]
44    Api {
45        /// HTTP status code.
46        status: u16,
47        /// API-provided reason, when available.
48        reason: String,
49    },
50
51    /// The API returned a rate-limit response.
52    #[error("rate limited (HTTP 429); retry-after: {retry_after:?}")]
53    RateLimited {
54        /// Parsed retry-after duration, if the server provided one in seconds.
55        retry_after: Option<Duration>,
56    },
57
58    /// JSON decoding failed.
59    #[error("failed to decode JSON response: {0}")]
60    JsonDecode(#[from] serde_json::Error),
61
62    /// Response content had an unexpected shape.
63    #[error("invalid API response: {reason}")]
64    InvalidResponse {
65        /// Human-readable reason.
66        reason: String,
67    },
68
69    /// A request parameter was invalid before the request was sent.
70    #[error("invalid parameter `{field}`: {reason}")]
71    InvalidParam {
72        /// Parameter name.
73        field: &'static str,
74        /// Human-readable reason.
75        reason: String,
76    },
77
78    /// Two mutually exclusive request parameters were both set.
79    #[error("mutually exclusive parameters: {first} and {second} cannot both be set")]
80    MutuallyExclusive {
81        /// First parameter name.
82        first: &'static str,
83        /// Second parameter name.
84        second: &'static str,
85    },
86
87    /// Timestamp parsing failed.
88    #[error("failed to parse timestamp `{value}`: {reason}")]
89    TimeParse {
90        /// Original timestamp value.
91        value: String,
92        /// Human-readable reason.
93        reason: String,
94    },
95}
96
97impl From<reqwest::Error> for Error {
98    fn from(source: reqwest::Error) -> Self {
99        Self::Http(HttpError::new(source))
100    }
101}
102
103pub(crate) fn map_reqwest_error(source: reqwest::Error, timeout: Duration) -> Error {
104    if source.is_timeout() {
105        Error::Timeout(timeout)
106    } else {
107        Error::Http(HttpError::new(source))
108    }
109}