fjordgard

A desktop clock application
Log | Files | Refs | README | LICENSE

commit 16285c914f21e84bb22f40d95c6ca815171fb1e2
parent ef89c772a66f01fa358101b50ef869f1bc829a63
Author: Sylvia Ivory <git@sivory.net>
Date:   Mon, 16 Jun 2025 00:08:41 -0700

Use enums in forecast hashmaps

Diffstat:
MCargo.lock | 300++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/weather/Cargo.toml | 1+
Mcrates/weather/src/error.rs | 4++++
Mcrates/weather/src/lib.rs | 12++++++++++--
Mcrates/weather/src/model.rs | 118++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
5 files changed, 415 insertions(+), 20 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -18,12 +18,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -78,6 +99,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -94,6 +128,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -105,6 +184,12 @@ dependencies = [ ] [[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -150,6 +235,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_with", "strum", "thiserror", "tokio", @@ -265,7 +351,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -274,6 +360,12 @@ dependencies = [ [[package]] name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" @@ -285,6 +377,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -403,6 +501,30 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -489,6 +611,12 @@ dependencies = [ ] [[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -511,12 +639,24 @@ dependencies = [ [[package]] name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.4", + "serde", ] [[package]] @@ -625,6 +765,21 @@ dependencies = [ ] [[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -717,6 +872,12 @@ dependencies = [ ] [[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -741,6 +902,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "reqwest" version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -868,6 +1049,18 @@ dependencies = [ ] [[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -935,6 +1128,37 @@ dependencies = [ ] [[package]] +name = "serde_with" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "schemars", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -969,6 +1193,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] name = "strum" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1082,6 +1312,37 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1362,6 +1623,41 @@ dependencies = [ ] [[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/weather/Cargo.toml b/crates/weather/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" reqwest = { version = "0.12.20", features = ["json"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" +serde_with = "3.13.0" strum = { version = "0.27.1", features = ["derive"] } thiserror = "2.0.12" diff --git a/crates/weather/src/error.rs b/crates/weather/src/error.rs @@ -4,6 +4,10 @@ pub enum Error { Reqwest(#[from] reqwest::Error), #[error("meteo error: {0}")] Meteo(String), + #[error("json: {0}")] + SerdeJson(#[from] serde_json::Error), + #[error("failed to parse pressure level")] + InvalidPressureLevel, } pub type Result<T, E = Error> = std::result::Result<T, E>; diff --git a/crates/weather/src/lib.rs b/crates/weather/src/lib.rs @@ -55,11 +55,14 @@ impl MeteoClient { req = req.query(opt) }; - let resp: MeteoResponse<T> = req.send().await?.json().await?; + let resp: MeteoResponse = req.send().await?.json().await?; match resp { - MeteoResponse::Success(s) => Ok(s), MeteoResponse::Error { reason } => Err(Error::Meteo(reason)), + MeteoResponse::Success(v) => match serde_json::from_value(v) { + Ok(o) => Ok(o), + Err(e) => Err(Error::SerdeJson(e)), + }, } } @@ -123,6 +126,11 @@ mod tests { london.longitude, Some(ForecastOptions { current: Some(vec![CurrentVariable::Temperature2m]), + daily: Some(vec![DailyVariable::Temperature2mMean]), + hourly: Some(vec![ + HourlyVariable::Temperature2m, + HourlyVariable::TemperaturePressureLevel(1000), + ]), ..Default::default() }), ) diff --git a/crates/weather/src/model.rs b/crates/weather/src/model.rs @@ -1,16 +1,20 @@ -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, hash::Hash, str::FromStr}; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor}; +use serde_with::DeserializeFromStr; +use strum::{Display, EnumString}; + +use crate::Error; #[derive(Deserialize, Debug)] #[serde(untagged)] -pub(crate) enum MeteoResponse<T> { - Success(T), +pub(crate) enum MeteoResponse { + Success(serde_json::Value), Error { reason: String }, } +#[serde_with::skip_serializing_none] #[derive(Serialize, Default)] -#[serde(default)] pub struct GeocodeOptions { pub count: Option<usize>, pub language: Option<String>, @@ -57,7 +61,7 @@ pub(crate) struct GeocodeResponse { pub(crate) results: Vec<Location>, } -#[derive(strum::Display)] +#[derive(Display, EnumString, Clone, Copy, Debug, Hash, PartialEq, Eq)] #[strum(serialize_all = "snake_case")] pub enum HourlyVariable { #[strum(to_string = "temperature_2m")] @@ -142,9 +146,77 @@ pub enum HourlyVariable { IsDay, #[strum(to_string = "geopotential_height_{0}hPa")] GeopotentialHeightPressureLevel(usize), + /// NOTE: Not a valid variable, only found within `.hourly_units` + Time, } -#[derive(strum::Display)] +impl<'de> Deserialize<'de> for HourlyVariable { + fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + struct HourlyVariableVisitor; + + impl<'de> Visitor<'de> for HourlyVariableVisitor { + type Value = HourlyVariable; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid hourly variable") + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + match Self::Value::from_str(v) { + Ok(v) => Ok(v), + Err(e) => { + // temperature_{0}hPa + // relative_humidity_{0}hPa + // dew_point_{0}hPa + // cloud_cover_{0}hPa + // wind_speed_{0}hPa + // wind_direction_{0}hPa + // geopotential_height_{0}hPa + + if !v.ends_with("hPa") { + return Err(serde::de::Error::custom(e)); + } + + let stripped = v.strip_suffix("hPa").unwrap(); + + let pos = stripped + .find(|c: char| c.is_ascii_digit()) + .ok_or(serde::de::Error::custom(Error::InvalidPressureLevel))?; + + let var = &stripped[..pos]; + let num = stripped[pos..] + .parse::<usize>() + .map_err(serde::de::Error::custom)?; + + let res = match var { + "temperature_" => HourlyVariable::TemperaturePressureLevel(num), + "relative_humidity_" => { + HourlyVariable::RelativeHumidityPressureLevel(num) + } + "dew_point_" => HourlyVariable::DewPointPressureLevel(num), + "cloud_cover_" => HourlyVariable::CloudCoverPressureLevel(num), + "wind_speed_" => HourlyVariable::WindSpeedPressureLevel(num), + "wind_direction_" => HourlyVariable::WindDirectionPressureLevel(num), + "geopotential_height_" => { + HourlyVariable::GeopotentialHeightPressureLevel(num) + } + _ => return Err(serde::de::Error::custom(Error::InvalidPressureLevel)), + }; + + Ok(res) + } + } + } + } + + deserializer.deserialize_str(HourlyVariableVisitor) + } +} + +#[derive(Display, EnumString, Clone, Copy, Debug, Hash, PartialEq, Eq, DeserializeFromStr)] #[strum(serialize_all = "snake_case")] pub enum DailyVariable { #[strum(to_string = "temperature_2m_max")] @@ -179,9 +251,11 @@ pub enum DailyVariable { Et0FaoEvapotranspiration, UvIndexMax, UvIndexClearSkyMax, + /// NOTE: Not a valid variable, only found within `.daily_units` + Time, } -#[derive(strum::Display)] +#[derive(Display, EnumString, Clone, Copy, Debug, Hash, PartialEq, Eq, DeserializeFromStr)] #[strum(serialize_all = "snake_case")] pub enum CurrentVariable { #[strum(to_string = "temperature_2m")] @@ -218,6 +292,10 @@ pub enum CurrentVariable { WindGusts10m, Visibility, WeatherCode, + /// NOTE: Not a valid variable, only found within `.current_units` + Time, + /// NOTE: Not a valid variable, only found within `.current_units` + Interval, } #[derive(Serialize)] @@ -262,6 +340,7 @@ pub enum CellSelection { Nearest, } +#[serde_with::skip_serializing_none] #[derive(Serialize, Default)] pub struct ForecastOptions { pub elevation: Option<f64>, @@ -294,10 +373,17 @@ pub struct ForecastOptions { } #[derive(Deserialize, Debug, Clone)] -pub struct ForecastData { +pub struct HourlyData { + pub time: Vec<String>, + #[serde(flatten)] + pub data: HashMap<HourlyVariable, Vec<f64>>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct DailyData { pub time: Vec<String>, #[serde(flatten)] - pub data: HashMap<String, Vec<f64>>, + pub data: HashMap<DailyVariable, Vec<f64>>, } #[derive(Deserialize, Debug, Clone)] @@ -305,7 +391,7 @@ pub struct CurrentData { pub time: String, pub interval: usize, #[serde(flatten)] - pub data: HashMap<String, f64>, + pub data: HashMap<CurrentVariable, f64>, } #[derive(Deserialize, Debug, Clone)] @@ -316,12 +402,12 @@ pub struct Forecast { pub utc_offset_seconds: isize, pub timezone: String, pub timezone_abbreviation: String, - pub hourly: Option<ForecastData>, - pub hourly_units: Option<HashMap<String, String>>, - pub daily: Option<ForecastData>, - pub daily_units: Option<HashMap<String, String>>, + pub hourly: Option<HourlyData>, + pub hourly_units: Option<HashMap<HourlyVariable, String>>, + pub daily: Option<DailyData>, + pub daily_units: Option<HashMap<DailyVariable, String>>, pub current: Option<CurrentData>, - pub current_units: Option<HashMap<String, String>>, + pub current_units: Option<HashMap<CurrentVariable, String>>, } fn csv<S: Serializer, T: Display>(list: &Option<Vec<T>>, serializer: S) -> Result<S::Ok, S::Error> {