use std::sync::Arc;
use std::time::Duration;
use reqwest::Url;
use time::Date;
use crate::endpoints::air_quality::AirQualityBuilder;
use crate::endpoints::archive::ArchiveBuilder;
use crate::endpoints::climate::ClimateBuilder;
use crate::endpoints::ensemble::EnsembleBuilder;
use crate::endpoints::flood::FloodBuilder;
use crate::endpoints::forecast::{ForecastBatchBuilder, ForecastBuilder};
use crate::endpoints::geocoding::GeocodingBuilder;
use crate::endpoints::historical_forecast::HistoricalForecastBuilder;
use crate::endpoints::http::read_success_body;
use crate::endpoints::marine::MarineBuilder;
use crate::endpoints::previous_runs::PreviousRunsBuilder;
use crate::endpoints::satellite_radiation::SatelliteRadiationBuilder;
use crate::endpoints::seasonal::SeasonalBuilder;
use crate::error::map_reqwest_error;
use crate::{Error, Result};
const DEFAULT_FORECAST_BASE: &str = "https://api.open-meteo.com";
const DEFAULT_ARCHIVE_BASE: &str = "https://archive-api.open-meteo.com";
const DEFAULT_HISTORICAL_FORECAST_BASE: &str = "https://historical-forecast-api.open-meteo.com";
const DEFAULT_PREVIOUS_RUNS_BASE: &str = "https://previous-runs-api.open-meteo.com";
const DEFAULT_ENSEMBLE_BASE: &str = "https://ensemble-api.open-meteo.com";
const DEFAULT_SEASONAL_BASE: &str = "https://seasonal-api.open-meteo.com";
const DEFAULT_CLIMATE_BASE: &str = "https://climate-api.open-meteo.com";
const DEFAULT_SATELLITE_RADIATION_BASE: &str = "https://satellite-api.open-meteo.com";
const DEFAULT_FLOOD_BASE: &str = "https://flood-api.open-meteo.com";
const DEFAULT_MARINE_BASE: &str = "https://marine-api.open-meteo.com";
const DEFAULT_AIR_QUALITY_BASE: &str = "https://air-quality-api.open-meteo.com";
const DEFAULT_ELEVATION_BASE: &str = "https://api.open-meteo.com";
const DEFAULT_GEOCODING_BASE: &str = "https://geocoding-api.open-meteo.com";
const CUSTOMER_FORECAST_BASE: &str = "https://customer-api.open-meteo.com";
const CUSTOMER_ARCHIVE_BASE: &str = "https://customer-archive-api.open-meteo.com";
const CUSTOMER_HISTORICAL_FORECAST_BASE: &str =
"https://customer-historical-forecast-api.open-meteo.com";
const CUSTOMER_ELEVATION_BASE: &str = "https://customer-api.open-meteo.com";
const CUSTOMER_GEOCODING_BASE: &str = "https://customer-geocoding-api.open-meteo.com";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub struct Client {
pub(crate) http: reqwest::Client,
pub(crate) forecast_base: Url,
pub(crate) archive_base: Url,
pub(crate) historical_forecast_base: Url,
pub(crate) previous_runs_base: Url,
pub(crate) ensemble_base: Url,
pub(crate) seasonal_base: Url,
pub(crate) climate_base: Url,
pub(crate) satellite_radiation_base: Url,
pub(crate) flood_base: Url,
pub(crate) marine_base: Url,
pub(crate) air_quality_base: Url,
pub(crate) elevation_base: Url,
pub(crate) geocoding_base: Url,
pub(crate) api_key: Option<Arc<str>>,
pub(crate) timeout: Duration,
}
impl Client {
pub fn new() -> Self {
Self::builder()
.build()
.expect("default Open-Meteo client configuration is valid")
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn forecast(&self, latitude: f64, longitude: f64) -> ForecastBuilder<'_> {
ForecastBuilder::new(self, latitude, longitude)
}
pub fn forecast_batch<I>(&self, locations: I) -> ForecastBatchBuilder<'_>
where
I: IntoIterator<Item = (f64, f64)>,
{
ForecastBatchBuilder::new(self, locations)
}
pub fn forecast_many<I>(&self, locations: I) -> ForecastBatchBuilder<'_>
where
I: IntoIterator<Item = (f64, f64)>,
{
self.forecast_batch(locations)
}
pub fn archive(
&self,
latitude: f64,
longitude: f64,
start_date: Date,
end_date: Date,
) -> ArchiveBuilder<'_> {
ArchiveBuilder::new(self, latitude, longitude, start_date, end_date)
}
pub fn historical_forecast(
&self,
latitude: f64,
longitude: f64,
start_date: Date,
end_date: Date,
) -> HistoricalForecastBuilder<'_> {
HistoricalForecastBuilder::new(self, latitude, longitude, start_date, end_date)
}
pub fn previous_runs(&self, latitude: f64, longitude: f64) -> PreviousRunsBuilder<'_> {
PreviousRunsBuilder::new(self, latitude, longitude)
}
pub fn ensemble(&self, latitude: f64, longitude: f64) -> EnsembleBuilder<'_> {
EnsembleBuilder::new(self, latitude, longitude)
}
pub fn seasonal(&self, latitude: f64, longitude: f64) -> SeasonalBuilder<'_> {
SeasonalBuilder::new(self, latitude, longitude)
}
pub fn climate(
&self,
latitude: f64,
longitude: f64,
start_date: Date,
end_date: Date,
) -> ClimateBuilder<'_> {
ClimateBuilder::new(self, latitude, longitude, start_date, end_date)
}
pub fn satellite_radiation(
&self,
latitude: f64,
longitude: f64,
) -> SatelliteRadiationBuilder<'_> {
SatelliteRadiationBuilder::new(self, latitude, longitude)
}
pub fn flood(&self, latitude: f64, longitude: f64) -> FloodBuilder<'_> {
FloodBuilder::new(self, latitude, longitude)
}
pub fn marine(&self, latitude: f64, longitude: f64) -> MarineBuilder<'_> {
MarineBuilder::new(self, latitude, longitude)
}
pub fn air_quality(&self, latitude: f64, longitude: f64) -> AirQualityBuilder<'_> {
AirQualityBuilder::new(self, latitude, longitude)
}
pub fn geocode(&self, name: impl Into<String>) -> GeocodingBuilder<'_> {
GeocodingBuilder::new(self, name.into())
}
pub(crate) async fn execute(&self, request: reqwest::RequestBuilder) -> Result<Vec<u8>> {
let response = request
.send()
.await
.map_err(|err| map_reqwest_error(err, self.timeout))?;
read_success_body(response, self.timeout).await
}
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct ClientBuilder {
forecast_base: Option<Url>,
archive_base: Option<Url>,
historical_forecast_base: Option<Url>,
previous_runs_base: Option<Url>,
ensemble_base: Option<Url>,
seasonal_base: Option<Url>,
climate_base: Option<Url>,
satellite_radiation_base: Option<Url>,
flood_base: Option<Url>,
marine_base: Option<Url>,
air_quality_base: Option<Url>,
elevation_base: Option<Url>,
geocoding_base: Option<Url>,
api_key: Option<String>,
user_agent: Option<String>,
timeout: Option<Duration>,
}
impl ClientBuilder {
#[must_use]
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
#[must_use]
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn forecast_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.forecast_base = Some(parse_url("forecast_base_url", url.as_ref())?);
Ok(self)
}
pub fn archive_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.archive_base = Some(parse_url("archive_base_url", url.as_ref())?);
Ok(self)
}
pub fn historical_forecast_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.historical_forecast_base =
Some(parse_url("historical_forecast_base_url", url.as_ref())?);
Ok(self)
}
pub fn previous_runs_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.previous_runs_base = Some(parse_url("previous_runs_base_url", url.as_ref())?);
Ok(self)
}
pub fn ensemble_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.ensemble_base = Some(parse_url("ensemble_base_url", url.as_ref())?);
Ok(self)
}
pub fn seasonal_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.seasonal_base = Some(parse_url("seasonal_base_url", url.as_ref())?);
Ok(self)
}
pub fn climate_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.climate_base = Some(parse_url("climate_base_url", url.as_ref())?);
Ok(self)
}
pub fn satellite_radiation_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.satellite_radiation_base =
Some(parse_url("satellite_radiation_base_url", url.as_ref())?);
Ok(self)
}
pub fn flood_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.flood_base = Some(parse_url("flood_base_url", url.as_ref())?);
Ok(self)
}
pub fn marine_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.marine_base = Some(parse_url("marine_base_url", url.as_ref())?);
Ok(self)
}
pub fn air_quality_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.air_quality_base = Some(parse_url("air_quality_base_url", url.as_ref())?);
Ok(self)
}
pub fn elevation_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.elevation_base = Some(parse_url("elevation_base_url", url.as_ref())?);
Ok(self)
}
pub fn geocoding_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
self.geocoding_base = Some(parse_url("geocoding_base_url", url.as_ref())?);
Ok(self)
}
pub fn build(self) -> Result<Client> {
let forecast_base = self.forecast_base.unwrap_or(parse_default_url(
"forecast_base_url",
self.api_key.is_some(),
DEFAULT_FORECAST_BASE,
CUSTOMER_FORECAST_BASE,
)?);
let archive_base = self.archive_base.unwrap_or(parse_default_url(
"archive_base_url",
self.api_key.is_some(),
DEFAULT_ARCHIVE_BASE,
CUSTOMER_ARCHIVE_BASE,
)?);
let historical_forecast_base = self.historical_forecast_base.unwrap_or(parse_default_url(
"historical_forecast_base_url",
self.api_key.is_some(),
DEFAULT_HISTORICAL_FORECAST_BASE,
CUSTOMER_HISTORICAL_FORECAST_BASE,
)?);
let previous_runs_base = self.previous_runs_base.unwrap_or(parse_url(
"previous_runs_base_url",
DEFAULT_PREVIOUS_RUNS_BASE,
)?);
let ensemble_base = self
.ensemble_base
.unwrap_or(parse_url("ensemble_base_url", DEFAULT_ENSEMBLE_BASE)?);
let seasonal_base = self
.seasonal_base
.unwrap_or(parse_url("seasonal_base_url", DEFAULT_SEASONAL_BASE)?);
let climate_base = self
.climate_base
.unwrap_or(parse_url("climate_base_url", DEFAULT_CLIMATE_BASE)?);
let satellite_radiation_base = self.satellite_radiation_base.unwrap_or(parse_url(
"satellite_radiation_base_url",
DEFAULT_SATELLITE_RADIATION_BASE,
)?);
let flood_base = self
.flood_base
.unwrap_or(parse_url("flood_base_url", DEFAULT_FLOOD_BASE)?);
let marine_base = self
.marine_base
.unwrap_or(parse_url("marine_base_url", DEFAULT_MARINE_BASE)?);
let air_quality_base = self
.air_quality_base
.unwrap_or(parse_url("air_quality_base_url", DEFAULT_AIR_QUALITY_BASE)?);
let elevation_base = self.elevation_base.unwrap_or(parse_default_url(
"elevation_base_url",
self.api_key.is_some(),
DEFAULT_ELEVATION_BASE,
CUSTOMER_ELEVATION_BASE,
)?);
let geocoding_base = self.geocoding_base.unwrap_or(parse_default_url(
"geocoding_base_url",
self.api_key.is_some(),
DEFAULT_GEOCODING_BASE,
CUSTOMER_GEOCODING_BASE,
)?);
let user_agent = self
.user_agent
.unwrap_or_else(|| format!("openmeteo-rs/{}", env!("CARGO_PKG_VERSION")));
let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
let http = reqwest::Client::builder()
.user_agent(user_agent)
.timeout(timeout)
.build()?;
Ok(Client {
http,
forecast_base,
archive_base,
historical_forecast_base,
previous_runs_base,
ensemble_base,
seasonal_base,
climate_base,
satellite_radiation_base,
flood_base,
marine_base,
air_quality_base,
elevation_base,
geocoding_base,
api_key: self.api_key.map(Arc::from),
timeout,
})
}
}
fn parse_default_url(
field: &'static str,
use_customer_base: bool,
public_base: &str,
customer_base: &str,
) -> Result<Url> {
parse_url(
field,
if use_customer_base {
customer_base
} else {
public_base
},
)
}
fn parse_url(field: &'static str, value: &str) -> Result<Url> {
let mut url = Url::parse(value).map_err(|err| Error::InvalidParam {
field,
reason: err.to_string(),
})?;
if !url.path().ends_with('/') {
let mut path = url.path().to_owned();
path.push('/');
url.set_path(&path);
}
Ok(url)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_key_switches_default_endpoint_bases_to_customer_hosts() {
let client = Client::builder().api_key("secret").build().unwrap();
assert_eq!(
client.forecast_base.as_str(),
"https://customer-api.open-meteo.com/"
);
assert_eq!(
client.archive_base.as_str(),
"https://customer-archive-api.open-meteo.com/"
);
assert_eq!(
client.historical_forecast_base.as_str(),
"https://customer-historical-forecast-api.open-meteo.com/"
);
assert_eq!(
client.previous_runs_base.as_str(),
"https://previous-runs-api.open-meteo.com/"
);
assert_eq!(
client.ensemble_base.as_str(),
"https://ensemble-api.open-meteo.com/"
);
assert_eq!(
client.seasonal_base.as_str(),
"https://seasonal-api.open-meteo.com/"
);
assert_eq!(
client.climate_base.as_str(),
"https://climate-api.open-meteo.com/"
);
assert_eq!(
client.satellite_radiation_base.as_str(),
"https://satellite-api.open-meteo.com/"
);
assert_eq!(
client.flood_base.as_str(),
"https://flood-api.open-meteo.com/"
);
assert_eq!(
client.marine_base.as_str(),
"https://marine-api.open-meteo.com/"
);
assert_eq!(
client.air_quality_base.as_str(),
"https://air-quality-api.open-meteo.com/"
);
assert_eq!(
client.elevation_base.as_str(),
"https://customer-api.open-meteo.com/"
);
assert_eq!(
client.geocoding_base.as_str(),
"https://customer-geocoding-api.open-meteo.com/"
);
}
#[test]
fn explicit_endpoint_bases_override_customer_defaults() {
let client = Client::builder()
.api_key("secret")
.forecast_base_url("https://forecast.example")
.unwrap()
.archive_base_url("https://archive.example")
.unwrap()
.historical_forecast_base_url("https://historical.example")
.unwrap()
.previous_runs_base_url("https://previous.example")
.unwrap()
.ensemble_base_url("https://ensemble.example")
.unwrap()
.seasonal_base_url("https://seasonal.example")
.unwrap()
.climate_base_url("https://climate.example")
.unwrap()
.satellite_radiation_base_url("https://satellite.example")
.unwrap()
.flood_base_url("https://flood.example")
.unwrap()
.marine_base_url("https://marine.example")
.unwrap()
.air_quality_base_url("https://air.example")
.unwrap()
.elevation_base_url("https://elevation.example")
.unwrap()
.geocoding_base_url("https://geocoding.example")
.unwrap()
.build()
.unwrap();
assert_eq!(client.forecast_base.as_str(), "https://forecast.example/");
assert_eq!(client.archive_base.as_str(), "https://archive.example/");
assert_eq!(
client.historical_forecast_base.as_str(),
"https://historical.example/"
);
assert_eq!(
client.previous_runs_base.as_str(),
"https://previous.example/"
);
assert_eq!(client.ensemble_base.as_str(), "https://ensemble.example/");
assert_eq!(client.seasonal_base.as_str(), "https://seasonal.example/");
assert_eq!(client.climate_base.as_str(), "https://climate.example/");
assert_eq!(
client.satellite_radiation_base.as_str(),
"https://satellite.example/"
);
assert_eq!(client.flood_base.as_str(), "https://flood.example/");
assert_eq!(client.marine_base.as_str(), "https://marine.example/");
assert_eq!(client.air_quality_base.as_str(), "https://air.example/");
assert_eq!(client.elevation_base.as_str(), "https://elevation.example/");
assert_eq!(client.geocoding_base.as_str(), "https://geocoding.example/");
}
}