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_manager::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::GENERATOR_URI`] that takes a string
32    ///     value, and falling back to
33    ///     [`crate::constant::template_manager::GENERATOR_URI`] if not provided in cli
34    ///     args.
35    pub generator_uri: String,
36
37    /// The gitignore template lister service endpoint uri.
38    ///
39    /// * Optional value represented by the cli option
40    ///     [`crate::constant::cli_options::LISTER_URI`] that takes a string
41    ///     value, and falling back to
42    ///     [`crate::constant::template_manager::LISTER_URI`] if not provided in cli
43    ///     args.
44    pub lister_uri: String,
45
46    /// The boolean indicator of whether to display help infos or not.
47    ///
48    /// * Optional value represented by the cli option
49    ///     [`crate::constant::cli_options::HELP`], and falling back to `false`
50    ///     if not provided in cli args.
51    /// * Has precedence over version and author options if multiple are given
52    pub show_help: bool,
53
54    /// The boolean indicator of whether to display version infos or not.
55    ///
56    /// * Optional value represented by the cli option
57    ///     [`crate::constant::cli_options::VERSION`], and falling back to
58    ///     `false` if not provided in cli args.
59    /// * Has precedence over author option if multiple are given
60    pub show_version: bool,
61
62    /// The boolean indicator of whether to display author infos or not.
63    ///
64    /// * Optional value represented by the cli option
65    ///     [`crate::constant::cli_options::AUTHOR`], and falling back to
66    ///     `false` if not provided in cli args.
67    pub show_author: bool,
68
69    /// The boolean indicator of whether to display list of available templates
70    /// or not.
71    ///
72    /// * Optional value represented by the cli option
73    ///     [`crate::constant::cli_options::LIST`], and falling back to
74    ///     `false` if not provided in cli args.
75    pub show_list: bool,
76}
77
78impl Args {
79    /// Sets new value for `template_names` field.
80    ///
81    /// It needs to be called on struct instance and effectively mutates it.
82    ///
83    /// # Arguments
84    ///
85    /// * `template_names` - The new value to be assigned to `template_names`
86    ///     field.
87    ///
88    /// # Returns
89    ///
90    /// The mutated borrowed instance.
91    pub fn with_template_names(mut self, template_names: Vec<String>) -> Self {
92        self.template_names = template_names;
93        self
94    }
95
96    /// Sets new value for `server_url` field.
97    ///
98    /// It needs to be called on struct instance and effectively mutates it.
99    ///
100    /// # Arguments
101    ///
102    /// * `server_url` - The new value to be assigned to `server_url`
103    ///     field.
104    ///
105    /// # Returns
106    ///
107    /// The mutated borrowed instance.
108    pub fn with_server_url(mut self, server_url: &str) -> Self {
109        self.server_url = server_url.to_string();
110        self
111    }
112
113    /// Sets new value for `generator_uri` field.
114    ///
115    /// It needs to be called on struct instance and effectively mutates it.
116    ///
117    /// # Arguments
118    ///
119    /// * `generator_uri` - The new value to be assigned to
120    ///     `generator_uri` field.
121    ///
122    /// # Returns
123    ///
124    /// The mutated borrowed instance.
125    pub fn with_generator_uri(mut self, generator_uri: &str) -> Self {
126        self.generator_uri = generator_uri.to_string();
127        self
128    }
129
130    /// Sets new value for `lister_uri` field.
131    ///
132    /// It needs to be called on struct instance and effectively mutates it.
133    ///
134    /// # Arguments
135    ///
136    /// * `lister_uri` - The new value to be assigned to `lister_uri` field.
137    ///
138    /// # Returns
139    ///
140    /// The mutated borrowed instance.
141    pub fn with_lister_uri(mut self, lister_uri: &str) -> Self {
142        self.lister_uri = lister_uri.to_string();
143        self
144    }
145
146    /// Sets new value for `show_list` field.
147    ///
148    /// It needs to be called on struct instance and effectively mutates it.
149    ///
150    /// # Arguments
151    ///
152    /// * `show_list` - The new value to be assigned to `show_list`
153    ///     field.
154    ///
155    /// # Returns
156    ///
157    /// The mutated borrowed instance.
158    pub fn with_show_list(mut self, show_list: bool) -> Self {
159        self.show_list = show_list;
160        self
161    }
162}
163
164/// Cli args parser trait to parse CLI args and return them in an [`Args`].
165///
166/// The produced Args instance needs to comply with constraints of each
167/// one of its fields (see fields doc in [`Args`] for more infos).
168pub trait ArgsParser {
169    /// Parses given cli args and return them as an [`Args`] instance.
170    ///
171    /// * First CLI args should be the binary name
172    /// * Rely on [`ArgsParser::try_parse`] method but additionally wrap
173    ///     error handling logic
174    ///
175    /// # Arguments
176    ///
177    /// * `args` - The CLI args to be parsed. Typically retrieved from
178    ///     [`std::env::args_os`].
179    ///
180    /// # Returns
181    ///
182    /// An owned instance of [`Args`] containing parsing result of given args.
183    fn parse(&self, args: impl IntoIterator<Item = OsString>) -> Args;
184
185    /// Parses given cli args and return them as an [`Args`] instance if no
186    /// error or early exit occurred.
187    ///
188    /// * First CLI args should be the binary name
189    /// * Version, author and help options are considered as early program
190    ///     exit
191    /// * Returned Args complies with expected constraints (see fields doc
192    ///     in [`Args`] for more infos)
193    ///
194    /// # Arguments
195    ///
196    ///  * `args` - The CLI args to be parsed. Typically retrieved from
197    ///     [`std::env::args_os`].
198    ///
199    /// # Returns
200    ///
201    /// A result containing an owned instance of [`Args`] if successful parsing,
202    /// or a [`ProgramExit`] if any error or early exit occurred (e.g. version/
203    /// author/help infos printing, invalid cli args...)
204    fn try_parse(
205        &self,
206        args: impl IntoIterator<Item = OsString>,
207    ) -> Result<Args, ProgramExit>;
208}
209
210#[cfg(test)]
211mod tests {
212    use rstest::*;
213
214    use super::*;
215    use crate::helper::*;
216
217    mod default_args_parser {
218        use super::*;
219
220        mod try_parse {
221            use super::*;
222
223            mod success {
224                use super::*;
225                use crate::{ExitKind, constant};
226
227                #[rstest]
228                #[case("-V")]
229                #[case("--version")]
230                #[case("-V rust")]
231                #[case("rust -V")]
232                #[case("rust -s foo -V")]
233                #[case("rust -g bar -V")]
234                #[case("rust -i bar -V")]
235                #[case("-aV")]
236                #[case("rust -l -V")]
237                fn it_parses_version_cli_option(#[case] cli_args: &str) {
238                    let cli_args = parse_cli_args(cli_args);
239                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
240
241                    let actual_error = parsed_args.as_ref().err();
242                    let expected_error = ProgramExit {
243                        message: format!(
244                            "{} {}",
245                            env!("CARGO_PKG_NAME"),
246                            env!("CARGO_PKG_VERSION")
247                        ),
248                        exit_status: 0,
249                        styled_message: None,
250                        kind: ExitKind::VersionInfos,
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("-h")]
260                #[case("--help")]
261                #[case("-h rust")]
262                #[case("rust -h")]
263                #[case("rust -s foo -h")]
264                #[case("rust -g bar -h")]
265                #[case("rust -i bar -h")]
266                #[case("-aVh")]
267                #[case("rust -l -h")]
268                fn it_parses_help_cli_option(#[case] cli_args: &str) {
269                    let cli_args = parse_cli_args(cli_args);
270                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
271
272                    let actual_error = parsed_args.as_ref().err();
273                    let expected_error = ProgramExit {
274                        message: get_help_message(),
275                        exit_status: 0,
276                        styled_message: Some(get_ansi_help_message()),
277                        kind: ExitKind::HelpInfos,
278                    };
279                    let expected_error = Some(&expected_error);
280
281                    assert!(actual_error.is_some());
282                    assert_eq!(actual_error, expected_error);
283                }
284
285                #[rstest]
286                #[case("-a")]
287                #[case("--author")]
288                #[case("-a rust")]
289                #[case("rust -a")]
290                #[case("rust -s foo -a")]
291                #[case("rust -g bar -a")]
292                #[case("rust -i bar -a")]
293                #[case("rust -l -a")]
294                fn it_parses_author_cli_option_preemptively(
295                    #[case] cli_args: &str,
296                ) {
297                    let cli_args = parse_cli_args(cli_args);
298                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
299
300                    let actual_error = parsed_args.as_ref().err();
301                    let expected_error = ProgramExit {
302                        message: env!("CARGO_PKG_AUTHORS").to_string(),
303                        exit_status: 0,
304                        styled_message: None,
305                        kind: ExitKind::AuthorInfos,
306                    };
307                    let expected_error = Some(&expected_error);
308
309                    assert!(actual_error.is_some());
310                    assert_eq!(actual_error, expected_error);
311                }
312
313                #[rstest]
314                #[case("rust")]
315                #[case("rust python node")]
316                fn it_parses_pos_args_without_server_url_cli_option(
317                    #[case] cli_options: &str,
318                ) {
319                    let cli_args = parse_cli_args(cli_options);
320                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
321
322                    let actual_result = parsed_args.as_ref().ok();
323                    let expected_result = Args::default()
324                        .with_template_names(make_string_vec(cli_options))
325                        .with_server_url(constant::template_manager::BASE_URL)
326                        .with_generator_uri(
327                            constant::template_manager::GENERATOR_URI,
328                        )
329                        .with_lister_uri(
330                            constant::template_manager::LISTER_URI,
331                        );
332                    let expected_result = Some(&expected_result);
333
334                    println!("{:?}", parsed_args);
335                    assert!(actual_result.is_some());
336                    assert_eq!(actual_result, expected_result);
337                }
338
339                #[rstest]
340                #[case("rust -s https://test.com")]
341                #[case("rust --server-url https://test.com")]
342                fn it_parses_pos_args_with_server_url_cli_option(
343                    #[case] cli_args: &str,
344                ) {
345                    let cli_args = parse_cli_args(cli_args);
346                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
347
348                    let actual_result = parsed_args.as_ref().ok();
349                    let expected_result = Args::default()
350                        .with_template_names(make_string_vec("rust"))
351                        .with_server_url("https://test.com")
352                        .with_generator_uri(
353                            constant::template_manager::GENERATOR_URI,
354                        )
355                        .with_lister_uri(
356                            constant::template_manager::LISTER_URI,
357                        );
358                    let expected_result = Some(&expected_result);
359
360                    assert!(actual_result.is_some());
361                    assert_eq!(actual_result, expected_result);
362                }
363
364                #[rstest]
365                #[case("rust -g /test/api")]
366                #[case("rust --generator-uri /test/api")]
367                fn it_parses_pos_args_with_generator_uri_cli_option(
368                    #[case] cli_args: &str,
369                ) {
370                    let cli_args = parse_cli_args(cli_args);
371                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
372
373                    let actual_result = parsed_args.as_ref().ok();
374                    let expected_result = Args::default()
375                        .with_template_names(make_string_vec("rust"))
376                        .with_server_url(constant::template_manager::BASE_URL)
377                        .with_generator_uri("/test/api")
378                        .with_lister_uri(
379                            constant::template_manager::LISTER_URI,
380                        );
381                    let expected_result = Some(&expected_result);
382
383                    assert!(actual_result.is_some());
384                    assert_eq!(actual_result, expected_result);
385                }
386
387                #[rstest]
388                #[case("rust -i /test/api")]
389                #[case("rust --lister-uri /test/api")]
390                fn it_parses_pos_args_with_lister_uri_cli_option(
391                    #[case] cli_args: &str,
392                ) {
393                    let cli_args = parse_cli_args(cli_args);
394                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
395
396                    let actual_result = parsed_args.as_ref().ok();
397                    let expected_result = Args::default()
398                        .with_template_names(make_string_vec("rust"))
399                        .with_server_url(constant::template_manager::BASE_URL)
400                        .with_generator_uri(
401                            constant::template_manager::GENERATOR_URI,
402                        )
403                        .with_lister_uri("/test/api");
404                    let expected_result = Some(&expected_result);
405
406                    assert!(actual_result.is_some());
407                    assert_eq!(actual_result, expected_result);
408                }
409
410                #[rstest]
411                #[case("-l", "")]
412                #[case("--list", "")]
413                #[case("rust --list", "rust")]
414                #[case("rust python --list", "rust python")]
415                fn it_parses_list_cli_option(
416                    #[case] cli_args: &str,
417                    #[case] template_names: &str,
418                ) {
419                    let cli_args = parse_cli_args(cli_args);
420                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
421
422                    let actual_result = parsed_args.as_ref().ok();
423                    let expected_result = Args::default()
424                        .with_template_names(make_string_vec(template_names))
425                        .with_server_url(constant::template_manager::BASE_URL)
426                        .with_generator_uri(
427                            constant::template_manager::GENERATOR_URI,
428                        )
429                        .with_show_list(true)
430                        .with_lister_uri(
431                            constant::template_manager::LISTER_URI,
432                        );
433                    let expected_result = Some(&expected_result);
434
435                    assert!(actual_result.is_some());
436                    assert_eq!(actual_result, expected_result);
437                }
438            }
439
440            mod failure {
441                use super::*;
442                use crate::{ExitKind, constant};
443
444                #[test]
445                fn it_fails_parsing_when_no_pos_args_given() {
446                    let cli_args = parse_cli_args("");
447                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
448
449                    let actual_error = parsed_args.as_ref().err();
450                    let expected_error = ProgramExit {
451                        message: load_expectation_file_as_string(
452                            "no_pos_args_error",
453                        ),
454                        exit_status: constant::exit_status::GENERIC,
455
456                        styled_message: Some(load_expectation_file_as_string(
457                            "ansi_no_pos_args_error",
458                        )),
459                        kind: ExitKind::Error,
460                    };
461                    let expected_error = Some(&expected_error);
462
463                    assert!(actual_error.is_some());
464                    assert_eq!(actual_error, expected_error);
465                }
466
467                #[test]
468                fn it_fails_parsing_when_commas_in_pos_args() {
469                    let cli_args = parse_cli_args("python,java");
470                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
471
472                    let actual_error = parsed_args.as_ref().err();
473                    let expected_error = ProgramExit {
474                        message: load_expectation_file_as_string(
475                            "comma_pos_args_error",
476                        ),
477                        exit_status: constant::exit_status::GENERIC,
478
479                        styled_message: Some(load_expectation_file_as_string(
480                            "ansi_comma_pos_args_error",
481                        )),
482                        kind: ExitKind::Error,
483                    };
484                    let expected_error = Some(&expected_error);
485
486                    assert!(actual_error.is_some());
487                    assert_eq!(actual_error, expected_error);
488                }
489
490                #[test]
491                fn it_fails_parsing_when_whitespaces_in_pos_args() {
492                    let cli_args = vec![
493                        OsString::from(env!("CARGO_PKG_NAME")),
494                        OsString::from("r "),
495                    ];
496                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
497
498                    let actual_error = parsed_args.as_ref().err();
499                    let expected_error = ProgramExit {
500                        message: load_expectation_file_as_string(
501                            "whitespace_pos_args_error",
502                        ),
503                        exit_status: constant::exit_status::GENERIC,
504
505                        styled_message: Some(load_expectation_file_as_string(
506                            "ansi_whitespace_pos_args_error",
507                        )),
508                        kind: ExitKind::Error,
509                    };
510                    let expected_error = Some(&expected_error);
511
512                    assert!(actual_error.is_some());
513                    assert_eq!(actual_error, expected_error);
514                }
515
516                #[test]
517                fn it_fails_parsing_when_commas_and_whitespaces_in_pos_args() {
518                    let cli_args = vec![
519                        OsString::from(env!("CARGO_PKG_NAME")),
520                        OsString::from("r ,"),
521                    ];
522                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
523
524                    let actual_error = parsed_args.as_ref().err();
525                    let expected_error = ProgramExit {
526                        message: load_expectation_file_as_string(
527                            "comma_whitespace_pos_args_error",
528                        ),
529                        exit_status: constant::exit_status::GENERIC,
530
531                        styled_message: Some(load_expectation_file_as_string(
532                            "ansi_comma_whitespace_pos_args_error",
533                        )),
534                        kind: ExitKind::Error,
535                    };
536                    let expected_error = Some(&expected_error);
537
538                    assert!(actual_error.is_some());
539                    assert_eq!(actual_error, expected_error);
540                }
541
542                #[test]
543                fn it_fails_parsing_when_server_url_but_no_pos_args() {
544                    let cli_args = parse_cli_args("-s https://test.com");
545                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
546
547                    let actual_error = parsed_args.as_ref().err();
548                    let expected_error = ProgramExit {
549                        message: load_expectation_file_as_string(
550                            "server_url_no_pos_args_error",
551                        ),
552                        exit_status: constant::exit_status::GENERIC,
553
554                        styled_message: Some(load_expectation_file_as_string(
555                            "ansi_server_url_no_pos_args_error",
556                        )),
557                        kind: ExitKind::Error,
558                    };
559                    let expected_error = Some(&expected_error);
560
561                    assert!(actual_error.is_some());
562                    assert_eq!(actual_error, expected_error);
563                }
564
565                #[test]
566                fn it_fails_parsing_when_generator_uri_but_no_pos_args() {
567                    let cli_args = parse_cli_args("-g /test/api");
568                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
569
570                    let actual_error = parsed_args.as_ref().err();
571                    let expected_error = ProgramExit {
572                        message: load_expectation_file_as_string(
573                            "generator_uri_no_pos_args_error",
574                        ),
575                        exit_status: constant::exit_status::GENERIC,
576
577                        styled_message: Some(load_expectation_file_as_string(
578                            "ansi_generator_uri_no_pos_args_error",
579                        )),
580                        kind: ExitKind::Error,
581                    };
582                    let expected_error = Some(&expected_error);
583
584                    assert!(actual_error.is_some());
585                    assert_eq!(actual_error, expected_error);
586                }
587
588                #[test]
589                fn it_fails_parsing_when_lister_uri_but_no_pos_args() {
590                    let cli_args = parse_cli_args("-i /test/api");
591                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
592
593                    let actual_error = parsed_args.as_ref().err();
594                    let expected_error = ProgramExit {
595                        message: load_expectation_file_as_string(
596                            "lister_uri_no_pos_args_error",
597                        ),
598                        exit_status: constant::exit_status::GENERIC,
599
600                        styled_message: Some(load_expectation_file_as_string(
601                            "ansi_lister_uri_no_pos_args_error",
602                        )),
603                        kind: ExitKind::Error,
604                    };
605                    let expected_error = Some(&expected_error);
606
607                    assert!(actual_error.is_some());
608                    assert_eq!(actual_error, expected_error);
609                }
610
611                #[test]
612                fn it_fails_parsing_when_inexistent_cli_option() {
613                    let cli_args = parse_cli_args("-x");
614                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
615
616                    let actual_error = parsed_args.as_ref().err();
617                    let expected_error = ProgramExit {
618                        message: load_expectation_file_as_string(
619                            "unexpected_argument_error",
620                        ),
621                        exit_status: constant::exit_status::GENERIC,
622                        styled_message: Some(load_expectation_file_as_string(
623                            "ansi_unexpected_argument_error",
624                        )),
625                        kind: ExitKind::Error,
626                    };
627                    let expected_error = Some(&expected_error);
628
629                    assert!(actual_error.is_some());
630                    assert_eq!(actual_error, expected_error);
631                }
632            }
633        }
634
635        mod parse {
636            use super::*;
637
638            mod success {
639                use super::*;
640
641                #[test]
642                fn it_parses_given_cli_options() {
643                    let cli_args =
644                        parse_cli_args("rust python -s test -g foo -i bar");
645
646                    let actual_result = ClapArgsParser::new().parse(cli_args);
647                    let expected_result = Args::default()
648                        .with_template_names(make_string_vec("rust python"))
649                        .with_server_url("test")
650                        .with_generator_uri("foo")
651                        .with_lister_uri("bar");
652
653                    assert_eq!(actual_result, expected_result);
654                }
655            }
656        }
657    }
658}