tardis_cli/
lib.rs

1pub mod cli;
2pub mod config;
3pub mod core;
4pub mod errors;
5
6use core::App;
7
8use chrono_tz::Tz;
9use cli::Command;
10use config::Config;
11pub use errors::{Error, Failable, Result};
12
13impl App {
14    /// Build an [`App`] from the parsed CLI and loaded configuration.
15    ///
16    /// * CLI values **override** config values.
17    /// * If no time-zone is provided anywhere, falls back to the OS local TZ.
18    pub fn from_cli(cmd: &Command, cfg: &Config) -> Result<Self> {
19        let format = cmd.format.clone().unwrap_or_else(|| cfg.format.clone());
20
21        if format.trim().is_empty() {
22            return Err(user_input_error!(
23                MissingArgument,
24                "no output format specified"
25            ));
26        }
27
28        let tz_raw = cmd
29            .timezone
30            .clone()
31            .unwrap_or_else(|| cfg.timezone.clone())
32            .trim()
33            .to_owned();
34
35        let timezone: Tz = if tz_raw.is_empty() {
36            let local = iana_time_zone::get_timezone()
37                .map_err(|e| system_error!(Config, "failed to read local timezone: {}", e))?;
38            local.parse().map_err(|_| {
39                user_input_error!(UnsupportedTimezone, "invalid timezone ID: {}", local)
40            })?
41        } else {
42            tz_raw.parse().map_err(|_| {
43                user_input_error!(UnsupportedTimezone, "invalid timezone ID: {}", tz_raw)
44            })?
45        };
46
47        let now = cmd.now.map(|dt| dt.with_timezone(&timezone));
48
49        Ok(Self::new(cmd.input.clone(), format, timezone, now))
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use chrono::{DateTime, FixedOffset};
57    use pretty_assertions::assert_eq;
58    use std::collections::HashMap;
59
60    fn cmd(
61        input: &str,
62        format: Option<&str>,
63        timezone: Option<&str>,
64        now: Option<&str>,
65    ) -> cli::Command {
66        cli::Command {
67            input: input.to_string(),
68            format: format.map(|s| s.to_string()),
69            timezone: timezone.map(|s| s.to_string()),
70            now: now.map(|s| {
71                DateTime::parse_from_rfc3339(s)
72                    .unwrap()
73                    .with_timezone(&FixedOffset::east_opt(0).unwrap())
74            }),
75        }
76    }
77
78    fn cfg(format: &str, timezone: &str) -> config::Config {
79        config::Config {
80            format: format.to_string(),
81            timezone: timezone.to_string(),
82            formats: None,
83        }
84    }
85
86    fn tz_name(tz: &Tz) -> &'static str {
87        tz.name()
88    }
89
90    #[test]
91    fn cli_overrides_config_format() {
92        let cli = cmd("2025-01-01", Some("%Y"), None, None);
93        let cfg = cfg("%F", "UTC");
94
95        let app = App::from_cli(&cli, &cfg).unwrap();
96
97        assert_eq!(app.format, "%Y");
98        assert_eq!(tz_name(&app.timezone), "UTC");
99    }
100
101    #[test]
102    fn empty_format_is_error() {
103        let cli = cmd("2025-01-01", Some("   "), None, None);
104        let cfg = cfg("%F", "UTC");
105
106        let err = App::from_cli(&cli, &cfg).unwrap_err();
107
108        assert!(matches!(
109            err,
110            Error::UserInput(errors::UserInputError::MissingArgument { .. })
111        ));
112    }
113
114    #[test]
115    fn cli_overrides_config_timezone() {
116        let cli = cmd("2025-01-01", Some("%Y"), Some("Europe/London"), None);
117        let cfg = cfg("%Y", "UTC");
118
119        let app = App::from_cli(&cli, &cfg).unwrap();
120
121        assert_eq!(tz_name(&app.timezone), "Europe/London");
122    }
123
124    #[test]
125    fn invalid_timezone_returns_error() {
126        let cli = cmd("2025-01-01", Some("%Y"), Some("Mars/Olympus"), None);
127        let cfg = cfg("%Y", "UTC");
128
129        let err = App::from_cli(&cli, &cfg).unwrap_err();
130
131        assert!(matches!(
132            err,
133            Error::UserInput(errors::UserInputError::UnsupportedTimezone { .. })
134        ));
135    }
136
137    #[test]
138    fn preset_name_kept_in_app() {
139        let cli = cmd("2030-12-31", Some("br"), None, None);
140
141        let mut fmts = HashMap::new();
142        fmts.insert("br".into(), "%d/%m/%Y".into());
143        let cfg = config::Config {
144            format: "%F".into(),
145            timezone: "UTC".into(),
146            formats: Some(fmts),
147        };
148
149        let app = App::from_cli(&cli, &cfg).unwrap();
150        assert_eq!(app.format, "br");
151    }
152}