1use std::str::FromStr;
28
29use chrono::{DateTime, TimeZone, Utc};
30use simplelog::LevelFilter;
31
32const TIMESTAMP_FMT_EN_US: &str = "%a %b %e %I:%M:%S %p %z %Y";
33const S_TO_MS_CUTOFF: i64 = 1000000000000;
34
35#[derive(Debug, thiserror::Error)]
36pub enum Error {
37 #[error("Logger initialization error")]
38 Logger(#[from] log::SetLoggerError),
39 #[error("Invalid timestamp format")]
40 InvalidTimestamp(String),
41}
42
43fn select_log_level_filter(verbosity: u8) -> LevelFilter {
44 match verbosity {
45 0 => LevelFilter::Off,
46 1 => LevelFilter::Error,
47 2 => LevelFilter::Warn,
48 3 => LevelFilter::Info,
49 4 => LevelFilter::Debug,
50 _ => LevelFilter::Trace,
51 }
52}
53
54#[derive(clap::Args, Debug, Clone, PartialEq, Eq)]
55pub struct Verbosity {
56 #[clap(long, short = 'v', global = true, action = clap::ArgAction::Count)]
58 verbose: u8,
59}
60
61impl Verbosity {
62 pub fn new(verbose: u8) -> Self {
63 Self { verbose }
64 }
65
66 pub fn init_logging(&self) -> Result<(), Error> {
68 Ok(simplelog::TermLogger::init(
69 select_log_level_filter(self.verbose),
70 simplelog::Config::default(),
71 simplelog::TerminalMode::Stderr,
72 simplelog::ColorChoice::Auto,
73 )?)
74 }
75}
76
77#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
79pub struct Timestamp(DateTime<Utc>);
80
81impl From<Timestamp> for DateTime<Utc> {
82 fn from(value: Timestamp) -> Self {
83 value.0
84 }
85}
86
87impl FromStr for Timestamp {
88 type Err = Error;
89
90 fn from_str(s: &str) -> Result<Self, Self::Err> {
91 s.parse::<i64>()
92 .ok()
93 .and_then(|timestamp_n| {
94 if timestamp_n < S_TO_MS_CUTOFF {
95 Utc.timestamp_opt(timestamp_n, 0).single()
96 } else {
97 Utc.timestamp_millis_opt(timestamp_n).single()
98 }
99 })
100 .map(Timestamp)
101 .or_else(|| {
102 DateTime::parse_from_str(&tz_name_to_offset(s), TIMESTAMP_FMT_EN_US)
103 .ok()
104 .map(|timestamp| Timestamp(timestamp.into()))
105 })
106 .ok_or_else(|| Error::InvalidTimestamp(s.to_string()))
107 }
108}
109
110fn tz_name_to_offset(input: &str) -> String {
112 input.replace("CET", "+0100").replace("CEST", "+0200")
113}
114
115pub mod prelude {
116 pub use super::{Timestamp, Verbosity};
117 pub use ::clap::Parser;
118 pub use clap;
119 pub mod log {
120 pub use log::{error, info, warn, SetLoggerError};
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 #[test]
127 fn test_prelude() {
128 use super::prelude::*;
129 use chrono::{TimeZone, Utc};
130
131 #[derive(Debug, Parser, PartialEq, Eq)]
132 #[clap(name = "test", version, author)]
133 struct Opts {
134 #[clap(flatten)]
135 verbose: Verbosity,
136 #[clap(long)]
137 timestamp_a: Timestamp,
138 #[clap(long)]
139 timestamp_b: Timestamp,
140 #[clap(long)]
141 timestamp_c: Timestamp,
142 }
143
144 let parsed = Opts::try_parse_from([
145 "test",
146 "-vvvv",
147 "--timestamp-a",
148 "1692946034",
149 "--timestamp-b",
150 "Fri Aug 25 08:47:09 AM CEST 2023",
151 "--timestamp-c",
152 "1692946034632",
153 ])
154 .unwrap();
155
156 let expected = Opts {
157 verbose: Verbosity { verbose: 4 },
158 timestamp_a: Timestamp(Utc.timestamp_opt(1692946034, 0).single().unwrap()),
159 timestamp_b: Timestamp(Utc.timestamp_opt(1692946029, 0).single().unwrap()),
160 timestamp_c: Timestamp(Utc.timestamp_opt(1692946034, 632000000).single().unwrap()),
161 };
162
163 assert_eq!(parsed, expected);
164 }
165}