lib.rs (3957B)
1 use std::fmt::Debug; 2 3 use reqwest::Client; 4 5 pub use error::Error; 6 use error::Result; 7 use model::*; 8 use serde::{Serialize, de::DeserializeOwned}; 9 10 mod error; 11 pub mod model; 12 13 #[cfg(not(target_arch = "wasm32"))] 14 const USER_AGENT: &str = concat!("fjordgard/", env!("CARGO_PKG_VERSION")); 15 const GEOCODING_API_HOST: &str = "geocoding-api.open-meteo.com"; 16 const FORECASTING_API_HOST: &str = "api.open-meteo.com"; 17 18 pub struct MeteoClient { 19 api_key: Option<String>, 20 client: Client, 21 } 22 23 impl MeteoClient { 24 pub fn new(api_key: Option<&str>) -> Result<Self> { 25 #[cfg(not(target_arch = "wasm32"))] 26 let client = Client::builder().user_agent(USER_AGENT).build()?; 27 #[cfg(target_arch = "wasm32")] 28 let client = Client::new(); 29 30 Ok(Self { 31 api_key: api_key.map(|k| k.to_string()), 32 client, 33 }) 34 } 35 36 async fn request<O1: Serialize, O2: Serialize, T: DeserializeOwned + Debug>( 37 &self, 38 url: &str, 39 route: &str, 40 opt1: Option<O1>, 41 opt2: Option<O2>, 42 ) -> Result<T> { 43 let prefix = if self.api_key.is_some() { 44 "customer-" 45 } else { 46 "" 47 }; 48 49 let mut req = self.client.get(format!("https://{prefix}{url}/v1/{route}")); 50 51 if let Some(ref key) = self.api_key { 52 req = req.query(&[("apikey", key)]) 53 }; 54 55 if let Some(ref opt) = opt1 { 56 req = req.query(opt) 57 }; 58 59 if let Some(ref opt) = opt2 { 60 req = req.query(opt) 61 }; 62 63 let resp: MeteoResponse = req.send().await?.json().await?; 64 65 match resp { 66 MeteoResponse::Error { reason } => Err(Error::Meteo(reason)), 67 MeteoResponse::Success(v) => match serde_json::from_value(v) { 68 Ok(o) => Ok(o), 69 Err(e) => Err(Error::SerdeJson(e)), 70 }, 71 } 72 } 73 74 /// Endpoint: `/search` 75 pub async fn geocode(&self, name: &str, opt: Option<GeocodeOptions>) -> Result<Vec<Location>> { 76 let resp: GeocodeResponse = self 77 .request(GEOCODING_API_HOST, "search", Some(&[("name", name)]), opt) 78 .await?; 79 80 Ok(resp.results) 81 } 82 83 /// Endpoint: `/forecast` 84 pub async fn forecast_single( 85 &self, 86 latitude: f64, 87 longitude: f64, 88 opt: Option<ForecastOptions>, 89 ) -> Result<Forecast> { 90 self.request( 91 FORECASTING_API_HOST, 92 "forecast", 93 Some(&[("latitude", latitude), ("longitude", longitude)]), 94 opt, 95 ) 96 .await 97 } 98 } 99 100 #[cfg(test)] 101 mod tests { 102 use super::*; 103 104 async fn get_london(client: &MeteoClient) -> Location { 105 let res = client 106 .geocode("London, United Kingdom", None) 107 .await 108 .unwrap(); 109 110 res.first().unwrap().clone() 111 } 112 113 #[tokio::test] 114 async fn geocode() { 115 let client = MeteoClient::new(None).unwrap(); 116 let london = get_london(&client).await; 117 118 assert_eq!(london.timezone, "Europe/London"); 119 assert_eq!(london.admin1, Some("England".to_string())); 120 assert_eq!(london.country, "United Kingdom"); 121 } 122 123 #[tokio::test] 124 async fn forecast_single() { 125 let client = MeteoClient::new(None).unwrap(); 126 let london = get_london(&client).await; 127 128 client 129 .forecast_single( 130 london.latitude, 131 london.longitude, 132 Some(ForecastOptions { 133 current: Some(vec![CurrentVariable::Temperature2m]), 134 daily: Some(vec![DailyVariable::Temperature2mMean]), 135 hourly: Some(vec![ 136 HourlyVariable::Temperature2m, 137 HourlyVariable::TemperaturePressureLevel(1000), 138 ]), 139 ..Default::default() 140 }), 141 ) 142 .await 143 .unwrap(); 144 } 145 }