1use std::time::Duration;
2
3pub type Result<T> = std::result::Result<T, Error>;
5
6#[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#[derive(Debug, thiserror::Error)]
32#[non_exhaustive]
33pub enum Error {
34 #[error("HTTP request failed: {0}")]
36 Http(#[source] HttpError),
37
38 #[error("request timed out after {0:?}")]
40 Timeout(Duration),
41
42 #[error("Open-Meteo API error ({status}): {reason}")]
44 Api {
45 status: u16,
47 reason: String,
49 },
50
51 #[error("rate limited (HTTP 429); retry-after: {retry_after:?}")]
53 RateLimited {
54 retry_after: Option<Duration>,
56 },
57
58 #[error("failed to decode JSON response: {0}")]
60 JsonDecode(#[from] serde_json::Error),
61
62 #[error("invalid API response: {reason}")]
64 InvalidResponse {
65 reason: String,
67 },
68
69 #[error("invalid parameter `{field}`: {reason}")]
71 InvalidParam {
72 field: &'static str,
74 reason: String,
76 },
77
78 #[error("mutually exclusive parameters: {first} and {second} cannot both be set")]
80 MutuallyExclusive {
81 first: &'static str,
83 second: &'static str,
85 },
86
87 #[error("failed to parse timestamp `{value}`: {reason}")]
89 TimeParse {
90 value: String,
92 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}