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