openmeteo-rs 1.0.0

Rust client for the Open-Meteo weather API.
Documentation
use std::borrow::Cow;

use super::{PressureLevel, SoilMoistureDepth, SoilTemperatureDepth, TowerLevel};
use crate::query::AsApiStr;
use crate::{Error, Result};

/// Hourly variables for the general forecast endpoint.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum HourlyVar {
    /// Air temperature at 2 m.
    Temperature2m,
    /// Air temperature at a tower level.
    ///
    /// The forecast endpoint documents temperature tower variables at 80 m,
    /// 120 m, and 180 m. `TowerLevel::M10` is rejected by the forecast
    /// builder.
    TemperatureAtTower(TowerLevel),
    /// Relative humidity at 2 m.
    RelativeHumidity2m,
    /// Dew point at 2 m.
    DewPoint2m,
    /// Apparent temperature.
    ApparentTemperature,
    /// Wet-bulb temperature at 2 m.
    WetBulbTemperature2m,
    /// Mean sea-level pressure.
    PressureMsl,
    /// Surface pressure.
    SurfacePressure,
    /// Total cloud cover.
    CloudCover,
    /// Low-level cloud cover.
    CloudCoverLow,
    /// Mid-level cloud cover.
    CloudCoverMid,
    /// High-level cloud cover.
    CloudCoverHigh,
    /// Probability of precipitation.
    PrecipitationProbability,
    /// Precipitation amount.
    Precipitation,
    /// Rain amount.
    Rain,
    /// Showers amount.
    Showers,
    /// Snowfall amount.
    Snowfall,
    /// Snow depth on the ground.
    SnowDepth,
    /// WMO weather code.
    WeatherCode,
    /// Visibility distance.
    Visibility,
    /// Evapotranspiration from land surface and plants.
    Evapotranspiration,
    /// FAO reference evapotranspiration.
    Et0FaoEvapotranspiration,
    /// Vapour pressure deficit.
    VapourPressureDeficit,
    /// Day/night flag.
    IsDay,
    /// Sunshine duration.
    SunshineDuration,
    /// UV index.
    UvIndex,
    /// Clear-sky UV index.
    UvIndexClearSky,
    /// Total column integrated water vapour.
    TotalColumnIntegratedWaterVapour,
    /// Shortwave solar radiation, averaged over the preceding hour.
    ShortwaveRadiation,
    /// Direct solar radiation, averaged over the preceding hour.
    DirectRadiation,
    /// Direct normal irradiance, averaged over the preceding hour.
    DirectNormalIrradiance,
    /// Diffuse solar radiation, averaged over the preceding hour.
    DiffuseRadiation,
    /// Global tilted irradiance, averaged over the preceding hour.
    GlobalTiltedIrradiance,
    /// Terrestrial solar radiation, averaged over the preceding hour.
    TerrestrialRadiation,
    /// Instantaneous shortwave solar radiation.
    ShortwaveRadiationInstant,
    /// Instantaneous direct solar radiation.
    DirectRadiationInstant,
    /// Instantaneous direct normal irradiance.
    DirectNormalIrradianceInstant,
    /// Instantaneous diffuse solar radiation.
    DiffuseRadiationInstant,
    /// Instantaneous global tilted irradiance.
    GlobalTiltedIrradianceInstant,
    /// Instantaneous terrestrial solar radiation.
    TerrestrialRadiationInstant,
    /// Convective available potential energy.
    Cape,
    /// Lifted index.
    LiftedIndex,
    /// Convective inhibition.
    ConvectiveInhibition,
    /// Freezing level height.
    FreezingLevelHeight,
    /// Atmospheric boundary layer height.
    BoundaryLayerHeight,
    /// Wind speed at 10 m.
    WindSpeed10m,
    /// Wind speed at a tower or near-surface level.
    ///
    /// The forecast endpoint documents wind speed at 10 m, 80 m, 120 m, and
    /// 180 m.
    WindSpeedAtTower(TowerLevel),
    /// Wind direction at 10 m.
    WindDirection10m,
    /// Wind direction at a tower or near-surface level.
    ///
    /// The forecast endpoint documents wind direction at 10 m, 80 m, 120 m,
    /// and 180 m.
    WindDirectionAtTower(TowerLevel),
    /// Wind gusts at 10 m.
    WindGusts10m,
    /// Soil temperature at a supported depth.
    SoilTemperature(SoilTemperatureDepth),
    /// Soil moisture between two supported depth boundaries.
    ///
    /// The builder validates that `from` is shallower than `to` when the
    /// request is sent or converted to a URL.
    SoilMoisture {
        /// Upper depth boundary.
        from: SoilMoistureDepth,
        /// Lower depth boundary.
        to: SoilMoistureDepth,
    },
    /// Temperature at a pressure level.
    TemperatureAtPressure(PressureLevel),
    /// Relative humidity at a pressure level.
    RelativeHumidityAtPressure(PressureLevel),
    /// Dew point at a pressure level.
    DewPointAtPressure(PressureLevel),
    /// Cloud cover at a pressure level.
    CloudCoverAtPressure(PressureLevel),
    /// Wind speed at a pressure level.
    WindSpeedAtPressure(PressureLevel),
    /// Wind direction at a pressure level.
    WindDirectionAtPressure(PressureLevel),
    /// Geopotential height at a pressure level.
    GeopotentialHeightAtPressure(PressureLevel),
    /// Exact Open-Meteo hourly variable token not yet represented by this enum.
    ///
    /// This is primarily useful for additive upstream variables and endpoint
    /// suffixes such as previous-runs `_previous_dayN` columns. The token is
    /// passed through unchanged and is not validated by the crate.
    Other(Cow<'static, str>),
}

