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