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 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}