rpn_cli/
config.rs

1use crate::error::{MyError, MyResult};
2#[cfg(debug_assertions)]
3use chrono::DateTime;
4use clap::{Arg, ArgAction, ArgMatches, Command};
5use clap_builder::parser::ValuesRef;
6use clap_complete::Shell;
7use std::fmt::Display;
8use std::process;
9use std::time::SystemTime;
10
11pub enum ShellKind {
12    Bash,
13    PowerShell,
14}
15
16pub struct Config {
17    pub now: Option<SystemTime>,
18    pub paths: Vec<String>,
19    pub values: Vec<String>,
20    pub import: Option<String>,
21    pub sum: bool,
22    pub hex: bool,
23    pub sep: bool,
24    pub dp: Option<u8>,
25    pub mock: bool,
26    pub completion: Option<ShellKind>,
27}
28
29const PATHS_SHORT: &'static str = "Read values from text files (batch mode)";
30const VALUES_SHORT: &'static str = "Read values from command line (batch mode)";
31const IMPORT_SHORT: &'static str = "Import values from text file";
32const SUM_SHORT: &'static str = "Sum values (batch mode)";
33const HEX_SHORT: &'static str = "Export hexadecimal (batch mode)";
34const SEP_SHORT: &'static str = "Export separator (batch mode)";
35const DP_SHORT: &'static str = "Export precision (batch mode)";
36#[cfg(debug_assertions)]
37const NOW_SHORT: &'static str = "Set current time for readme examples";
38#[cfg(debug_assertions)]
39const MOCK_SHORT: &'static str = "Mock interactive mode for readme examples";
40const COMPLETION_SHORT: &'static str = "Create completion script";
41
42const PATHS_LONG: &'static str = "\
43Read values and operations from text files or stdin in batch mode";
44const VALUES_LONG: &'static str = "\
45Read values and operations from command line in batch mode";
46const IMPORT_LONG: &'static str = "\
47Import values and operations from text file";
48const SUM_LONG: &'static str = "\
49Sum values if running in batch mode";
50const HEX_LONG: &'static str = "\
51Export hexadecimal if running in batch mode";
52const SEP_LONG: &'static str = "\
53Export separator if running in batch mode";
54const DP_LONG: &'static str = "\
55Export precision if running in batch mode";
56#[cfg(debug_assertions)]
57const NOW_LONG: &'static str = "\
58Set current time for readme examples, e.g. \"2025-03-31T00:00:00.000Z\"";
59#[cfg(debug_assertions)]
60const MOCK_LONG: &'static str = "\
61Mock interactive mode for readme examples; disables piped output";
62const COMPLETION_LONG: &'static str = "\
63Create completion script:
64Use \"--completion bash\" to create script for Bash
65Use \"--completion ps\" to create script for PowerShell";
66
67impl Config {
68    pub fn new(name: String, args: Vec<String>) -> MyResult<Self> {
69        let mut command = Self::create_command(name.clone());
70        let matches = Self::create_matches(&mut command, args)?;
71        let config = Self::create_config(&mut command, matches)?;
72        if let Some(completion) = config.completion {
73            Self::create_completion(&mut command, name, completion);
74            process::exit(1);
75        }
76        Ok(config)
77    }
78
79    fn create_command(name: String) -> Command {
80        let mut index = 0;
81        let command = Command::new(name)
82            .version(clap::crate_version!())
83            .about(clap::crate_description!())
84            .author(clap::crate_authors!());
85        let command = command.arg(Self::create_arg("paths", &mut index)
86            .action(ArgAction::Append)
87            .help(PATHS_SHORT)
88            .long_help(PATHS_LONG));
89        let command = command.arg(Self::create_arg("command", &mut index)
90            .long("command")
91            .short('c')
92            .value_name("VALUE")
93            .action(ArgAction::Append)
94            .num_args(1..)
95            .allow_negative_numbers(true)
96            .help(VALUES_SHORT)
97            .long_help(VALUES_LONG));
98        let command = command.arg(Self::create_arg("import", &mut index)
99            .long("import")
100            .value_name("FILE")
101            .action(ArgAction::Set)
102            .help(IMPORT_SHORT)
103            .long_help(IMPORT_LONG));
104        let command = command.arg(Self::create_arg("sum", &mut index)
105            .long("sum")
106            .action(ArgAction::SetTrue)
107            .help(SUM_SHORT)
108            .long_help(SUM_LONG));
109        let command = command.arg(Self::create_arg("hex", &mut index)
110            .long("hex")
111            .action(ArgAction::SetTrue)
112            .help(HEX_SHORT)
113            .long_help(HEX_LONG));
114        let command = command.arg(Self::create_arg("sep", &mut index)
115            .long("sep")
116            .action(ArgAction::SetTrue)
117            .help(SEP_SHORT)
118            .long_help(SEP_LONG));
119        let command = command.arg(Self::create_arg("dp", &mut index)
120            .long("dp")
121            .value_name("PLACES")
122            .action(ArgAction::Set)
123            .help(DP_SHORT)
124            .long_help(DP_LONG));
125        #[cfg(debug_assertions)]
126        let command = command.arg(Self::create_arg("now", &mut index)
127            .long("now")
128            .value_name("TIME")
129            .action(ArgAction::Set)
130            .help(NOW_SHORT)
131            .long_help(NOW_LONG));
132        #[cfg(debug_assertions)]
133        let command = command.arg(Self::create_arg("mock", &mut index)
134            .long("mock")
135            .action(ArgAction::SetTrue)
136            .help(MOCK_SHORT)
137            .long_help(MOCK_LONG));
138        let command = command.arg(Self::create_arg("completion", &mut index)
139            .long("completion")
140            .value_name("SHELL")
141            .action(ArgAction::Set)
142            .value_parser(["bash", "ps"])
143            .hide_possible_values(true)
144            .help(COMPLETION_SHORT)
145            .long_help(COMPLETION_LONG));
146        command
147    }
148
149    fn create_arg(name: &'static str, index: &mut usize) -> Arg {
150        *index += 1;
151        Arg::new(name).display_order(*index)
152    }
153
154    fn create_matches(command: &mut Command, args: Vec<String>) -> clap::error::Result<ArgMatches> {
155        match command.try_get_matches_from_mut(args) {
156            Ok(found) => Ok(found),
157            Err(error) => match error.kind() {
158                clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
159                    let error = error.to_string();
160                    let error = error.trim_end();
161                    eprintln!("{error}");
162                    process::exit(1);
163                },
164                _ => Err(error),
165            }
166        }
167    }
168
169    fn create_config(command: &mut Command, matches: ArgMatches) -> MyResult<Self> {
170        #[cfg(debug_assertions)]
171        let now = Self::parse_time(matches.get_one("now"))?;
172        #[cfg(not(debug_assertions))]
173        let now = None;
174        let paths = Self::parse_values(matches.get_many("paths"));
175        let values = Self::parse_values(matches.get_many("command"));
176        let import = Self::parse_value(matches.get_one("import"));
177        let sum = matches.get_flag("sum");
178        let hex = matches.get_flag("hex");
179        let sep = matches.get_flag("sep");
180        let dp = Self::parse_integer(matches.get_one("dp"))?;
181        #[cfg(debug_assertions)]
182        let mock = matches.get_flag("mock");
183        #[cfg(not(debug_assertions))]
184        let mock = false;
185        let completion = Self::parse_completion(command, matches.get_one("completion"))?;
186        let config = Self {
187            now,
188            paths,
189            values,
190            import,
191            sum,
192            hex,
193            sep,
194            dp,
195            mock,
196            completion,
197        };
198        Ok(config)
199    }
200
201    #[cfg(debug_assertions)]
202    fn parse_time(value: Option<&String>) -> MyResult<Option<SystemTime>> {
203        if let Some(value) = value {
204            let time = DateTime::parse_from_rfc3339(value)?;
205            let time = SystemTime::from(time.to_utc());
206            Ok(Some(time))
207        } else {
208            Ok(None)
209        }
210    }
211
212    fn parse_value(value: Option<&String>) -> Option<String> {
213        value.map(String::to_string)
214    }
215
216    fn parse_values(values: Option<ValuesRef<String>>) -> Vec<String> {
217        values.unwrap_or_default().map(String::to_string).collect()
218    }
219
220    fn parse_integer(value: Option<&String>) -> MyResult<Option<u8>> {
221        if let Some(value) = value {
222            let value = value.parse()?;
223            Ok(Some(value))
224        } else {
225            Ok(None)
226        }
227    }
228
229    fn parse_completion(command: &mut Command, value: Option<&String>) -> MyResult<Option<ShellKind>> {
230        let value = value.map(String::as_ref);
231        match value {
232            Some("bash") => Ok(Some(ShellKind::Bash)),
233            Some("ps") => Ok(Some(ShellKind::PowerShell)),
234            Some(value) => Err(Self::make_error(command, "completion", value)),
235            None => Ok(None),
236        }
237    }
238
239    fn create_completion(command: &mut Command, name: String, value: ShellKind) {
240        let mut stdout = std::io::stdout();
241        let value = match value {
242            ShellKind::Bash => Shell::Bash,
243            ShellKind::PowerShell => Shell::PowerShell,
244        };
245        clap_complete::generate(value, command, name, &mut stdout);
246    }
247
248    fn make_error<T: Display>(command: &mut Command, option: &str, value: T) -> MyError {
249        let message = format!("Invalid {option} option: {value}");
250        let error = command.error(clap::error::ErrorKind::ValueValidation, message);
251        MyError::Clap(error)
252    }
253}
254
255impl Default for Config {
256    fn default() -> Self {
257        Self {
258            now: None,
259            paths: Vec::new(),
260            values: Vec::new(),
261            import: None,
262            sum: false,
263            hex: false,
264            sep: false,
265            dp: None,
266            mock: false,
267            completion: None,
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use crate::config::Config;
275    use std::fmt::Display;
276
277    #[test]
278    fn test_paths_are_handled() {
279        let expected: Vec<String> = vec![];
280        let args = vec!["rpn"];
281        let config = create_config(args);
282        assert_eq!(expected, config.paths);
283
284        let expected = vec!["file1"];
285        let args = vec!["rpn", "file1"];
286        let config = create_config(args);
287        assert_eq!(expected, config.paths);
288
289        let expected = vec!["file1", "file2"];
290        let args = vec!["rpn", "file1", "file2"];
291        let config = create_config(args);
292        assert_eq!(expected, config.paths);
293    }
294
295    #[test]
296    fn test_values_are_handled() {
297        let expected: Vec<String> = vec![];
298        let args = vec!["rpn"];
299        let config = create_config(args);
300        assert_eq!(expected, config.values);
301
302        let expected = vec!["1"];
303        let args = vec!["rpn", "-c", "1"];
304        let config = create_config(args);
305        assert_eq!(expected, config.values);
306
307        let expected = vec!["1", "-2", "add"];
308        let args = vec!["rpn", "--command", "1", "-2", "add"];
309        let config = create_config(args);
310        assert_eq!(expected, config.values);
311    }
312
313    #[test]
314    fn test_import_is_handled() {
315        let args = vec!["rpn"];
316        let config = create_config(args);
317        assert_eq!(None, config.import);
318
319        let args = vec!["rpn", "--import", "file"];
320        let config = create_config(args);
321        assert_eq!(Some(String::from("file")), config.import);
322    }
323
324    #[test]
325    fn test_sum_is_handled() {
326        let args = vec!["rpn"];
327        let config = create_config(args);
328        assert_eq!(false, config.sum);
329
330        let args = vec!["rpn", "--sum"];
331        let config = create_config(args);
332        assert_eq!(true, config.sum);
333    }
334
335    #[test]
336    fn test_hex_is_handled() {
337        let args = vec!["rpn"];
338        let config = create_config(args);
339        assert_eq!(false, config.hex);
340
341        let args = vec!["rpn", "--hex"];
342        let config = create_config(args);
343        assert_eq!(true, config.hex);
344    }
345
346    #[test]
347    fn test_sep_is_handled() {
348        let args = vec!["rpn"];
349        let config = create_config(args);
350        assert_eq!(false, config.sep);
351
352        let args = vec!["rpn", "--sep"];
353        let config = create_config(args);
354        assert_eq!(true, config.sep);
355    }
356
357    #[test]
358    fn test_dp_is_handled() {
359        let args = vec!["rpn"];
360        let config = create_config(args);
361        assert_eq!(None, config.dp);
362
363        let args = vec!["rpn", "--dp", "6"];
364        let config = create_config(args);
365        assert_eq!(Some(6), config.dp);
366    }
367
368    fn create_config(args: Vec<&str>) -> Config {
369        let mut command = Config::create_command(String::from("rpn"));
370        let args = args.into_iter().map(String::from).collect();
371        let matches = Config::create_matches(&mut command, args).unwrap_or_else(handle_error);
372        Config::create_config(&mut command, matches).unwrap_or_else(handle_error)
373    }
374
375    fn handle_error<T, E: Display>(err: E)-> T {
376        panic!("{}", err);
377    }
378}