Better error handling

[With input from AI in VSCode]
This commit is contained in:
2025-07-26 13:14:05 -07:00
parent 0e027005a1
commit a8591da49b
3 changed files with 66 additions and 28 deletions

View File

@@ -14,3 +14,4 @@ chrono = "0.4.41"
chrono-tz = "0.10.3" chrono-tz = "0.10.3"
iana-time-zone = "0.1.63" iana-time-zone = "0.1.63"
clap = { version = "4.5.1", features = ["derive"] } clap = { version = "4.5.1", features = ["derive"] }
thiserror = "1.0"

View File

@@ -3,11 +3,19 @@ use curl::easy::{Easy, List};
use serde_json::json; use serde_json::json;
use serde_json::Value; use serde_json::Value;
use regex::Regex; use regex::Regex;
use thiserror::Error;
use std::num::ParseFloatError;
use crate::common; use crate::common;
use crate::common::UpdateResult; use crate::common::UpdateResult;
use crate::bar_modules; use crate::bar_modules;
/// Number of cycles between weather updates
const UPDATE_INTERVAL: u32 = 300;
/// Base URL for the National Weather Service API
const WEATHER_API_BASE: &str = "https://api.weather.gov";
#[derive(Clone)] #[derive(Clone)]
pub struct UnibarModuleWeather { pub struct UnibarModuleWeather {
opts: common::AppOptions, opts: common::AppOptions,
@@ -15,7 +23,27 @@ pub struct UnibarModuleWeather {
update_cnt: u32, update_cnt: u32,
} }
#[derive(Debug, Error)]
pub enum WeatherError {
#[error("Invalid temperature format: {0}")]
ParseError(String),
#[error("API response missing temperature field: {0}")]
ApiResponseStrErr(String),
}
impl From<ParseFloatError> for WeatherError {
fn from(err: ParseFloatError) -> Self {
WeatherError::ParseError(err.to_string())
}
}
impl UnibarModuleWeather { impl UnibarModuleWeather {
const FAHRENHEIT_MULTIPLIER: f64 = 9.0 / 5.0;
const FAHRENHEIT_OFFSET: f64 = 32.0;
fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * Self::FAHRENHEIT_MULTIPLIER + Self::FAHRENHEIT_OFFSET
}
// -------------------- // --------------------
pub fn new(o :common::AppOptions) -> Self { pub fn new(o :common::AppOptions) -> Self {
@@ -27,23 +55,27 @@ impl UnibarModuleWeather {
} }
// -------------------- // --------------------
fn get_current_temperature(&self) -> f32 { fn get_current_temperature(&self) -> Result<f64, WeatherError> {
let deg_c :f32; let deg_c = match &self.weather_info {
serde_json::Value::Null => 0.0,
match self.weather_info {
serde_json::Value::Null => {
deg_c = 0.0;
}
_ => { _ => {
deg_c = self.weather_info["features"][0]["properties"]["temperature"]["value"] // Safely navigate the JSON structure
.to_string().parse().unwrap(); self.weather_info
.get("features")
.and_then(|f| f.get(0))
.and_then(|f| f.get("properties"))
.and_then(|p| p.get("temperature"))
.and_then(|t| t.get("value"))
.and_then(|v| v.as_f64())
.ok_or(WeatherError::ApiResponseStrErr("as_str likely failed".to_string()))?
// .parse()?
} }
} };
match self.opts.weather_units { Ok(match self.opts.weather_units {
common::TemperatureUnits::Metric => return deg_c, common::TemperatureUnits::Metric => deg_c,
common::TemperatureUnits::Imperial => return (deg_c * 9.0 / 5.0) + 32.0, common::TemperatureUnits::Imperial => Self::celsius_to_fahrenheit(deg_c),
} })
} }
// -------------------- // --------------------
@@ -100,7 +132,8 @@ impl UnibarModuleWeather {
let mut url_string = Vec::new(); let mut url_string = Vec::new();
let mut curl_ret = Vec::new(); let mut curl_ret = Vec::new();
url_string.push("https://api.weather.gov/icons".to_owned()); url_string.push(WEATHER_API_BASE.to_owned());
url_string.push("/icons".to_owned());
curl.url(url_string.concat().as_str()).unwrap(); curl.url(url_string.concat().as_str()).unwrap();
{ {
@@ -171,7 +204,7 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather {
// Weather update every 60 cycles // Weather update every 60 cycles
fn should_update(&mut self) -> UpdateResult { fn should_update(&mut self) -> UpdateResult {
if self.update_cnt == 0 { if self.update_cnt == 0 {
self.update_cnt = 300; self.update_cnt = UPDATE_INTERVAL;
return UpdateResult::Update; return UpdateResult::Update;
} else { } else {
self.update_cnt -= 1; self.update_cnt -= 1;
@@ -192,7 +225,8 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather {
let mut curl_ret = Vec::new(); let mut curl_ret = Vec::new();
let mut curl_err = false; let mut curl_err = false;
url_string.push("https://api.weather.gov/stations/".to_owned()); url_string.push(WEATHER_API_BASE.to_owned());
url_string.push("/stations/".to_owned());
url_string.push(self.opts.weather_station.to_owned()); url_string.push(self.opts.weather_station.to_owned());
url_string.push("/observations?limit=1".to_owned()); url_string.push("/observations?limit=1".to_owned());
@@ -233,8 +267,8 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather {
// -------------------- // --------------------
fn get_content(&self) -> String { fn get_content(&self) -> String {
let temperature_value :f32 = self.get_current_temperature(); let temperature_value :f64 = self.get_current_temperature().expect("Temperature query");
let temperature_unit :String = self.get_temperature_unit(); let temperature_unit :String = self.get_temperature_unit();
// let temperature_icon :String = self.get_icon(v.clone()); // let temperature_icon :String = self.get_icon(v.clone());
return format!("{:.0}{}", temperature_value, temperature_unit); return format!("{:.0}{}", temperature_value, temperature_unit);
@@ -251,6 +285,9 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather {
// "icon": "https://api.weather.gov/icons/land/night/ovc?size=medium", // "icon": "https://api.weather.gov/icons/land/night/ovc?size=medium",
let re = Regex::new(r"(\w+)\?size").unwrap(); let re = Regex::new(r"(\w+)\?size").unwrap();
let json_val = self.weather_info["features"][0]["properties"]["icon"].to_string(); let json_val = self.weather_info["features"][0]["properties"]["icon"].to_string();
if self.opts.debug {
println!("-----> weather_data - {:#?}", self.weather_info["features"][0]["properties"]);
}
match self.weather_info["features"][0]["properties"]["icon"] { match self.weather_info["features"][0]["properties"]["icon"] {
serde_json::Value::Null => { serde_json::Value::Null => {
return "".to_string(); return "".to_string();

View File

@@ -3,34 +3,34 @@ use crate::common::UpdateResult;
// -------------------- // --------------------
/// All Bar modules must implement the actions /// All Bar modules must implement the actions
pub trait BarModuleActions { pub trait BarModuleActions {
/// Return String with name of the module /// Returns the name of the module
fn get_name(&self) -> String; fn get_name(&self) -> String;
/// Do necessary clean up before starting new fetch /// Cleans up any state before refreshing data
fn clear(&mut self); fn clear(&mut self);
/// Do necessary processing to generate data for this bar module /// Fetches and processes new data for the module
fn generate_data(&mut self); fn generate_data(&mut self);
/// Return String content to be displayed in the bar /// Returns the formatted content to display in the bar
fn get_content(&self) -> String; fn get_content(&self) -> String;
/// Return a Unicode icon to display before content in the bar. /// Returns a Unicode icon representing the module's current state
/// This icon may differ based on content of the data
fn get_icon(&self) -> String; fn get_icon(&self) -> String;
/// Returns UpdateResult depending on whether the module /// Determines if the module should update its data
/// should be updated during the current update cycle /// Default implementation always returns Update
fn should_update(&mut self) -> UpdateResult { fn should_update(&mut self) -> UpdateResult {
return UpdateResult::Update; return UpdateResult::Update;
} }
} }
pub trait BarModuleDebug { pub trait BarModuleDebug {
/// Print debug information at the end /// Prints debug information about the module
fn post_debug(&self); fn post_debug(&self);
} }
// Module declarations
pub mod bar_module_weather; pub mod bar_module_weather;
pub mod bar_module_music; pub mod bar_module_music;
pub mod bar_module_network; pub mod bar_module_network;