// 41.78, -71.41 // https://api.solunar.org/solunar/41.78,-71.41,20211029,-4 use chrono::{DateTime, Duration, NaiveTime, Offset, TimeZone, Utc}; use geo_types::{Latitude, Longitude}; use memorycache::MemoryCache; use reqwest; use serde::Deserialize; const ENDPOINT: &str = "https://api.solunar.org/solunar"; #[derive(Clone, Debug, PartialEq)] pub struct SunMoon { pub sunrise: NaiveTime, pub sunset: NaiveTime, pub moonrise: Option, pub moonset: Option, pub moon_phase: LunarPhase, } impl SunMoon { fn from_js(val: SunMoonJs) -> Self { fn parse_time(val: String) -> Option { NaiveTime::parse_from_str(&val, "%H:%M").ok() } let sunrise = parse_time(val.sunrise).unwrap(); let sunset = parse_time(val.sunset).unwrap(); let moonrise = val.moonrise.and_then(|v| parse_time(v)); let moonset = val.moonset.and_then(|v| parse_time(v)); Self { sunrise, sunset, moonrise, moonset, moon_phase: val.moon_phase, } } } #[derive(Clone, Debug, Deserialize)] pub(crate) struct SunMoonJs { #[serde(alias = "sunRise")] sunrise: String, #[serde(alias = "sunSet")] sunset: String, #[serde(alias = "moonRise")] moonrise: Option, #[serde(alias = "moonSet")] moonset: Option, #[serde(alias = "moonPhase")] moon_phase: LunarPhase, } #[derive(Clone, Debug, Deserialize, PartialEq)] pub enum LunarPhase { #[serde(alias = "New Moon")] NewMoon, #[serde(alias = "Waxing Crescent")] WaxingCrescent, #[serde(alias = "First Quarter")] FirstQuarter, #[serde(alias = "Waxing Gibbous")] WaxingGibbous, #[serde(alias = "Full Moon")] FullMoon, #[serde(alias = "Waning Gibbous")] WaningGibbous, #[serde(alias = "Last Quarter")] LastQuarter, #[serde(alias = "Waning Crescent")] WaningCrescent, } pub struct SolunaClient { client: reqwest::Client, memory_cache: MemoryCache, } impl SolunaClient { pub fn new() -> Self { Self { client: reqwest::Client::new(), memory_cache: MemoryCache::new(), } } pub async fn request( &self, latitude: Latitude, longitude: Longitude, day: DateTime, ) -> SunMoon { let date = day.date_naive().format("%Y%m%d"); let url = format!( "{}/{},{},{},{}", ENDPOINT, latitude, longitude, date, day.offset().fix().local_minus_utc() / 3600 ); let js = self .memory_cache .find(&url, async { let response = self.client.get(&url).send().await.unwrap(); let expiration = response .headers() .get(reqwest::header::EXPIRES) .and_then(|header| header.to_str().ok()) .and_then(|expiration| DateTime::parse_from_rfc2822(expiration).ok()) .map(|dt_local| DateTime::::from(dt_local)) .unwrap_or(Utc::now() + Duration::seconds(3600)); let soluna: SunMoonJs = response.json().await.unwrap(); (expiration, soluna) }) .await; SunMoon::from_js(js) } } #[cfg(test)] mod test { use super::*; use serde_json; const EXAMPLE: &str = "{\"sunRise\":\"7:15\",\"sunTransit\":\"12:30\",\"sunSet\":\"17:45\",\"moonRise\":null,\"moonTransit\":\"7:30\",\"moonUnder\":\"19:54\",\"moonSet\":\"15:02\",\"moonPhase\":\"Waning Crescent\",\"moonIllumination\":0.35889454647387764,\"sunRiseDec\":7.25,\"sunTransitDec\":12.5,\"sunSetDec\":17.75,\"moonRiseDec\":null,\"moonSetDec\":15.033333333333333,\"moonTransitDec\":7.5,\"moonUnderDec\":19.9,\"minor1Start\":null,\"minor1Stop\":null,\"minor2StartDec\":14.533333333333333,\"minor2Start\":\"14:32\",\"minor2StopDec\":15.533333333333333,\"minor2Stop\":\"15:32\",\"major1StartDec\":6.5,\"major1Start\":\"06:30\",\"major1StopDec\":8.5,\"major1Stop\":\"08:30\",\"major2StartDec\":18.9,\"major2Start\":\"18:54\",\"major2StopDec\":20.9,\"major2Stop\":\"20:54\",\"dayRating\":1,\"hourlyRating\":{\"0\":20,\"1\":20,\"2\":0,\"3\":0,\"4\":0,\"5\":0,\"6\":20,\"7\":40,\"8\":40,\"9\":20,\"10\":0,\"11\":0,\"12\":0,\"13\":0,\"14\":0,\"15\":20,\"16\":20,\"17\":20,\"18\":40,\"19\":20,\"20\":20,\"21\":20,\"22\":0,\"23\":0}}"; #[test] fn it_parses_a_response() { let sun_moon_js: SunMoonJs = serde_json::from_str(EXAMPLE).unwrap(); let sun_moon = SunMoon::from_js(sun_moon_js); assert_eq!( sun_moon, SunMoon { sunrise: NaiveTime::from_hms_opt(7, 15, 0).unwrap(), sunset: NaiveTime::from_hms_opt(17, 45, 0).unwrap(), moonrise: None, moonset: Some(NaiveTime::from_hms_opt(15, 02, 0).unwrap()), moon_phase: LunarPhase::WaningCrescent, } ); } }