From 8e5d4720183bddcc9b6419504a234e487028f345 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 25 Jul 2024 18:16:12 +0200 Subject: [PATCH] tests and time support --- src/config.rs | 95 ++++++++++++++++++++++++++++++- src/lib.rs | 5 +- src/tests/config_serialization.rs | 75 ++++++++++++++++++++++++ src/tests/mod.rs | 2 + 4 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 src/tests/config_serialization.rs create mode 100644 src/tests/mod.rs diff --git a/src/config.rs b/src/config.rs index e12c01d..64d7b2a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,16 +6,20 @@ use log::warn; use serde::{self, Deserialize, Serialize}; use std::env; use std::path::{Path, PathBuf}; +use std::time::Duration; use tokio::{fs, io::AsyncReadExt}; use crate::PROGRAM_NAME; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct Config { - pub zone_id: Box, - pub api_key: Box, + pub zone_id: String, + pub api_key: String, pub domains: Vec>, + #[serde(default)] pub max_errors_in_row: Option, + #[serde(with = "duration_format", default)] + pub max_duration: Option, } pub async fn get_config_path() -> Result> { @@ -67,3 +71,88 @@ pub async fn read_config>(path: &P) -> anyhow::Result { file.read_to_string(&mut buf).await?; Ok(toml::from_str(&buf)?) } + +mod duration_format { + use super::*; + use serde::{self, Deserialize, Deserializer, Serializer}; + use std::time::Duration; + + pub fn serialize(duration: &Option, serializer: S) -> Result + where + S: Serializer, + { + let duration = if let Some(aux) = duration.as_ref() { + aux + } else { + return serializer.serialize_none(); + }; + + let mut secs = duration.as_secs(); + let nanos = duration.subsec_nanos(); + + let days = secs / 86400; + secs %= 86400; + let hours = secs / 3600; + secs %= 3600; + let minutes = secs / 60; + secs %= 60; + + let mut formatted = String::new(); + if days > 0 { + formatted.push_str(&format!("{}d ", days)); + } + if hours > 0 { + formatted.push_str(&format!("{}h ", hours)); + } + if minutes > 0 { + formatted.push_str(&format!("{}m ", minutes)); + } + if secs > 0 { + formatted.push_str(&format!("{}s ", secs)); + } + if nanos > 0 { + formatted.push_str(&format!("{}ns", nanos)); + } + + serializer.serialize_str(formatted.trim()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::>::deserialize(deserializer)?.map_or(Ok(None), |s| { + parse_duration(&s) + .map(Some) + .map_err(serde::de::Error::custom) + }) + } + + fn parse_duration(s: &str) -> Result { + let mut total_duration = Duration::new(0, 0); + let units = [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)]; + let mut remainder = s; + + for &(unit, factor) in &units { + if let Some(idx) = remainder.find(unit) { + let (value, rest) = remainder.split_at(idx); + let value: u64 = value.trim().parse().map_err(|_| "Invalid number")?; + total_duration += Duration::from_secs(value * factor); + remainder = &rest[unit.len()..]; + } + } + + if let Some(idx) = remainder.find("ns") { + let (value, rest) = remainder.split_at(idx); + let value: u32 = value.trim().parse().map_err(|_| "Invalid number")?; + total_duration += Duration::new(0, value); + remainder = &rest["ns".len()..]; + } + + if !remainder.trim().is_empty() { + return Err("Invalid duration format".to_string()); + } + + Ok(total_duration) + } +} diff --git a/src/lib.rs b/src/lib.rs index bd6adfd..244b6d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,15 +3,16 @@ mod cloudflare; mod config; mod logging; -mod public_ip; mod message_handler; +mod public_ip; +mod tests; pub mod utils; pub use cloudflare::CloudflareClient; pub use config::{get_config_path, read_config, Config}; pub use logging::init_logger; -pub use public_ip::get_current_public_ipv4; pub use message_handler::MessageHandler; +pub use public_ip::get_current_public_ipv4; pub const PROGRAM_NAME: &'static str = "dynip-cloudflare"; pub const MAX_ERORS_IN_ROW_DEFAULT: usize = 10; diff --git a/src/tests/config_serialization.rs b/src/tests/config_serialization.rs new file mode 100644 index 0000000..ba95f87 --- /dev/null +++ b/src/tests/config_serialization.rs @@ -0,0 +1,75 @@ +use std::time::Duration; +use toml; +use crate::Config; + +const TOML_STR_ONE: &str = r#" +zone_id = "" +api_key = "" +domains = [""] +max_duration = "1d 2h 30m 45s 500000000ns" +"#; + +#[test] +fn test_deserialize() { + let config: Config = toml::from_str(TOML_STR_ONE).unwrap(); + assert_eq!(config.max_duration, Some(Duration::new(95445, 500000000))); +} + +#[test] +fn test_serialize() { + let config = Config { + zone_id: "".into(), + api_key: "".into(), + domains: vec!["".into()], + max_errors_in_row: None, + max_duration: Some(Duration::new(95445, 500000000)), + }; + let toml_str = toml::to_string(&config).unwrap(); + assert_eq!(TOML_STR_ONE.trim(), toml_str.trim()); +} + +#[test] +fn test_deserialize_none() { + let toml_str = r#" +zone_id = "" +api_key = "" +domains = [""] +max_errors_in_row = 5 +"#; + let config: Config = toml::from_str(toml_str).unwrap(); + assert_eq!(config.max_duration, None); +} + +#[test] +fn test_serialize_none() { + let toml_to_be = r#" +zone_id = "" +api_key = "" +domains = [""] +"#; + let config = Config { + zone_id: "".into(), + api_key: "".into(), + domains: vec!["".into()], + max_errors_in_row: None, + max_duration: None, + }; + let toml_str = toml::to_string(&config).unwrap(); + assert_eq!(toml_to_be.trim(), toml_str.trim()); +} + +fn main() { + let toml_str = r#" +zone_id = "" +api_key = "" +domains = [""] +max_errors_in_row = 5 +max_duration = "1d 2h 30m 45s 500000000ns" +"#; + + let config: Config = toml::from_str(toml_str).unwrap(); + println!("{:?}", config); + + let toml_out = toml::to_string(&config).unwrap(); + println!("{}", toml_out); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..8c8b93c --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +mod config_serialization;