impl AsApiStr for HourlyVar {
    fn as_api_str(&self) -> Cow<'static, str> {
        match self {
            Self::Temperature2m => Cow::Borrowed("temperature_2m"),
            Self::TemperatureAtTower(level) => {
                Cow::Owned(format!("temperature_{}m", level.meters()))
            }
            Self::RelativeHumidity2m => Cow::Borrowed("relative_humidity_2m"),
            Self::DewPoint2m => Cow::Borrowed("dew_point_2m"),
            Self::ApparentTemperature => Cow::Borrowed("apparent_temperature"),
            Self::WetBulbTemperature2m => Cow::Borrowed("wet_bulb_temperature_2m"),
            Self::PressureMsl => Cow::Borrowed("pressure_msl"),
            Self::SurfacePressure => Cow::Borrowed("surface_pressure"),
            Self::CloudCover => Cow::Borrowed("cloud_cover"),
            Self::CloudCoverLow => Cow::Borrowed("cloud_cover_low"),
            Self::CloudCoverMid => Cow::Borrowed("cloud_cover_mid"),
            Self::CloudCoverHigh => Cow::Borrowed("cloud_cover_high"),
            Self::PrecipitationProbability => Cow::Borrowed("precipitation_probability"),
            Self::Precipitation => Cow::Borrowed("precipitation"),
            Self::Rain => Cow::Borrowed("rain"),
            Self::Showers => Cow::Borrowed("showers"),
            Self::Snowfall => Cow::Borrowed("snowfall"),
            Self::SnowDepth => Cow::Borrowed("snow_depth"),
            Self::WeatherCode => Cow::Borrowed("weather_code"),
            Self::Visibility => Cow::Borrowed("visibility"),
            Self::Evapotranspiration => Cow::Borrowed("evapotranspiration"),
            Self::Et0FaoEvapotranspiration => Cow::Borrowed("et0_fao_evapotranspiration"),
            Self::VapourPressureDeficit => Cow::Borrowed("vapour_pressure_deficit"),
            Self::IsDay => Cow::Borrowed("is_day"),
            Self::SunshineDuration => Cow::Borrowed("sunshine_duration"),
            Self::UvIndex => Cow::Borrowed("uv_index"),
            Self::UvIndexClearSky => Cow::Borrowed("uv_index_clear_sky"),
            Self::TotalColumnIntegratedWaterVapour => {
                Cow::Borrowed("total_column_integrated_water_vapour")
            }
            Self::ShortwaveRadiation => Cow::Borrowed("shortwave_radiation"),
            Self::DirectRadiation => Cow::Borrowed("direct_radiation"),
            Self::DirectNormalIrradiance => Cow::Borrowed("direct_normal_irradiance"),
            Self::DiffuseRadiation => Cow::Borrowed("diffuse_radiation"),
            Self::GlobalTiltedIrradiance => Cow::Borrowed("global_tilted_irradiance"),
            Self::TerrestrialRadiation => Cow::Borrowed("terrestrial_radiation"),
            Self::ShortwaveRadiationInstant => Cow::Borrowed("shortwave_radiation_instant"),
            Self::DirectRadiationInstant => Cow::Borrowed("direct_radiation_instant"),
            Self::DirectNormalIrradianceInstant => {
                Cow::Borrowed("direct_normal_irradiance_instant")
            }
            Self::DiffuseRadiationInstant => Cow::Borrowed("diffuse_radiation_instant"),
            Self::GlobalTiltedIrradianceInstant => {
                Cow::Borrowed("global_tilted_irradiance_instant")
            }
            Self::TerrestrialRadiationInstant => Cow::Borrowed("terrestrial_radiation_instant"),
            Self::Cape => Cow::Borrowed("cape"),
            Self::LiftedIndex => Cow::Borrowed("lifted_index"),
            Self::ConvectiveInhibition => Cow::Borrowed("convective_inhibition"),
            Self::FreezingLevelHeight => Cow::Borrowed("freezing_level_height"),
            Self::BoundaryLayerHeight => Cow::Borrowed("boundary_layer_height"),
            Self::WindSpeed10m => Cow::Borrowed("wind_speed_10m"),
            Self::WindSpeedAtTower(level) => Cow::Owned(format!("wind_speed_{}m", level.meters())),
            Self::WindDirection10m => Cow::Borrowed("wind_direction_10m"),
            Self::WindDirectionAtTower(level) => {
                Cow::Owned(format!("wind_direction_{}m", level.meters()))
            }
            Self::WindGusts10m => Cow::Borrowed("wind_gusts_10m"),
            Self::SoilTemperature(depth) => {
                Cow::Owned(format!("soil_temperature_{}cm", depth.cm()))
            }
            Self::SoilMoisture { from, to } => {
                Cow::Owned(format!("soil_moisture_{}_to_{}cm", from.cm(), to.cm()))
            }
            Self::TemperatureAtPressure(level) => {
                Cow::Owned(format!("temperature_{}hPa", level.hpa()))
            }
            Self::RelativeHumidityAtPressure(level) => {
                Cow::Owned(format!("relative_humidity_{}hPa", level.hpa()))
            }
            Self::DewPointAtPressure(level) => Cow::Owned(format!("dew_point_{}hPa", level.hpa())),
            Self::CloudCoverAtPressure(level) => {
                Cow::Owned(format!("cloud_cover_{}hPa", level.hpa()))
            }
            Self::WindSpeedAtPressure(level) => {
                Cow::Owned(format!("wind_speed_{}hPa", level.hpa()))
            }
            Self::WindDirectionAtPressure(level) => {
                Cow::Owned(format!("wind_direction_{}hPa", level.hpa()))
            }
            Self::GeopotentialHeightAtPressure(level) => {
                Cow::Owned(format!("geopotential_height_{}hPa", level.hpa()))
            }
            Self::Other(token) => token.clone(),
        }
    }
}

