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}