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,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(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user