Separate config loading from API library

- Create new library crate for config parsing
- Modularity & flexibility
This commit is contained in:
Candifloss 2025-09-20 08:41:28 +05:30
parent d45ad9bad5
commit 01c12478f6
11 changed files with 177 additions and 91 deletions

View File

@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = [
"owm_api25",
"owm_api25", "owm_widg_config",
"widget",
]

View File

@ -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<String>, // Any of these location parameters are required
pub city_name: Option<String>, // Find City ID or Name from https://openweathermap.org/find?
pub lat: Option<f32>, // Latitude and Longitude must be used together
pub lon: Option<f32>,
pub zip: Option<String>, // 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<Self, Box<dyn std::error::Error>> {
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<String, String> {
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("&")))
}
}

View File

@ -1,3 +1,3 @@
pub mod config;
pub mod forecast;
pub mod weather;
pub mod query;

71
owm_api25/src/query.rs Normal file
View File

@ -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<String>,
pub city_name: Option<String>,
pub lat: Option<f32>,
pub lon: Option<f32>,
pub zip: Option<String>,
pub units: Units,
pub lang: String,
}
impl QueryParams {
/// Build query URL for the `/weather` endpoint
pub fn weather_url(&self) -> Result<String, String> {
self.build_url(WEATHER_URL)
}
/// Build query URL for the `/forecast` endpoint
pub fn forecast_url(&self) -> Result<String, String> {
self.build_url(FORECAST_URL)
}
fn build_url(&self, base: &str) -> Result<String, String> {
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("&")))
}
}

View File

@ -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"

View File

@ -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,
}

View File

@ -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<String>,
pub city_name: Option<String>,
pub lat: Option<f32>,
pub lon: Option<f32>,
pub zip: Option<String>,
#[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<Free25Query> 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,
}
}
}

View File

@ -0,0 +1,6 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct General {
pub api_version: String, // "free_2.5", "onecall_3.0", etc.
}

View File

@ -0,0 +1,3 @@
pub mod general;
pub mod free25;
pub mod config;

View File

@ -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"

View File

@ -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<dyn std::error::Error>> {
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::<WeatherResponse>()?;
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::<WeatherResponse>()?;
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(())
}