From 01c12478f60d2a8d7cbff4bae9d419528f6ec885 Mon Sep 17 00:00:00 2001 From: Candifloss Date: Sat, 20 Sep 2025 08:41:28 +0530 Subject: [PATCH] Separate config loading from API library - Create new library crate for config parsing - Modularity & flexibility --- Cargo.toml | 2 +- owm_api25/src/config.rs | 80 ---------------------------------- owm_api25/src/lib.rs | 2 +- owm_api25/src/query.rs | 71 ++++++++++++++++++++++++++++++ owm_widg_config/Cargo.toml | 10 +++++ owm_widg_config/src/config.rs | 9 ++++ owm_widg_config/src/free25.rs | 45 +++++++++++++++++++ owm_widg_config/src/general.rs | 6 +++ owm_widg_config/src/lib.rs | 3 ++ widget/Cargo.toml | 3 ++ widget/src/main.rs | 37 ++++++++++++---- 11 files changed, 177 insertions(+), 91 deletions(-) delete mode 100644 owm_api25/src/config.rs create mode 100644 owm_api25/src/query.rs create mode 100644 owm_widg_config/Cargo.toml create mode 100644 owm_widg_config/src/config.rs create mode 100644 owm_widg_config/src/free25.rs create mode 100644 owm_widg_config/src/general.rs create mode 100644 owm_widg_config/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index fc2bd94..621535c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" members = [ - "owm_api25", + "owm_api25", "owm_widg_config", "widget", ] diff --git a/owm_api25/src/config.rs b/owm_api25/src/config.rs deleted file mode 100644 index 7eecc06..0000000 --- a/owm_api25/src/config.rs +++ /dev/null @@ -1,80 +0,0 @@ -use serde::Deserialize; -use std::fs; - -#[derive(Debug, Deserialize)] -pub struct General { - pub api_key: String, // Required for API query. Get yours for free from https://openweathermap.org - pub city_id: Option, // Any of these location parameters are required - pub city_name: Option, // Find City ID or Name from https://openweathermap.org/find? - pub lat: Option, // Latitude and Longitude must be used together - pub lon: Option, - pub zip: Option, // Zip code - #[serde(default = "default_units")] - pub units: String, // "metric", "imperial", "standard" (Default) - #[serde(default = "default_lang")] - pub lang: String, // Default: "en" -} - -#[derive(Debug, Deserialize)] -pub struct Config { - pub general: General, -} - -fn default_units() -> String { - "standard".into() -} - -fn default_lang() -> String { - "en".into() -} - -impl Config { - /// Load configuration from `~/.config/candywidgets/openweathermap.toml`. - /// - /// # Errors - /// - /// Returns an error if: - /// - `$XDG_CONFIG_HOME` (or `~/.config`) cannot be determined, - /// - the file cannot be read, - /// - or the TOML cannot be parsed. - pub fn load() -> Result> { - let mut path = dirs::config_dir().ok_or("No config dir found")?; - path.push("candywidgets/openweathermap.toml"); - - let toml_str = fs::read_to_string(&path) - .map_err(|_| format!("Failed to read config: {}", path.display()))?; - - Ok(toml::from_str(&toml_str)?) - } - - /// Build a query URL for the `OpenWeatherMap` API v2.5. - /// - /// # Errors - /// - /// Returns an error if no valid location parameter is set - /// (`city_id`, `city_name`, `lat`+`lon`, or `zip`). - pub fn build_url(&self) -> Result { - let base = "https://api.openweathermap.org/data/2.5/weather"; - - let mut params = vec![ - format!("appid={}", self.general.api_key), - format!("units={}", self.general.units), - format!("lang={}", self.general.lang), - "mode=json".to_string(), - ]; - - if let Some(ref id) = self.general.city_id { - params.push(format!("id={id}")); - } else if let Some(ref name) = self.general.city_name { - params.push(format!("q={name}")); - } else if let (Some(lat), Some(lon)) = (self.general.lat, self.general.lon) { - params.push(format!("lat={lat}&lon={lon}")); - } else if let Some(ref zip) = self.general.zip { - params.push(format!("zip={zip}")); - } else { - return Err("No valid location field found in config".into()); - } - - Ok(format!("{}?{}", base, params.join("&"))) - } -} diff --git a/owm_api25/src/lib.rs b/owm_api25/src/lib.rs index fe6de35..a5930a3 100644 --- a/owm_api25/src/lib.rs +++ b/owm_api25/src/lib.rs @@ -1,3 +1,3 @@ -pub mod config; pub mod forecast; pub mod weather; +pub mod query; diff --git a/owm_api25/src/query.rs b/owm_api25/src/query.rs new file mode 100644 index 0000000..1b9f554 --- /dev/null +++ b/owm_api25/src/query.rs @@ -0,0 +1,71 @@ +use std::fmt; + +pub const BASE_URL: &str = "https://api.openweathermap.org/data/2.5"; +pub const WEATHER_URL: &str = "https://api.openweathermap.org/data/2.5/weather"; +pub const FORECAST_URL: &str = "https://api.openweathermap.org/data/2.5/forecast"; + +/// Units of measurement for temperature and wind speed +#[derive(Debug)] +pub enum Units { + Standard, + Metric, + Imperial, +} + +impl fmt::Display for Units { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Units::Standard => write!(f, "standard"), + Units::Metric => write!(f, "metric"), + Units::Imperial => write!(f, "imperial"), + } + } +} + +/// Query parameters for OpenWeatherMap Free API v2.5 endpoints +#[derive(Debug)] +pub struct QueryParams { + pub api_key: String, + pub city_id: Option, + pub city_name: Option, + pub lat: Option, + pub lon: Option, + pub zip: Option, + pub units: Units, + pub lang: String, +} + +impl QueryParams { + /// Build query URL for the `/weather` endpoint + pub fn weather_url(&self) -> Result { + self.build_url(WEATHER_URL) + } + + /// Build query URL for the `/forecast` endpoint + pub fn forecast_url(&self) -> Result { + self.build_url(FORECAST_URL) + } + + fn build_url(&self, base: &str) -> Result { + let mut params = vec![ + format!("appid={}", self.api_key), + format!("units={}", self.units), + format!("lang={}", self.lang), + "mode=json".to_string(), + ]; + + if let Some(ref id) = self.city_id { + params.push(format!("id={id}")); + } else if let Some(ref name) = self.city_name { + params.push(format!("q={name}")); + } else if let (Some(lat), Some(lon)) = (self.lat, self.lon) { + params.push(format!("lat={lat}&lon={lon}")); + } else if let Some(ref zip) = self.zip { + params.push(format!("zip={zip}")); + } else { + return Err("No valid location field found".into()); + } + + Ok(format!("{base}?{}", params.join("&"))) + } +} diff --git a/owm_widg_config/Cargo.toml b/owm_widg_config/Cargo.toml new file mode 100644 index 0000000..9ed1e67 --- /dev/null +++ b/owm_widg_config/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "owm_widg_config" +version = "0.0.1" +edition = "2024" + +[dependencies] +owm_api25 = { path = "../owm_api25" } +serde = { version = "1.0.225", features = ["derive"] } +toml = "0.9.7" +dirs = "6.0.0" \ No newline at end of file diff --git a/owm_widg_config/src/config.rs b/owm_widg_config/src/config.rs new file mode 100644 index 0000000..7b9c4a0 --- /dev/null +++ b/owm_widg_config/src/config.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; +use crate::general::General; +use crate::free25::Free25Query; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub general: General, + pub query_params: Free25Query, +} diff --git a/owm_widg_config/src/free25.rs b/owm_widg_config/src/free25.rs new file mode 100644 index 0000000..4553b38 --- /dev/null +++ b/owm_widg_config/src/free25.rs @@ -0,0 +1,45 @@ +use serde::Deserialize; +use owm_api25::query::{QueryParams, Units}; + +#[derive(Debug, Deserialize)] +pub struct Free25Query { + pub api_key: String, + pub city_id: Option, + pub city_name: Option, + pub lat: Option, + pub lon: Option, + pub zip: Option, + #[serde(default = "default_units")] + pub units: String, // "metric", "imperial", "standard" + #[serde(default = "default_lang")] + pub lang: String, // default "en" +} + +fn default_units() -> String { + "standard".into() +} + +fn default_lang() -> String { + "en".into() +} + +impl From for QueryParams { + fn from(cfg: Free25Query) -> Self { + let units = match cfg.units.as_str() { + "metric" => Units::Metric, + "imperial" => Units::Imperial, + _ => Units::Standard, + }; + + QueryParams { + api_key: cfg.api_key, + city_id: cfg.city_id, + city_name: cfg.city_name, + lat: cfg.lat, + lon: cfg.lon, + zip: cfg.zip, + units, + lang: cfg.lang, + } + } +} diff --git a/owm_widg_config/src/general.rs b/owm_widg_config/src/general.rs new file mode 100644 index 0000000..e4b0361 --- /dev/null +++ b/owm_widg_config/src/general.rs @@ -0,0 +1,6 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct General { + pub api_version: String, // "free_2.5", "onecall_3.0", etc. +} diff --git a/owm_widg_config/src/lib.rs b/owm_widg_config/src/lib.rs new file mode 100644 index 0000000..c7f5b50 --- /dev/null +++ b/owm_widg_config/src/lib.rs @@ -0,0 +1,3 @@ +pub mod general; +pub mod free25; +pub mod config; \ No newline at end of file diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 390facf..43b8053 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -5,5 +5,8 @@ edition = "2024" [dependencies] owm_api25 = { path = "../owm_api25" } +owm_widg_config = { path = "../owm_widg_config" } reqwest = {version = "0.12.23", features = ["blocking", "json"] } +toml = "0.9.7" +dirs = "6.0.0" diff --git a/widget/src/main.rs b/widget/src/main.rs index e6b4266..bca7353 100644 --- a/widget/src/main.rs +++ b/widget/src/main.rs @@ -1,18 +1,37 @@ -use owm_api25::{config::Config, weather::WeatherResponse}; +use owm_api25::weather::WeatherResponse; +use owm_widg_config::config::Config; use reqwest::blocking; +use std::fs; fn main() -> Result<(), Box> { - let config = Config::load()?; - let url = config.build_url()?; + let path = dirs::config_dir() + .ok_or("No config dir found")? + .join("candywidgets/openweathermap.toml"); - let resp = blocking::get(&url)?.json::()?; + let toml_str = fs::read_to_string(&path) + .map_err(|_| format!("Failed to read config: {}", path.display()))?; - println!("City: {}, {}", resp.name, resp.sys.country); - if let Some(w) = resp.weather.first() { - println!("Weather: {}", w.main); - println!("Icon: {}", w.icon); + // Deserialize whole config + let cfg: Config = toml::from_str(&toml_str)?; + + match cfg.general.api_version.as_str() { + "free_2.5" => { + let query = owm_api25::query::QueryParams::from(cfg.query_params); + + let url = query.weather_url()?; + let resp = blocking::get(&url)?.json::()?; + + println!("City: {}, {}", resp.name, resp.sys.country); + if let Some(w) = resp.weather.first() { + println!("Weather: {}", w.main); + println!("Icon: {}", w.icon); + } + println!("Temperature: {}°C", resp.main.temp); + } + other => { + return Err(format!("Unsupported api_version: {other}").into()); + } } - println!("Temperature: {}°C", resp.main.temp); Ok(()) }