gitignore_template_generator/parser/
api.rs

1use std::ffi::OsString;
2
3use crate::ProgramExit;
4pub use crate::parser::impls::ClapArgsParser;
5
6/// Struct to gather cli args parsing result.
7///
8/// Used by [`crate::parser::ArgsParser`] implementations to store
9/// parsing result.
10#[derive(Debug, PartialEq, Default)]
11pub struct Args {
12    /// A non-empty list of gitignore template names.
13    ///
14    /// * Represented by the provided positional arguments, and required
15    ///     unless any of `author`, `version` or `help` options are given.
16    /// * This field does not allow commas in any of its field.
17    pub template_names: Vec<String>,
18
19    /// The gitignore template generator service url.
20    ///
21    /// * Optional value represented by the cli option
22    ///     [`crate::constant::cli_options::SERVER_URL`] that takes a string
23    ///     value, and falling back to
24    ///     [`crate::constant::template_generator::BASE_URL`] if not provided
25    ///     in cli args.
26    pub server_url: String,
27
28    /// The gitignore template generator service endpoint uri.
29    ///
30    /// * Optional value represented by the cli option
31    ///     [`crate::constant::cli_options::ENDPOINT_URI`] that takes a string
32    ///     value, and falling back to
33    ///     [`crate::constant::template_generator::URI`] if not provided in cli
34    ///     args.
35    pub endpoint_uri: String,
36
37    /// The boolean indicator of whether to display help infos or not.
38    ///
39    /// * Optional value represented by the cli option
40    ///     [`crate::constant::cli_options::HELP`], and falling back to `false`
41    ///     if not provided in cli args.
42    /// * Has precedence over version and author options if multiple are given
43    pub show_help: bool,
44
45    /// The boolean indicator of whether to display version infos or not.
46    ///
47    /// * Optional value represented by the cli option
48    ///     [`crate::constant::cli_options::VERSION`], and falling back to
49    ///     `false` if not provided in cli args.
50    /// * Has precedence over author option if multiple are given
51    pub show_version: bool,
52
53    /// The boolean indicator of whether to display author infos or not.
54    ///
55    /// * Optional value represented by the cli option
56    ///     [`crate::constant::cli_options::AUTHOR`], and falling back to
57    ///     `false` if not provided in cli args.
58    pub show_author: bool,
59}
60
61impl Args {
62    /// Sets new value for `template_names` field.
63    ///
64    /// It needs to be called on struct instance and effectively mutates it.
65    ///
66    /// # Arguments
67    ///
68    /// * `template_names` - The new value to be assigned to `template_names`
69    ///     field.
70    ///
71    /// # Returns
72    ///
73    /// The mutated borrowed instance.
74    pub fn with_template_names(mut self, template_names: Vec<String>) -> Self {
75        self.template_names = template_names;
76        self
77    }
78
79    /// Sets new value for `server_url` field.
80    ///
81    /// It needs to be called on struct instance and effectively mutates it.
82    ///
83    /// # Arguments
84    ///
85    /// * `server_url` - The new value to be assigned to `server_url`
86    ///     field.
87    ///
88    /// # Returns
89    ///
90    /// The mutated borrowed instance.
91    pub fn with_server_url(mut self, server_url: &str) -> Self {
92        self.server_url = server_url.to_string();
93        self
94    }
95
96    /// Sets new value for `endpoint_uri` field.
97    ///
98    /// It needs to be called on struct instance and effectively mutates it.
99    ///
100    /// # Arguments
101    ///
102    /// * `endpoint_uri` - The new value to be assigned to `endpoint_uri`
103    ///     field.
104    ///
105    /// # Returns
106    ///
107    /// The mutated borrowed instance.
108    pub fn with_endpoint_uri(mut self, endpoint_uri: &str) -> Self {
109        self.endpoint_uri = endpoint_uri.to_string();
110        self
111    }
112}
113
114/// Cli args parser trait to parse CLI args and return them in an [`Args`].
115///
116/// The produced Args instance needs to comply with constraints of each
117/// one of its fields (see fields doc in [`Args`] for more infos).
118pub trait ArgsParser {
119    /// Parses given cli args and return them as an [`Args`] instance.
120    ///
121    /// * First CLI args should be the binary name
122    /// * Rely on [`ArgsParser::try_parse`] method but additionally wrap
123    ///     error handling logic
124    ///
125    /// # Arguments
126    ///
127    /// * `args` - The CLI args to be parsed. Typically retrieved from
128    ///     [`std::env::args_os`].
129    ///
130    /// # Returns
131    ///
132    /// An owned instance of [`Args`] containing parsing result of given args.
133    fn parse(&self, args: impl IntoIterator<Item = OsString>) -> Args;
134
135    /// Parses given cli args and return them as an [`Args`] instance if no
136    /// error or early exit occurred.
137    ///
138    /// * First CLI args should be the binary name
139    /// * Version, author and help options are considered as early program
140    ///     exit
141    /// * Returned Args complies with expected constraints (see fields doc
142    ///     in [`Args`] for more infos)
143    ///
144    /// # Arguments
145    ///
146    ///  * `args` - The CLI args to be parsed. Typically retrieved from
147    ///     [`std::env::args_os`].
148    ///
149    /// # Returns
150    ///
151    /// A result containing an owned instance of [`Args`] if successful parsing,
152    /// or a [`ProgramExit`] if any error or early exit occurred (e.g. version/
153    /// author/help infos printing, invalid cli args...)
154    fn try_parse(
155        &self,
156        args: impl IntoIterator<Item = OsString>,
157    ) -> Result<Args, ProgramExit>;
158}
159
160#[cfg(test)]
161mod tests {
162    use rstest::*;
163
164    use super::*;
165    use crate::helper::*;
166
167    mod default_args_parser {
168        use super::*;
169
170        mod try_parse {
171            use super::*;
172
173            mod success {
174                use crate::{ExitKind, constant};
175
176                use super::*;
177
178                #[rstest]
179                #[case("-V")]
180                #[case("--version")]
181                #[case("-V rust")]
182                #[case("rust -V")]
183                #[case("rust -s foo -V")]
184                #[case("rust -e bar -V")]
185                #[case("-aV")]
186                fn it_parses_version_cli_option(#[case] cli_args: &str) {
187                    let cli_args = parse_cli_args(cli_args);
188                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
189
190                    let actual_error = parsed_args.as_ref().err();
191                    let expected_error = ProgramExit {
192                        message: format!(
193                            "{} {}",
194                            env!("CARGO_PKG_NAME"),
195                            env!("CARGO_PKG_VERSION")
196                        ),
197                        exit_status: 0,
198                        styled_message: None,
199                        kind: ExitKind::VersionInfos,
200                    };
201                    let expected_error = Some(&expected_error);
202
203                    assert!(actual_error.is_some());
204                    assert_eq!(actual_error, expected_error);
205                }
206
207                #[rstest]
208                #[case("-h")]
209                #[case("--help")]
210                #[case("-h rust")]
211                #[case("rust -h")]
212                #[case("rust -s foo -h")]
213                #[case("rust -e bar -h")]
214                #[case("-aVh")]
215                fn it_parses_help_cli_option(#[case] cli_args: &str) {
216                    let cli_args = parse_cli_args(cli_args);
217                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
218
219                    let actual_error = parsed_args.as_ref().err();
220                    let expected_error = ProgramExit {
221                        message: get_help_message(),
222                        exit_status: 0,
223                        styled_message: Some(get_ansi_help_message()),
224                        kind: ExitKind::HelpInfos,
225                    };
226                    let expected_error = Some(&expected_error);
227
228                    assert!(actual_error.is_some());
229                    assert_eq!(actual_error, expected_error);
230                }
231
232                #[rstest]
233                #[case("-a")]
234                #[case("--author")]
235                #[case("-a rust")]
236                #[case("rust -a")]
237                #[case("rust -s foo -a")]
238                #[case("rust -e bar -a")]
239                fn it_parses_author_cli_option_preemptively(
240                    #[case] cli_args: &str,
241                ) {
242                    let cli_args = parse_cli_args(cli_args);
243                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
244
245                    let actual_error = parsed_args.as_ref().err();
246                    let expected_error = ProgramExit {
247                        message: env!("CARGO_PKG_AUTHORS").to_string(),
248                        exit_status: 0,
249                        styled_message: None,
250                        kind: ExitKind::AuthorInfos,
251                    };
252                    let expected_error = Some(&expected_error);
253
254                    assert!(actual_error.is_some());
255                    assert_eq!(actual_error, expected_error);
256                }
257
258                #[rstest]
259                #[case("rust")]
260                #[case("rust python node")]
261                fn it_parses_pos_args_without_server_url_cli_option(
262                    #[case] cli_options: &str,
263                ) {
264                    let cli_args = parse_cli_args(cli_options);
265                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
266
267                    let actual_result = parsed_args.as_ref().ok();
268                    let expected_result = Args::default()
269                        .with_template_names(make_string_vec(cli_options))
270                        .with_server_url(constant::template_generator::BASE_URL)
271                        .with_endpoint_uri(constant::template_generator::URI);
272                    let expected_result = Some(&expected_result);
273
274                    println!("{:?}", parsed_args);
275                    assert!(actual_result.is_some());
276                    assert_eq!(actual_result, expected_result);
277                }
278
279                #[rstest]
280                #[case("rust -s https://test.com")]
281                #[case("rust --server-url https://test.com")]
282                fn it_parses_pos_args_with_server_url_cli_option(
283                    #[case] cli_args: &str,
284                ) {
285                    let cli_args = parse_cli_args(cli_args);
286                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
287
288                    let actual_result = parsed_args.as_ref().ok();
289                    let expected_result = Args::default()
290                        .with_template_names(make_string_vec("rust"))
291                        .with_server_url("https://test.com")
292                        .with_endpoint_uri(constant::template_generator::URI);
293                    let expected_result = Some(&expected_result);
294
295                    assert!(actual_result.is_some());
296                    assert_eq!(actual_result, expected_result);
297                }
298
299                #[rstest]
300                #[case("rust -e /test/api")]
301                #[case("rust --endpoint-uri /test/api")]
302                fn it_parses_pos_args_with_endpoint_uri_cli_option(
303                    #[case] cli_args: &str,
304                ) {
305                    let cli_args = parse_cli_args(cli_args);
306                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
307
308                    let actual_result = parsed_args.as_ref().ok();
309                    let expected_result = Args::default()
310                        .with_template_names(make_string_vec("rust"))
311                        .with_server_url(constant::template_generator::BASE_URL)
312                        .with_endpoint_uri("/test/api");
313                    let expected_result = Some(&expected_result);
314
315                    assert!(actual_result.is_some());
316                    assert_eq!(actual_result, expected_result);
317                }
318            }
319
320            mod failure {
321                use crate::{ExitKind, constant};
322
323                use super::*;
324
325                #[test]
326                fn it_fails_parsing_when_no_pos_args_given() {
327                    let cli_args = parse_cli_args("");
328                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
329
330                    let actual_error = parsed_args.as_ref().err();
331                    let expected_error = ProgramExit {
332                        message: load_expectation_file_as_string(
333                            "no_pos_args_error",
334                        ),
335                        exit_status: constant::exit_status::GENERIC,
336
337                        styled_message: Some(load_expectation_file_as_string(
338                            "ansi_no_pos_args_error",
339                        )),
340                        kind: ExitKind::Error,
341                    };
342                    let expected_error = Some(&expected_error);
343
344                    assert!(actual_error.is_some());
345                    assert_eq!(actual_error, expected_error);
346                }
347
348                #[test]
349                fn it_fails_parsing_when_commas_in_pos_args() {
350                    let cli_args = parse_cli_args("python,java");
351                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
352
353                    let actual_error = parsed_args.as_ref().err();
354                    let expected_error = ProgramExit {
355                        message: load_expectation_file_as_string(
356                            "comma_pos_args_error",
357                        ),
358                        exit_status: constant::exit_status::GENERIC,
359
360                        styled_message: Some(load_expectation_file_as_string(
361                            "ansi_comma_pos_args_error",
362                        )),
363                        kind: ExitKind::Error,
364                    };
365                    let expected_error = Some(&expected_error);
366
367                    assert!(actual_error.is_some());
368                    assert_eq!(actual_error, expected_error);
369                }
370
371                #[test]
372                fn it_fails_parsing_when_server_url_but_no_pos_args() {
373                    let cli_args = parse_cli_args("-s https://test.com");
374                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
375
376                    let actual_error = parsed_args.as_ref().err();
377                    let expected_error = ProgramExit {
378                        message: load_expectation_file_as_string(
379                            "server_url_no_pos_args_error",
380                        ),
381                        exit_status: constant::exit_status::GENERIC,
382
383                        styled_message: Some(load_expectation_file_as_string(
384                            "ansi_server_url_no_pos_args_error",
385                        )),
386                        kind: ExitKind::Error,
387                    };
388                    let expected_error = Some(&expected_error);
389
390                    assert!(actual_error.is_some());
391                    assert_eq!(actual_error, expected_error);
392                }
393
394                #[test]
395                fn it_fails_parsing_when_endpoint_uri_but_no_pos_args() {
396                    let cli_args = parse_cli_args("-e /test/api");
397                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
398
399                    let actual_error = parsed_args.as_ref().err();
400                    let expected_error = ProgramExit {
401                        message: load_expectation_file_as_string(
402                            "endpoint_uri_no_pos_args_error",
403                        ),
404                        exit_status: constant::exit_status::GENERIC,
405
406                        styled_message: Some(load_expectation_file_as_string(
407                            "ansi_endpoint_uri_no_pos_args_error",
408                        )),
409                        kind: ExitKind::Error,
410                    };
411                    let expected_error = Some(&expected_error);
412
413                    assert!(actual_error.is_some());
414                    assert_eq!(actual_error, expected_error);
415                }
416
417                #[test]
418                fn it_fails_parsing_when_inexistent_cli_option() {
419                    let cli_args = parse_cli_args("-x");
420                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
421
422                    let actual_error = parsed_args.as_ref().err();
423                    let expected_error = ProgramExit {
424                        message: load_expectation_file_as_string(
425                            "unexpected_argument_error",
426                        ),
427                        exit_status: constant::exit_status::GENERIC,
428                        styled_message: Some(load_expectation_file_as_string(
429                            "ansi_unexpected_argument_error",
430                        )),
431                        kind: ExitKind::Error,
432                    };
433                    let expected_error = Some(&expected_error);
434
435                    assert!(actual_error.is_some());
436                    assert_eq!(actual_error, expected_error);
437                }
438            }
439        }
440    }
441}