Separate config loading from API library
- Create new library crate for config parsing - Modularity & flexibility
This commit is contained in:
parent
d45ad9bad5
commit
01c12478f6
@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"owm_api25",
|
||||
"owm_api25", "owm_widg_config",
|
||||
"widget",
|
||||
]
|
||||
|
@ -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("&")))
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
pub mod config;
|
||||
pub mod forecast;
|
||||
pub mod weather;
|
||||
pub mod query;
|
||||
|
71
owm_api25/src/query.rs
Normal file
71
owm_api25/src/query.rs
Normal 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("&")))
|
||||
}
|
||||
}
|
10
owm_widg_config/Cargo.toml
Normal file
10
owm_widg_config/Cargo.toml
Normal 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"
|
9
owm_widg_config/src/config.rs
Normal file
9
owm_widg_config/src/config.rs
Normal 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,
|
||||
}
|
45
owm_widg_config/src/free25.rs
Normal file
45
owm_widg_config/src/free25.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
6
owm_widg_config/src/general.rs
Normal file
6
owm_widg_config/src/general.rs
Normal 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.
|
||||
}
|
3
owm_widg_config/src/lib.rs
Normal file
3
owm_widg_config/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod general;
|
||||
pub mod free25;
|
||||
pub mod config;
|
@ -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"
|
||||
|
||||
|
@ -1,10 +1,24 @@
|
||||
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 toml_str = fs::read_to_string(&path)
|
||||
.map_err(|_| format!("Failed to read config: {}", path.display()))?;
|
||||
|
||||
// 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);
|
||||
@ -13,6 +27,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Icon: {}", w.icon);
|
||||
}
|
||||
println!("Temperature: {}°C", resp.main.temp);
|
||||
}
|
||||
other => {
|
||||
return Err(format!("Unsupported api_version: {other}").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user