use std::fmt::Formatter; use std::time::Duration; use std::{env, fs}; use regex::Regex; use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer}; #[derive(Default, Deserialize)] pub struct Config { #[serde(default, deserialize_with = "deserialize_duration")] pub interval: Option, #[serde(default)] pub colors: ColorConfig, #[serde(default)] pub checks: Vec, } impl Config { fn get_config_file() -> String { match env::args().nth(1) { Some(config_file) => config_file, None => format!( "{}/.checkbar.toml", dirs::home_dir().unwrap().to_str().unwrap_or("") ), } } pub fn read() -> Self { Self::read_file(Self::get_config_file().as_str()) } pub fn read_file(filename: &str) -> Self { match fs::read_to_string(filename) { Ok(config) => toml::from_str(config.as_str()).unwrap_or_default(), Err(_) => Config::default(), } } } #[derive(Deserialize)] pub struct ColorConfig { pub up: String, pub warn: String, pub down: String, } impl Default for ColorConfig { fn default() -> Self { Self { up: String::from("#00FF00"), warn: String::from("#FFFF00"), down: String::from("#FF0000"), } } } #[derive(Deserialize)] pub struct CheckConfig { pub name: String, pub url: String, pub check_type: Option, pub click_cmd: Option, } #[derive(Deserialize, PartialEq, Eq)] pub enum CheckType { Http, Actuator, Tcp, } fn deserialize_duration<'de, D>(d: D) -> Result, D::Error> where D: Deserializer<'de>, { struct StringVisitor; impl<'de> Visitor<'de> for StringVisitor { type Value = String; fn expecting(&self, f: &mut Formatter) -> std::fmt::Result { f.write_str("a number or string with parsable duration") } fn visit_i64(self, v: i64) -> Result where E: Error, { Ok(format!("{}", v)) } fn visit_str(self, v: &str) -> Result where E: Error, { Ok(v.to_string()) } } match d.deserialize_string(StringVisitor) { Ok(value) => Ok(parse_duration(value.as_str())), Err(err) => Err(err), } } fn parse_duration(value: &str) -> Option { let mut duration_in_secs = 0; if let Ok(re) = Regex::new(r"^((?P\d+)h\s*)?((?P\d+)m\s*)?((?P\d+)s?\s*)?$") { if re.is_match(value) { let parts = re.captures_iter(value).next().unwrap(); if let Some(hours) = parts.name("hours") { duration_in_secs += hours.as_str().parse::().unwrap_or(0) * 60 * 60 } if let Some(minutes) = parts.name("minutes") { duration_in_secs += minutes.as_str().parse::().unwrap_or(0) * 60 } if let Some(seconds) = parts.name("seconds") { duration_in_secs += seconds.as_str().parse::().unwrap_or(0) } } else { return None; } } Some(Duration::from_secs(duration_in_secs)) } #[cfg(test)] mod tests { use std::time::Duration; use crate::config::{parse_duration, Config}; #[test] fn test_should_parse_config_with_number_interval() { let config: Config = toml::from_str( r#" interval = 123 [[checks]] name = "example" url = "https://example.com" "#, ) .unwrap(); assert_eq!(config.interval, Some(Duration::from_secs(123))); } #[test] fn test_should_parse_config_with_parsed_interval() { let config: Config = toml::from_str( r#" interval = "2m 3s" [[checks]] name = "example" url = "https://example.com" "#, ) .unwrap(); assert_eq!(config.interval, Some(Duration::from_secs(123))); } #[test] fn test_should_parse_config_without_interval() { let config: Config = toml::from_str( r#" [[checks]] name = "example" url = "https://example.com" "#, ) .unwrap(); assert_eq!(config.interval, None); } #[test] fn test_should_parse_config_without_checks() { let config: Config = toml::from_str( r#" interval = "2m 3s" "#, ) .unwrap(); assert_eq!(config.checks.len(), 0); } #[test] fn test_should_parse_config_with_default_colors() { let config: Config = toml::from_str( r#" interval = "2m 3s" "#, ) .unwrap(); assert_eq!(config.colors.up, "#00FF00".to_string()); assert_eq!(config.colors.warn, "#FFFF00".to_string()); assert_eq!(config.colors.down, "#FF0000".to_string()); } #[test] fn test_should_read_and_parse_file() { let config = Config::read_file("./tests/testconfig1.toml"); assert_eq!(config.interval, Some(Duration::from_secs(10))); assert_eq!(config.checks.len(), 1); assert_eq!(config.checks[0].name, "www"); assert_eq!(config.checks[0].url, "https://example.com"); } #[test] fn test_should_return_default_if_no_config_file() { let config = Config::read_file("./tests/no_testconfig.toml"); assert_eq!(config.interval, None); assert_eq!(config.checks.len(), 0); } #[test] fn test_should_parse_durations() { assert_eq!(parse_duration("1m30s"), Some(Duration::from_secs(90))); assert_eq!(parse_duration("2m"), Some(Duration::from_secs(120))); assert_eq!(parse_duration("1h1m1s"), Some(Duration::from_secs(3661))); assert_eq!(parse_duration("90"), Some(Duration::from_secs(90))); } #[test] fn test_should_parse_durations_with_whitespaces() { assert_eq!(parse_duration("1m 30s"), Some(Duration::from_secs(90))); assert_eq!(parse_duration("1h 1m 1s"), Some(Duration::from_secs(3661))); } #[test] fn test_should_return_default_for_unparseable_durations() { assert_eq!(parse_duration("invalid"), None); assert_eq!(parse_duration("1x30m10q"), None); } }