use std::str::FromStr;
use chrono::{DateTime, TimeZone, Utc};
use simplelog::LevelFilter;
const TIMESTAMP_FMT_EN_US: &str = "%a %b %e %I:%M:%S %p %z %Y";
const S_TO_MS_CUTOFF: i64 = 1000000000000;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Logger initialization error")]
Logger(#[from] log::SetLoggerError),
#[error("Invalid timestamp format")]
InvalidTimestamp(String),
}
fn select_log_level_filter(verbosity: u8) -> LevelFilter {
match verbosity {
0 => LevelFilter::Off,
1 => LevelFilter::Error,
2 => LevelFilter::Warn,
3 => LevelFilter::Info,
4 => LevelFilter::Debug,
_ => LevelFilter::Trace,
}
}
#[derive(clap::Args, Debug, Clone, PartialEq, Eq)]
pub struct Verbosity {
#[clap(long, short = 'v', global = true, action = clap::ArgAction::Count)]
verbose: u8,
}
impl Verbosity {
pub fn new(verbose: u8) -> Self {
Self { verbose }
}
pub fn init_logging(&self) -> Result<(), Error> {
Ok(simplelog::TermLogger::init(
select_log_level_filter(self.verbose),
simplelog::Config::default(),
simplelog::TerminalMode::Stderr,
simplelog::ColorChoice::Auto,
)?)
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Timestamp(DateTime<Utc>);
impl From<Timestamp> for DateTime<Utc> {
fn from(value: Timestamp) -> Self {
value.0
}
}
impl FromStr for Timestamp {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<i64>()
.ok()
.and_then(|timestamp_n| {
if timestamp_n < S_TO_MS_CUTOFF {
Utc.timestamp_opt(timestamp_n, 0).single()
} else {
Utc.timestamp_millis_opt(timestamp_n).single()
}
})
.map(Timestamp)
.or_else(|| {
DateTime::parse_from_str(&tz_name_to_offset(s), TIMESTAMP_FMT_EN_US)
.ok()
.map(|timestamp| Timestamp(timestamp.into()))
})
.ok_or_else(|| Error::InvalidTimestamp(s.to_string()))
}
}
fn tz_name_to_offset(input: &str) -> String {
input.replace("CET", "+0100").replace("CEST", "+0200")
}
pub mod prelude {
pub use super::{Timestamp, Verbosity};
pub use ::clap::Parser;
pub use clap;
pub mod log {
pub use log::{error, info, warn, SetLoggerError};
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_prelude() {
use super::prelude::*;
use chrono::{TimeZone, Utc};
#[derive(Debug, Parser, PartialEq, Eq)]
#[clap(name = "test", version, author)]
struct Opts {
#[clap(flatten)]
verbose: Verbosity,
#[clap(long)]
timestamp_a: Timestamp,
#[clap(long)]
timestamp_b: Timestamp,
#[clap(long)]
timestamp_c: Timestamp,
}
let parsed = Opts::try_parse_from([
"test",
"-vvvv",
"--timestamp-a",
"1692946034",
"--timestamp-b",
"Fri Aug 25 08:47:09 AM CEST 2023",
"--timestamp-c",
"1692946034632",
])
.unwrap();
let expected = Opts {
verbose: Verbosity { verbose: 4 },
timestamp_a: Timestamp(Utc.timestamp_opt(1692946034, 0).single().unwrap()),
timestamp_b: Timestamp(Utc.timestamp_opt(1692946029, 0).single().unwrap()),
timestamp_c: Timestamp(Utc.timestamp_opt(1692946034, 632000000).single().unwrap()),
};
assert_eq!(parsed, expected);
}
}