impl HourlyVar {
    /// Creates an exact Open-Meteo hourly variable token not yet represented by this enum.
    ///
    /// The token is passed through unchanged and is not validated by the crate.
    pub fn other(token: impl Into<Cow<'static, str>>) -> Self {
        Self::Other(token.into())
    }

    pub(crate) fn validate_weather_request(&self) -> Result<()> {
        if let Self::SoilMoisture { from, to } = self
            && from.cm() >= to.cm()
        {
            return Err(Error::InvalidParam {
                field: "hourly.soil_moisture",
                reason: "soil moisture depth ranges must be ordered from shallower to deeper depth"
                    .into(),
            });
        }

        if let Self::TemperatureAtTower(TowerLevel::M10) = self {
            return Err(Error::InvalidParam {
                field: "hourly.temperature_at_tower",
                reason: "temperature tower variables are available at 80, 120, and 180 m".into(),
            });
        }

        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::query::AsApiStr;

    #[test]
    fn pressure_level_hourly_tokens_are_formatted() {
        assert_eq!(
            HourlyVar::TemperatureAtPressure(PressureLevel::Hpa850).as_api_str(),
            "temperature_850hPa"
        );
        assert_eq!(
            HourlyVar::DewPointAtPressure(PressureLevel::Hpa975).as_api_str(),
            "dew_point_975hPa"
        );
        assert_eq!(
            HourlyVar::CloudCoverAtPressure(PressureLevel::Hpa30).as_api_str(),
            "cloud_cover_30hPa"
        );
    }

    #[test]
    fn tower_altitude_tokens_are_formatted() {
        assert_eq!(
            HourlyVar::TemperatureAtTower(TowerLevel::M80).as_api_str(),
            "temperature_80m"
        );
        assert_eq!(
            HourlyVar::WindSpeedAtTower(TowerLevel::M120).as_api_str(),
            "wind_speed_120m"
        );
        assert_eq!(
            HourlyVar::WindDirectionAtTower(TowerLevel::M180).as_api_str(),
            "wind_direction_180m"
        );
    }
}