1use std::sync::Arc;
2use std::time::Duration;
3
4use reqwest::Url;
5use time::Date;
6
7use crate::endpoints::air_quality::AirQualityBuilder;
8use crate::endpoints::archive::ArchiveBuilder;
9use crate::endpoints::climate::ClimateBuilder;
10use crate::endpoints::ensemble::EnsembleBuilder;
11use crate::endpoints::flood::FloodBuilder;
12use crate::endpoints::forecast::{ForecastBatchBuilder, ForecastBuilder};
13use crate::endpoints::geocoding::GeocodingBuilder;
14use crate::endpoints::historical_forecast::HistoricalForecastBuilder;
15use crate::endpoints::http::read_success_body;
16use crate::endpoints::marine::MarineBuilder;
17use crate::endpoints::previous_runs::PreviousRunsBuilder;
18use crate::endpoints::satellite_radiation::SatelliteRadiationBuilder;
19use crate::endpoints::seasonal::SeasonalBuilder;
20use crate::error::map_reqwest_error;
21use crate::{Error, Result};
22
23const DEFAULT_FORECAST_BASE: &str = "https://api.open-meteo.com";
24const DEFAULT_ARCHIVE_BASE: &str = "https://archive-api.open-meteo.com";
25const DEFAULT_HISTORICAL_FORECAST_BASE: &str = "https://historical-forecast-api.open-meteo.com";
26const DEFAULT_PREVIOUS_RUNS_BASE: &str = "https://previous-runs-api.open-meteo.com";
27const DEFAULT_ENSEMBLE_BASE: &str = "https://ensemble-api.open-meteo.com";
28const DEFAULT_SEASONAL_BASE: &str = "https://seasonal-api.open-meteo.com";
29const DEFAULT_CLIMATE_BASE: &str = "https://climate-api.open-meteo.com";
30const DEFAULT_SATELLITE_RADIATION_BASE: &str = "https://satellite-api.open-meteo.com";
31const DEFAULT_FLOOD_BASE: &str = "https://flood-api.open-meteo.com";
32const DEFAULT_MARINE_BASE: &str = "https://marine-api.open-meteo.com";
33const DEFAULT_AIR_QUALITY_BASE: &str = "https://air-quality-api.open-meteo.com";
34const DEFAULT_ELEVATION_BASE: &str = "https://api.open-meteo.com";
35const DEFAULT_GEOCODING_BASE: &str = "https://geocoding-api.open-meteo.com";
36const CUSTOMER_FORECAST_BASE: &str = "https://customer-api.open-meteo.com";
37const CUSTOMER_ARCHIVE_BASE: &str = "https://customer-archive-api.open-meteo.com";
38const CUSTOMER_HISTORICAL_FORECAST_BASE: &str =
39 "https://customer-historical-forecast-api.open-meteo.com";
40const CUSTOMER_ELEVATION_BASE: &str = "https://customer-api.open-meteo.com";
41const CUSTOMER_GEOCODING_BASE: &str = "https://customer-geocoding-api.open-meteo.com";
42const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
43
44#[derive(Debug, Clone)]
46pub struct Client {
47 pub(crate) http: reqwest::Client,
48 pub(crate) forecast_base: Url,
49 pub(crate) archive_base: Url,
50 pub(crate) historical_forecast_base: Url,
51 pub(crate) previous_runs_base: Url,
52 pub(crate) ensemble_base: Url,
53 pub(crate) seasonal_base: Url,
54 pub(crate) climate_base: Url,
55 pub(crate) satellite_radiation_base: Url,
56 pub(crate) flood_base: Url,
57 pub(crate) marine_base: Url,
58 pub(crate) air_quality_base: Url,
59 pub(crate) elevation_base: Url,
60 pub(crate) geocoding_base: Url,
61 pub(crate) api_key: Option<Arc<str>>,
62 pub(crate) timeout: Duration,
63}
64
65impl Client {
66 pub fn new() -> Self {
71 Self::builder()
72 .build()
73 .expect("default Open-Meteo client configuration is valid")
74 }
75
76 pub fn builder() -> ClientBuilder {
78 ClientBuilder::default()
79 }
80
81 pub fn forecast(&self, latitude: f64, longitude: f64) -> ForecastBuilder<'_> {
83 ForecastBuilder::new(self, latitude, longitude)
84 }
85
86 pub fn forecast_batch<I>(&self, locations: I) -> ForecastBatchBuilder<'_>
88 where
89 I: IntoIterator<Item = (f64, f64)>,
90 {
91 ForecastBatchBuilder::new(self, locations)
92 }
93
94 pub fn forecast_many<I>(&self, locations: I) -> ForecastBatchBuilder<'_>
96 where
97 I: IntoIterator<Item = (f64, f64)>,
98 {
99 self.forecast_batch(locations)
100 }
101
102 pub fn archive(
104 &self,
105 latitude: f64,
106 longitude: f64,
107 start_date: Date,
108 end_date: Date,
109 ) -> ArchiveBuilder<'_> {
110 ArchiveBuilder::new(self, latitude, longitude, start_date, end_date)
111 }
112
113 pub fn historical_forecast(
115 &self,
116 latitude: f64,
117 longitude: f64,
118 start_date: Date,
119 end_date: Date,
120 ) -> HistoricalForecastBuilder<'_> {
121 HistoricalForecastBuilder::new(self, latitude, longitude, start_date, end_date)
122 }
123
124 pub fn previous_runs(&self, latitude: f64, longitude: f64) -> PreviousRunsBuilder<'_> {
126 PreviousRunsBuilder::new(self, latitude, longitude)
127 }
128
129 pub fn ensemble(&self, latitude: f64, longitude: f64) -> EnsembleBuilder<'_> {
131 EnsembleBuilder::new(self, latitude, longitude)
132 }
133
134 pub fn seasonal(&self, latitude: f64, longitude: f64) -> SeasonalBuilder<'_> {
136 SeasonalBuilder::new(self, latitude, longitude)
137 }
138
139 pub fn climate(
141 &self,
142 latitude: f64,
143 longitude: f64,
144 start_date: Date,
145 end_date: Date,
146 ) -> ClimateBuilder<'_> {
147 ClimateBuilder::new(self, latitude, longitude, start_date, end_date)
148 }
149
150 pub fn satellite_radiation(
152 &self,
153 latitude: f64,
154 longitude: f64,
155 ) -> SatelliteRadiationBuilder<'_> {
156 SatelliteRadiationBuilder::new(self, latitude, longitude)
157 }
158
159 pub fn flood(&self, latitude: f64, longitude: f64) -> FloodBuilder<'_> {
161 FloodBuilder::new(self, latitude, longitude)
162 }
163
164 pub fn marine(&self, latitude: f64, longitude: f64) -> MarineBuilder<'_> {
166 MarineBuilder::new(self, latitude, longitude)
167 }
168
169 pub fn air_quality(&self, latitude: f64, longitude: f64) -> AirQualityBuilder<'_> {
171 AirQualityBuilder::new(self, latitude, longitude)
172 }
173
174 pub fn geocode(&self, name: impl Into<String>) -> GeocodingBuilder<'_> {
195 GeocodingBuilder::new(self, name.into())
196 }
197
198 pub(crate) async fn execute(&self, request: reqwest::RequestBuilder) -> Result<Vec<u8>> {
199 let response = request
200 .send()
201 .await
202 .map_err(|err| map_reqwest_error(err, self.timeout))?;
203 read_success_body(response, self.timeout).await
204 }
205}
206
207impl Default for Client {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[derive(Debug, Default)]
215pub struct ClientBuilder {
216 forecast_base: Option<Url>,
217 archive_base: Option<Url>,
218 historical_forecast_base: Option<Url>,
219 previous_runs_base: Option<Url>,
220 ensemble_base: Option<Url>,
221 seasonal_base: Option<Url>,
222 climate_base: Option<Url>,
223 satellite_radiation_base: Option<Url>,
224 flood_base: Option<Url>,
225 marine_base: Option<Url>,
226 air_quality_base: Option<Url>,
227 elevation_base: Option<Url>,
228 geocoding_base: Option<Url>,
229 api_key: Option<String>,
230 user_agent: Option<String>,
231 timeout: Option<Duration>,
232}
233
234impl ClientBuilder {
235 #[must_use]
240 pub fn api_key(mut self, key: impl Into<String>) -> Self {
241 self.api_key = Some(key.into());
242 self
243 }
244
245 #[must_use]
247 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
248 self.user_agent = Some(user_agent.into());
249 self
250 }
251
252 #[must_use]
254 pub fn timeout(mut self, timeout: Duration) -> Self {
255 self.timeout = Some(timeout);
256 self
257 }
258
259 pub fn forecast_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
265 self.forecast_base = Some(parse_url("forecast_base_url", url.as_ref())?);
266 Ok(self)
267 }
268
269 pub fn archive_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
275 self.archive_base = Some(parse_url("archive_base_url", url.as_ref())?);
276 Ok(self)
277 }
278
279 pub fn historical_forecast_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
285 self.historical_forecast_base =
286 Some(parse_url("historical_forecast_base_url", url.as_ref())?);
287 Ok(self)
288 }
289
290 pub fn previous_runs_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
296 self.previous_runs_base = Some(parse_url("previous_runs_base_url", url.as_ref())?);
297 Ok(self)
298 }
299
300 pub fn ensemble_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
306 self.ensemble_base = Some(parse_url("ensemble_base_url", url.as_ref())?);
307 Ok(self)
308 }
309
310 pub fn seasonal_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
316 self.seasonal_base = Some(parse_url("seasonal_base_url", url.as_ref())?);
317 Ok(self)
318 }
319
320 pub fn climate_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
326 self.climate_base = Some(parse_url("climate_base_url", url.as_ref())?);
327 Ok(self)
328 }
329
330 pub fn satellite_radiation_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
336 self.satellite_radiation_base =
337 Some(parse_url("satellite_radiation_base_url", url.as_ref())?);
338 Ok(self)
339 }
340
341 pub fn flood_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
347 self.flood_base = Some(parse_url("flood_base_url", url.as_ref())?);
348 Ok(self)
349 }
350
351 pub fn marine_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
357 self.marine_base = Some(parse_url("marine_base_url", url.as_ref())?);
358 Ok(self)
359 }
360
361 pub fn air_quality_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
367 self.air_quality_base = Some(parse_url("air_quality_base_url", url.as_ref())?);
368 Ok(self)
369 }
370
371 pub fn elevation_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
377 self.elevation_base = Some(parse_url("elevation_base_url", url.as_ref())?);
378 Ok(self)
379 }
380
381 pub fn geocoding_base_url(mut self, url: impl AsRef<str>) -> Result<Self> {
387 self.geocoding_base = Some(parse_url("geocoding_base_url", url.as_ref())?);
388 Ok(self)
389 }
390
391 pub fn build(self) -> Result<Client> {
393 let forecast_base = self.forecast_base.unwrap_or(parse_default_url(
394 "forecast_base_url",
395 self.api_key.is_some(),
396 DEFAULT_FORECAST_BASE,
397 CUSTOMER_FORECAST_BASE,
398 )?);
399 let archive_base = self.archive_base.unwrap_or(parse_default_url(
400 "archive_base_url",
401 self.api_key.is_some(),
402 DEFAULT_ARCHIVE_BASE,
403 CUSTOMER_ARCHIVE_BASE,
404 )?);
405 let historical_forecast_base = self.historical_forecast_base.unwrap_or(parse_default_url(
406 "historical_forecast_base_url",
407 self.api_key.is_some(),
408 DEFAULT_HISTORICAL_FORECAST_BASE,
409 CUSTOMER_HISTORICAL_FORECAST_BASE,
410 )?);
411 let previous_runs_base = self.previous_runs_base.unwrap_or(parse_url(
412 "previous_runs_base_url",
413 DEFAULT_PREVIOUS_RUNS_BASE,
414 )?);
415 let ensemble_base = self
416 .ensemble_base
417 .unwrap_or(parse_url("ensemble_base_url", DEFAULT_ENSEMBLE_BASE)?);
418 let seasonal_base = self
419 .seasonal_base
420 .unwrap_or(parse_url("seasonal_base_url", DEFAULT_SEASONAL_BASE)?);
421 let climate_base = self
422 .climate_base
423 .unwrap_or(parse_url("climate_base_url", DEFAULT_CLIMATE_BASE)?);
424 let satellite_radiation_base = self.satellite_radiation_base.unwrap_or(parse_url(
425 "satellite_radiation_base_url",
426 DEFAULT_SATELLITE_RADIATION_BASE,
427 )?);
428 let flood_base = self
429 .flood_base
430 .unwrap_or(parse_url("flood_base_url", DEFAULT_FLOOD_BASE)?);
431 let marine_base = self
432 .marine_base
433 .unwrap_or(parse_url("marine_base_url", DEFAULT_MARINE_BASE)?);
434 let air_quality_base = self
435 .air_quality_base
436 .unwrap_or(parse_url("air_quality_base_url", DEFAULT_AIR_QUALITY_BASE)?);
437 let elevation_base = self.elevation_base.unwrap_or(parse_default_url(
438 "elevation_base_url",
439 self.api_key.is_some(),
440 DEFAULT_ELEVATION_BASE,
441 CUSTOMER_ELEVATION_BASE,
442 )?);
443 let geocoding_base = self.geocoding_base.unwrap_or(parse_default_url(
444 "geocoding_base_url",
445 self.api_key.is_some(),
446 DEFAULT_GEOCODING_BASE,
447 CUSTOMER_GEOCODING_BASE,
448 )?);
449
450 let user_agent = self
451 .user_agent
452 .unwrap_or_else(|| format!("openmeteo-rs/{}", env!("CARGO_PKG_VERSION")));
453 let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
454
455 let http = reqwest::Client::builder()
456 .user_agent(user_agent)
457 .timeout(timeout)
458 .build()?;
459
460 Ok(Client {
461 http,
462 forecast_base,
463 archive_base,
464 historical_forecast_base,
465 previous_runs_base,
466 ensemble_base,
467 seasonal_base,
468 climate_base,
469 satellite_radiation_base,
470 flood_base,
471 marine_base,
472 air_quality_base,
473 elevation_base,
474 geocoding_base,
475 api_key: self.api_key.map(Arc::from),
476 timeout,
477 })
478 }
479}
480
481fn parse_default_url(
482 field: &'static str,
483 use_customer_base: bool,
484 public_base: &str,
485 customer_base: &str,
486) -> Result<Url> {
487 parse_url(
488 field,
489 if use_customer_base {
490 customer_base
491 } else {
492 public_base
493 },
494 )
495}
496
497fn parse_url(field: &'static str, value: &str) -> Result<Url> {
498 let mut url = Url::parse(value).map_err(|err| Error::InvalidParam {
499 field,
500 reason: err.to_string(),
501 })?;
502
503 if !url.path().ends_with('/') {
504 let mut path = url.path().to_owned();
505 path.push('/');
506 url.set_path(&path);
507 }
508
509 Ok(url)
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn api_key_switches_default_endpoint_bases_to_customer_hosts() {
518 let client = Client::builder().api_key("secret").build().unwrap();
519
520 assert_eq!(
521 client.forecast_base.as_str(),
522 "https://customer-api.open-meteo.com/"
523 );
524 assert_eq!(
525 client.archive_base.as_str(),
526 "https://customer-archive-api.open-meteo.com/"
527 );
528 assert_eq!(
529 client.historical_forecast_base.as_str(),
530 "https://customer-historical-forecast-api.open-meteo.com/"
531 );
532 assert_eq!(
533 client.previous_runs_base.as_str(),
534 "https://previous-runs-api.open-meteo.com/"
535 );
536 assert_eq!(
537 client.ensemble_base.as_str(),
538 "https://ensemble-api.open-meteo.com/"
539 );
540 assert_eq!(
541 client.seasonal_base.as_str(),
542 "https://seasonal-api.open-meteo.com/"
543 );
544 assert_eq!(
545 client.climate_base.as_str(),
546 "https://climate-api.open-meteo.com/"
547 );
548 assert_eq!(
549 client.satellite_radiation_base.as_str(),
550 "https://satellite-api.open-meteo.com/"
551 );
552 assert_eq!(
553 client.flood_base.as_str(),
554 "https://flood-api.open-meteo.com/"
555 );
556 assert_eq!(
557 client.marine_base.as_str(),
558 "https://marine-api.open-meteo.com/"
559 );
560 assert_eq!(
561 client.air_quality_base.as_str(),
562 "https://air-quality-api.open-meteo.com/"
563 );
564 assert_eq!(
565 client.elevation_base.as_str(),
566 "https://customer-api.open-meteo.com/"
567 );
568 assert_eq!(
569 client.geocoding_base.as_str(),
570 "https://customer-geocoding-api.open-meteo.com/"
571 );
572 }
573
574 #[test]
575 fn explicit_endpoint_bases_override_customer_defaults() {
576 let client = Client::builder()
577 .api_key("secret")
578 .forecast_base_url("https://forecast.example")
579 .unwrap()
580 .archive_base_url("https://archive.example")
581 .unwrap()
582 .historical_forecast_base_url("https://historical.example")
583 .unwrap()
584 .previous_runs_base_url("https://previous.example")
585 .unwrap()
586 .ensemble_base_url("https://ensemble.example")
587 .unwrap()
588 .seasonal_base_url("https://seasonal.example")
589 .unwrap()
590 .climate_base_url("https://climate.example")
591 .unwrap()
592 .satellite_radiation_base_url("https://satellite.example")
593 .unwrap()
594 .flood_base_url("https://flood.example")
595 .unwrap()
596 .marine_base_url("https://marine.example")
597 .unwrap()
598 .air_quality_base_url("https://air.example")
599 .unwrap()
600 .elevation_base_url("https://elevation.example")
601 .unwrap()
602 .geocoding_base_url("https://geocoding.example")
603 .unwrap()
604 .build()
605 .unwrap();
606
607 assert_eq!(client.forecast_base.as_str(), "https://forecast.example/");
608 assert_eq!(client.archive_base.as_str(), "https://archive.example/");
609 assert_eq!(
610 client.historical_forecast_base.as_str(),
611 "https://historical.example/"
612 );
613 assert_eq!(
614 client.previous_runs_base.as_str(),
615 "https://previous.example/"
616 );
617 assert_eq!(client.ensemble_base.as_str(), "https://ensemble.example/");
618 assert_eq!(client.seasonal_base.as_str(), "https://seasonal.example/");
619 assert_eq!(client.climate_base.as_str(), "https://climate.example/");
620 assert_eq!(
621 client.satellite_radiation_base.as_str(),
622 "https://satellite.example/"
623 );
624 assert_eq!(client.flood_base.as_str(), "https://flood.example/");
625 assert_eq!(client.marine_base.as_str(), "https://marine.example/");
626 assert_eq!(client.air_quality_base.as_str(), "https://air.example/");
627 assert_eq!(client.elevation_base.as_str(), "https://elevation.example/");
628 assert_eq!(client.geocoding_base.as_str(), "https://geocoding.example/");
629 }
630}