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]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
"owm_api25",
|
"owm_api25", "owm_widg_config",
|
||||||
"widget",
|
"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 forecast;
|
||||||
pub mod weather;
|
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]
|
[dependencies]
|
||||||
owm_api25 = { path = "../owm_api25" }
|
owm_api25 = { path = "../owm_api25" }
|
||||||
|
owm_widg_config = { path = "../owm_widg_config" }
|
||||||
reqwest = {version = "0.12.23", features = ["blocking", "json"] }
|
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 reqwest::blocking;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load()?;
|
let path = dirs::config_dir()
|
||||||
let url = config.build_url()?;
|
.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>()?;
|
let resp = blocking::get(&url)?.json::<WeatherResponse>()?;
|
||||||
|
|
||||||
println!("City: {}, {}", resp.name, resp.sys.country);
|
println!("City: {}, {}", resp.name, resp.sys.country);
|
||||||
@ -13,6 +27,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("Icon: {}", w.icon);
|
println!("Icon: {}", w.icon);
|
||||||
}
|
}
|
||||||
println!("Temperature: {}°C", resp.main.temp);
|
println!("Temperature: {}°C", resp.main.temp);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(format!("Unsupported api_version: {other}").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user