use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::time::Duration;
pub fn parse_query_params(query: &str) -> HashMap<String, String> {
query
.split('&')
.filter(|s| !s.is_empty())
.filter_map(|pair| {
let (k, v) = pair.split_once('=')?;
Some((k.to_lowercase(), v.to_string()))
})
.collect()
}
pub fn parse_duration(s: &str) -> std::result::Result<Duration, String> {
let s = s.trim();
if s.is_empty() {
return Err("Empty duration string".into());
}
if let Ok(secs) = s.parse::<u64>() {
return Ok(Duration::from_secs(secs));
}
let num_end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
let (num_str, unit) = s.split_at(num_end);
if num_str.is_empty() {
return Err(format!("Invalid duration string: '{s}'"));
}
let n: u64 = num_str.parse().map_err(|_| format!("Invalid duration number in: '{s}'"))?;
match unit {
"µs" | "us" => Ok(Duration::from_micros(n)),
"ms" => Ok(Duration::from_millis(n)),
"s" => Ok(Duration::from_secs(n)),
"m" => Ok(Duration::from_secs(n * 60)),
"h" => Ok(Duration::from_secs(n * 3600)),
"d" => Ok(Duration::from_secs(n * 86400)),
_ => Err(format!(
"Unknown duration unit '{unit}' in: '{s}'. Expected µs, us, ms, s, m, h, or d"
)),
}
}
pub fn format_duration(d: Duration) -> String {
let micros = d.as_micros() as u64;
if micros == 0 {
return "0".to_string();
}
let secs = d.as_secs();
if secs > 0 && secs.is_multiple_of(86400) && d.subsec_nanos() == 0 {
return format!("{}d", secs / 86400);
}
if secs > 0 && secs.is_multiple_of(3600) && d.subsec_nanos() == 0 {
return format!("{}h", secs / 3600);
}
if secs > 0 && secs.is_multiple_of(60) && d.subsec_nanos() == 0 {
return format!("{}m", secs / 60);
}
if d.subsec_nanos() == 0 {
return format!("{secs}s");
}
if micros.is_multiple_of(1000) {
return format!("{}ms", micros / 1000);
}
format!("{micros}us")
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SyncMode {
Never,
#[default]
Every,
Interval(Duration),
}
impl fmt::Display for SyncMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Never => f.write_str("never"),
Self::Every => f.write_str("every"),
Self::Interval(d) => f.write_str(&format_duration(*d)),
}
}
}
impl FromStr for SyncMode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"never" => Ok(SyncMode::Never),
"every" => Ok(SyncMode::Every),
v => match parse_duration(v) {
Ok(dur) if dur.as_millis() > 100 => Ok(SyncMode::Interval(dur)),
_ => Err(format!(
"Invalid sync mode: '{v}'. Expected 'never', 'every', or a duration larger than 100ms (e.g. '1s')"
)),
},
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AolMode {
#[default]
Never,
Sync,
Async,
}
impl fmt::Display for AolMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Never => f.write_str("never"),
Self::Sync => f.write_str("sync"),
Self::Async => f.write_str("async"),
}
}
}
impl FromStr for AolMode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"never" => Ok(AolMode::Never),
"sync" => Ok(AolMode::Sync),
"async" => Ok(AolMode::Async),
v => Err(format!("Invalid aol mode: '{v}'. Expected 'never', 'sync', or 'async'")),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SnapshotMode {
#[default]
Never,
Interval(Duration),
}
impl fmt::Display for SnapshotMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Never => f.write_str("never"),
Self::Interval(d) => f.write_str(&format_duration(*d)),
}
}
}
impl FromStr for SnapshotMode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"never" => Ok(SnapshotMode::Never),
v => match parse_duration(v) {
Ok(dur) if dur.as_secs() > 30 => Ok(SnapshotMode::Interval(dur)),
_ => Err(format!(
"Invalid snapshot mode: '{v}'. Expected 'never', or a duration larger than 30s (e.g. '5m')"
)),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration_plain_seconds() {
assert_eq!(parse_duration("0").unwrap(), Duration::from_secs(0));
assert_eq!(parse_duration("60").unwrap(), Duration::from_secs(60));
assert_eq!(parse_duration("2592000").unwrap(), Duration::from_secs(2592000));
}
#[test]
fn test_parse_duration_with_units() {
assert_eq!(parse_duration("100µs").unwrap(), Duration::from_micros(100));
assert_eq!(parse_duration("100us").unwrap(), Duration::from_micros(100));
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
assert_eq!(parse_duration("60s").unwrap(), Duration::from_secs(60));
assert_eq!(parse_duration("30m").unwrap(), Duration::from_secs(30 * 60));
assert_eq!(parse_duration("24h").unwrap(), Duration::from_secs(24 * 3600));
assert_eq!(parse_duration("30d").unwrap(), Duration::from_secs(30 * 86400));
}
#[test]
fn test_parse_duration_invalid() {
assert!(parse_duration("").is_err());
assert!(parse_duration("abc").is_err());
assert!(parse_duration("30x").is_err());
assert!(parse_duration("30d8h").is_err());
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration(Duration::from_secs(0)), "0");
assert_eq!(format_duration(Duration::from_micros(500)), "500us");
assert_eq!(format_duration(Duration::from_millis(100)), "100ms");
assert_eq!(format_duration(Duration::from_secs(30)), "30s");
assert_eq!(format_duration(Duration::from_secs(60)), "1m");
assert_eq!(format_duration(Duration::from_secs(3600)), "1h");
assert_eq!(format_duration(Duration::from_secs(86400)), "1d");
assert_eq!(format_duration(Duration::from_secs(90)), "90s");
assert_eq!(format_duration(Duration::from_secs(90061)), "90061s");
}
#[test]
fn test_parse_query_params() {
let params = parse_query_params("versioned=true&sync=every");
assert_eq!(¶ms["versioned"], "true");
assert_eq!(¶ms["sync"], "every");
}
#[test]
fn test_sync_mode_parsing() {
assert_eq!("never".parse::<SyncMode>().unwrap(), SyncMode::Never);
assert_eq!("every".parse::<SyncMode>().unwrap(), SyncMode::Every);
assert_eq!("5s".parse::<SyncMode>().unwrap(), SyncMode::Interval(Duration::from_secs(5)));
assert_eq!("1m".parse::<SyncMode>().unwrap(), SyncMode::Interval(Duration::from_secs(60)));
assert!("invalid".parse::<SyncMode>().is_err());
}
#[test]
fn test_aol_mode_parsing() {
assert_eq!("never".parse::<AolMode>().unwrap(), AolMode::Never);
assert_eq!("sync".parse::<AolMode>().unwrap(), AolMode::Sync);
assert_eq!("async".parse::<AolMode>().unwrap(), AolMode::Async);
assert!("invalid".parse::<AolMode>().is_err());
}
#[test]
fn test_snapshot_mode_parsing() {
assert_eq!("never".parse::<SnapshotMode>().unwrap(), SnapshotMode::Never);
assert_eq!(
"60s".parse::<SnapshotMode>().unwrap(),
SnapshotMode::Interval(Duration::from_secs(60))
);
assert_eq!(
"5m".parse::<SnapshotMode>().unwrap(),
SnapshotMode::Interval(Duration::from_secs(300))
);
assert!("invalid".parse::<SnapshotMode>().is_err());
assert!("10s".parse::<SnapshotMode>().is_err());
}
}