openmeteo-rs 1.0.0

Rust client for the Open-Meteo weather API.
Documentation
use std::time::Duration;

/// Crate-wide result type.
pub type Result<T> = std::result::Result<T, Error>;

/// HTTP transport error hidden behind the crate error type.
#[derive(Debug)]
pub struct HttpError {
    source: reqwest::Error,
}

impl HttpError {
    pub(crate) fn new(source: reqwest::Error) -> Self {
        Self { source }
    }
}

impl std::fmt::Display for HttpError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.source.fmt(formatter)
    }
}

impl std::error::Error for HttpError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.source)
    }
}

/// Errors returned by the Open-Meteo client.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// The HTTP request failed before a response could be decoded.
    #[error("HTTP request failed: {0}")]
    Http(#[source] HttpError),

    /// The HTTP request timed out.
    #[error("request timed out after {0:?}")]
    Timeout(Duration),

    /// The Open-Meteo API returned an error response.
    #[error("Open-Meteo API error ({status}): {reason}")]
    Api {
        /// HTTP status code.
        status: u16,
        /// API-provided reason, when available.
        reason: String,
    },

    /// The API returned a rate-limit response.
    #[error("rate limited (HTTP 429); retry-after: {retry_after:?}")]
    RateLimited {
        /// Parsed retry-after duration, if the server provided one in seconds.
        retry_after: Option<Duration>,
    },

    /// JSON decoding failed.
    #[error("failed to decode JSON response: {0}")]
    JsonDecode(#[from] serde_json::Error),

    /// Response content had an unexpected shape.
    #[error("invalid API response: {reason}")]
    InvalidResponse {
        /// Human-readable reason.
        reason: String,
    },

    /// A request parameter was invalid before the request was sent.
    #[error("invalid parameter `{field}`: {reason}")]
    InvalidParam {
        /// Parameter name.
        field: &'static str,
        /// Human-readable reason.
        reason: String,
    },

    /// Two mutually exclusive request parameters were both set.
    #[error("mutually exclusive parameters: {first} and {second} cannot both be set")]
    MutuallyExclusive {
        /// First parameter name.
        first: &'static str,
        /// Second parameter name.
        second: &'static str,
    },

    /// Timestamp parsing failed.
    #[error("failed to parse timestamp `{value}`: {reason}")]
    TimeParse {
        /// Original timestamp value.
        value: String,
        /// Human-readable reason.
        reason: String,
    },
}

impl From<reqwest::Error> for Error {
    fn from(source: reqwest::Error) -> Self {
        Self::Http(HttpError::new(source))
    }
}

pub(crate) fn map_reqwest_error(source: reqwest::Error, timeout: Duration) -> Error {
    if source.is_timeout() {
        Error::Timeout(timeout)
    } else {
        Error::Http(HttpError::new(source))
    }
}