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:
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> {