gitignore_template_generator/parser/
impls.rs

1use std::{ffi::OsString, process::exit};
2
3use clap::{ArgMatches, Command};
4
5use super::{Args, ArgsParser, TimeoutUnit, command::build_clap_args};
6use crate::{
7    constant,
8    core::{ExitKind, ProgramExit},
9};
10
11/// Default implementation of args parser that parses CLI args using
12/// [`clap`].
13pub struct ClapArgsParser {
14    cli_parser: Command,
15}
16
17impl Default for ClapArgsParser {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl ClapArgsParser {
24    pub fn new() -> Self {
25        Self {
26            cli_parser: Command::new(env!("CARGO_PKG_NAME"))
27                .version(env!("CARGO_PKG_VERSION"))
28                .author(env!("CARGO_PKG_AUTHORS"))
29                .about(constant::parser_infos::ABOUT)
30                .help_template(include_str!("../../assets/help_template.txt"))
31                .disable_help_flag(true)
32                .disable_version_flag(true)
33                .args(build_clap_args()),
34        }
35    }
36
37    fn parse_global_options(&self, args: &Args) -> Option<ProgramExit> {
38        if args.show_help {
39            let rendered_help = self.cli_parser.clone().render_help();
40            Some(ProgramExit {
41                message: rendered_help.to_string().trim_end().to_string(),
42                exit_status: constant::exit_status::SUCCESS,
43                styled_message: Some(rendered_help.ansi().to_string().trim_end().to_string()),
44                kind: ExitKind::HelpInfos,
45            })
46        } else if args.show_version {
47            let message = match self.cli_parser.get_version() {
48                Some(version) => {
49                    format!("{} {version}", env!("CARGO_PKG_NAME"))
50                }
51                None => constant::error_messages::VERSION_INFOS_NOT_AVAILABLE.to_string(),
52            };
53
54            Some(ProgramExit {
55                message,
56                exit_status: constant::exit_status::SUCCESS,
57                styled_message: None,
58                kind: ExitKind::VersionInfos,
59            })
60        } else if args.show_author {
61            let message = String::from(match self.cli_parser.get_author() {
62                Some(author) => author,
63                None => constant::error_messages::AUTHOR_INFOS_NOT_AVAILABLE,
64            });
65
66            Some(ProgramExit {
67                message,
68                exit_status: constant::exit_status::SUCCESS,
69                styled_message: None,
70                kind: ExitKind::AuthorInfos,
71            })
72        } else {
73            None
74        }
75    }
76
77    fn map_arg_matches_to_struct(arg_matches: &ArgMatches) -> Args {
78        Args {
79            template_names: arg_matches
80                .get_many::<String>("TEMPLATE_NAMES")
81                .map(|vals| vals.cloned().collect())
82                .unwrap_or_default(),
83            server_url: arg_matches
84                .get_one::<String>("SERVER_URL")
85                .unwrap()
86                .to_string(),
87            generator_uri: arg_matches
88                .get_one::<String>("GENERATOR_URI")
89                .unwrap()
90                .to_string(),
91            lister_uri: arg_matches
92                .get_one::<String>("LISTER_URI")
93                .unwrap()
94                .to_string(),
95            timeout: match arg_matches.get_one::<u64>("TIMEOUT") {
96                Some(timeout) => *timeout,
97                None => constant::template_manager::TIMEOUT_INT,
98            },
99            timeout_unit: arg_matches
100                .get_one::<TimeoutUnit>("TIMEOUT_UNIT")
101                .unwrap()
102                .to_owned(),
103            check_template_names: arg_matches.get_flag("CHECK"),
104            show_help: arg_matches.get_flag("HELP"),
105            show_version: arg_matches.get_flag("VERSION"),
106            show_author: arg_matches.get_flag("AUTHOR"),
107            show_list: arg_matches.get_flag("LIST"),
108        }
109    }
110
111    fn print_error_message(error: &ProgramExit, message: &str) {
112        match error.kind {
113            ExitKind::VersionInfos | ExitKind::HelpInfos | ExitKind::AuthorInfos => {
114                println!("{message}")
115            }
116            ExitKind::Error => eprintln!("{message}"),
117        }
118    }
119}
120
121impl ArgsParser for ClapArgsParser {
122    /// Parses given cli args and perform basic error handling.
123    ///
124    /// * If the underlying [`ProgramExit`] contains a
125    ///   [`ProgramExit::styled_message`], it will be printed instead of
126    ///   [`ProgramExit::message`].
127    /// * Will exit using [`ProgramExit::exit_status`] if any
128    ///   [`ProgramExit`] received.
129    /// * Will print to stderr on error, to stdout on early exit (i.e. version,
130    ///   author, help options)
131    ///
132    /// See [`ArgsParser::parse`] for more infos.
133    fn parse(&self, args: impl IntoIterator<Item = OsString>) -> Args {
134        match self.try_parse(args) {
135            Ok(parsed_args) => parsed_args,
136            Err(error) => {
137                if let Some(value) = &error.styled_message {
138                    Self::print_error_message(&error, value);
139                } else {
140                    Self::print_error_message(&error, &error.message);
141                }
142
143                exit(error.exit_status);
144            }
145        }
146    }
147
148    fn try_parse(&self, args: impl IntoIterator<Item = OsString>) -> Result<Args, ProgramExit> {
149        match self.cli_parser.clone().try_get_matches_from(args) {
150            Ok(parsing_result) => {
151                let parsed_args = Self::map_arg_matches_to_struct(&parsing_result);
152                match self.parse_global_options(&parsed_args) {
153                    Some(error) => Err(error),
154                    None => Ok(parsed_args),
155                }
156            }
157            Err(error) => Err(ProgramExit {
158                message: format!("{}\nFor more information, try '--help'.", error.render()),
159                exit_status: error.exit_code(),
160                styled_message: Some(format!(
161                    "{}\nFor more information, try '\u{1b}[1m--help\u{1b}[0m'.",
162                    error.render().ansi()
163                )),
164                kind: ExitKind::Error,
165            }),
166        }
167    }
168}