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::ENDPOINT_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 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    /// The boolean indicator of whether to display list of available templates
61    /// or not.
62    ///
63    /// * Optional value represented by the cli option
64    ///     [`crate::constant::cli_options::LIST`], and falling back to
65    ///     `false` if not provided in cli args.
66    pub show_list: bool,
67}
68
69impl Args {
70    /// Sets new value for `template_names` field.
71    ///
72    /// It needs to be called on struct instance and effectively mutates it.
73    ///
74    /// # Arguments
75    ///
76    /// * `template_names` - The new value to be assigned to `template_names`
77    ///     field.
78    ///
79    /// # Returns
80    ///
81    /// The mutated borrowed instance.
82    pub fn with_template_names(mut self, template_names: Vec<String>) -> Self {
83        self.template_names = template_names;
84        self
85    }
86
87    /// Sets new value for `server_url` field.
88    ///
89    /// It needs to be called on struct instance and effectively mutates it.
90    ///
91    /// # Arguments
92    ///
93    /// * `server_url` - The new value to be assigned to `server_url`
94    ///     field.
95    ///
96    /// # Returns
97    ///
98    /// The mutated borrowed instance.
99    pub fn with_server_url(mut self, server_url: &str) -> Self {
100        self.server_url = server_url.to_string();
101        self
102    }
103
104    /// Sets new value for `endpoint_uri` field.
105    ///
106    /// It needs to be called on struct instance and effectively mutates it.
107    ///
108    /// # Arguments
109    ///
110    /// * `endpoint_uri` - The new value to be assigned to `endpoint_uri`
111    ///     field.
112    ///
113    /// # Returns
114    ///
115    /// The mutated borrowed instance.
116    pub fn with_endpoint_uri(mut self, endpoint_uri: &str) -> Self {
117        self.endpoint_uri = endpoint_uri.to_string();
118        self
119    }
120
121    /// Sets new value for `show_list` field.
122    ///
123    /// It needs to be called on struct instance and effectively mutates it.
124    ///
125    /// # Arguments
126    ///
127    /// * `show_list` - The new value to be assigned to `show_list`
128    ///     field.
129    ///
130    /// # Returns
131    ///
132    /// The mutated borrowed instance.
133    pub fn with_show_list(mut self, show_list: bool) -> Self {
134        self.show_list = show_list;
135        self
136    }
137}
138
139/// Cli args parser trait to parse CLI args and return them in an [`Args`].
140///
141/// The produced Args instance needs to comply with constraints of each
142/// one of its fields (see fields doc in [`Args`] for more infos).
143pub trait ArgsParser {
144    /// Parses given cli args and return them as an [`Args`] instance.
145    ///
146    /// * First CLI args should be the binary name
147    /// * Rely on [`ArgsParser::try_parse`] method but additionally wrap
148    ///     error handling logic
149    ///
150    /// # Arguments
151    ///
152    /// * `args` - The CLI args to be parsed. Typically retrieved from
153    ///     [`std::env::args_os`].
154    ///
155    /// # Returns
156    ///
157    /// An owned instance of [`Args`] containing parsing result of given args.
158    fn parse(&self, args: impl IntoIterator<Item = OsString>) -> Args;
159
160    /// Parses given cli args and return them as an [`Args`] instance if no
161    /// error or early exit occurred.
162    ///
163    /// * First CLI args should be the binary name
164    /// * Version, author and help options are considered as early program
165    ///     exit
166    /// * Returned Args complies with expected constraints (see fields doc
167    ///     in [`Args`] for more infos)
168    ///
169    /// # Arguments
170    ///
171    ///  * `args` - The CLI args to be parsed. Typically retrieved from
172    ///     [`std::env::args_os`].
173    ///
174    /// # Returns
175    ///
176    /// A result containing an owned instance of [`Args`] if successful parsing,
177    /// or a [`ProgramExit`] if any error or early exit occurred (e.g. version/
178    /// author/help infos printing, invalid cli args...)
179    fn try_parse(
180        &self,
181        args: impl IntoIterator<Item = OsString>,
182    ) -> Result<Args, ProgramExit>;
183}
184
185#[cfg(test)]
186mod tests {
187    use rstest::*;
188
189    use super::*;
190    use crate::helper::*;
191
192    mod default_args_parser {
193        use super::*;
194
195        mod try_parse {
196            use super::*;
197
198            mod success {
199                use super::*;
200                use crate::{ExitKind, constant};
201
202                #[rstest]
203                #[case("-V")]
204                #[case("--version")]
205                #[case("-V rust")]
206                #[case("rust -V")]
207                #[case("rust -s foo -V")]
208                #[case("rust -e bar -V")]
209                #[case("-aV")]
210                #[case("rust -l -V")]
211                fn it_parses_version_cli_option(#[case] cli_args: &str) {
212                    let cli_args = parse_cli_args(cli_args);
213                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
214
215                    let actual_error = parsed_args.as_ref().err();
216                    let expected_error = ProgramExit {
217                        message: format!(
218                            "{} {}",
219                            env!("CARGO_PKG_NAME"),
220                            env!("CARGO_PKG_VERSION")
221                        ),
222                        exit_status: 0,
223                        styled_message: None,
224                        kind: ExitKind::VersionInfos,
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("-h")]
234                #[case("--help")]
235                #[case("-h rust")]
236                #[case("rust -h")]
237                #[case("rust -s foo -h")]
238                #[case("rust -e bar -h")]
239                #[case("-aVh")]
240                #[case("rust -l -h")]
241                fn it_parses_help_cli_option(#[case] cli_args: &str) {
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: get_help_message(),
248                        exit_status: 0,
249                        styled_message: Some(get_ansi_help_message()),
250                        kind: ExitKind::HelpInfos,
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("-a")]
260                #[case("--author")]
261                #[case("-a rust")]
262                #[case("rust -a")]
263                #[case("rust -s foo -a")]
264                #[case("rust -e bar -a")]
265                #[case("rust -l -a")]
266                fn it_parses_author_cli_option_preemptively(
267                    #[case] cli_args: &str,
268                ) {
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: env!("CARGO_PKG_AUTHORS").to_string(),
275                        exit_status: 0,
276                        styled_message: None,
277                        kind: ExitKind::AuthorInfos,
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("rust")]
287                #[case("rust python node")]
288                fn it_parses_pos_args_without_server_url_cli_option(
289                    #[case] cli_options: &str,
290                ) {
291                    let cli_args = parse_cli_args(cli_options);
292                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
293
294                    let actual_result = parsed_args.as_ref().ok();
295                    let expected_result = Args::default()
296                        .with_template_names(make_string_vec(cli_options))
297                        .with_server_url(constant::template_manager::BASE_URL)
298                        .with_endpoint_uri(
299                            constant::template_manager::GENERATOR_URI,
300                        );
301                    let expected_result = Some(&expected_result);
302
303                    println!("{:?}", parsed_args);
304                    assert!(actual_result.is_some());
305                    assert_eq!(actual_result, expected_result);
306                }
307
308                #[rstest]
309                #[case("rust -s https://test.com")]
310                #[case("rust --server-url https://test.com")]
311                fn it_parses_pos_args_with_server_url_cli_option(
312                    #[case] cli_args: &str,
313                ) {
314                    let cli_args = parse_cli_args(cli_args);
315                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
316
317                    let actual_result = parsed_args.as_ref().ok();
318                    let expected_result = Args::default()
319                        .with_template_names(make_string_vec("rust"))
320                        .with_server_url("https://test.com")
321                        .with_endpoint_uri(
322                            constant::template_manager::GENERATOR_URI,
323                        );
324                    let expected_result = Some(&expected_result);
325
326                    assert!(actual_result.is_some());
327                    assert_eq!(actual_result, expected_result);
328                }
329
330                #[rstest]
331                #[case("rust -e /test/api")]
332                #[case("rust --endpoint-uri /test/api")]
333                fn it_parses_pos_args_with_endpoint_uri_cli_option(
334                    #[case] cli_args: &str,
335                ) {
336                    let cli_args = parse_cli_args(cli_args);
337                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
338
339                    let actual_result = parsed_args.as_ref().ok();
340                    let expected_result = Args::default()
341                        .with_template_names(make_string_vec("rust"))
342                        .with_server_url(constant::template_manager::BASE_URL)
343                        .with_endpoint_uri("/test/api");
344                    let expected_result = Some(&expected_result);
345
346                    assert!(actual_result.is_some());
347                    assert_eq!(actual_result, expected_result);
348                }
349
350                #[rstest]
351                #[case("-l", "")]
352                #[case("--list", "")]
353                #[case("rust --list", "rust")]
354                #[case("rust python --list", "rust python")]
355                fn it_parses_list_cli_option(
356                    #[case] cli_args: &str,
357                    #[case] template_names: &str,
358                ) {
359                    let cli_args = parse_cli_args(cli_args);
360                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
361
362                    let actual_result = parsed_args.as_ref().ok();
363                    let expected_result = Args::default()
364                        .with_template_names(make_string_vec(template_names))
365                        .with_server_url(constant::template_manager::BASE_URL)
366                        .with_endpoint_uri(
367                            constant::template_manager::GENERATOR_URI,
368                        )
369                        .with_show_list(true);
370                    let expected_result = Some(&expected_result);
371
372                    assert!(actual_result.is_some());
373                    assert_eq!(actual_result, expected_result);
374                }
375            }
376
377            mod failure {
378                use super::*;
379                use crate::{ExitKind, constant};
380
381                #[test]
382                fn it_fails_parsing_when_no_pos_args_given() {
383                    let cli_args = parse_cli_args("");
384                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
385
386                    let actual_error = parsed_args.as_ref().err();
387                    let expected_error = ProgramExit {
388                        message: load_expectation_file_as_string(
389                            "no_pos_args_error",
390                        ),
391                        exit_status: constant::exit_status::GENERIC,
392
393                        styled_message: Some(load_expectation_file_as_string(
394                            "ansi_no_pos_args_error",
395                        )),
396                        kind: ExitKind::Error,
397                    };
398                    let expected_error = Some(&expected_error);
399
400                    assert!(actual_error.is_some());
401                    assert_eq!(actual_error, expected_error);
402                }
403
404                #[test]
405                fn it_fails_parsing_when_commas_in_pos_args() {
406                    let cli_args = parse_cli_args("python,java");
407                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
408
409                    let actual_error = parsed_args.as_ref().err();
410                    let expected_error = ProgramExit {
411                        message: load_expectation_file_as_string(
412                            "comma_pos_args_error",
413                        ),
414                        exit_status: constant::exit_status::GENERIC,
415
416                        styled_message: Some(load_expectation_file_as_string(
417                            "ansi_comma_pos_args_error",
418                        )),
419                        kind: ExitKind::Error,
420                    };
421                    let expected_error = Some(&expected_error);
422
423                    assert!(actual_error.is_some());
424                    assert_eq!(actual_error, expected_error);
425                }
426
427                #[test]
428                fn it_fails_parsing_when_whitespaces_in_pos_args() {
429                    let cli_args = vec![
430                        OsString::from(env!("CARGO_PKG_NAME")),
431                        OsString::from("r "),
432                    ];
433                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
434
435                    let actual_error = parsed_args.as_ref().err();
436                    let expected_error = ProgramExit {
437                        message: load_expectation_file_as_string(
438                            "whitespace_pos_args_error",
439                        ),
440                        exit_status: constant::exit_status::GENERIC,
441
442                        styled_message: Some(load_expectation_file_as_string(
443                            "ansi_whitespace_pos_args_error",
444                        )),
445                        kind: ExitKind::Error,
446                    };
447                    let expected_error = Some(&expected_error);
448
449                    assert!(actual_error.is_some());
450                    assert_eq!(actual_error, expected_error);
451                }
452
453                #[test]
454                fn it_fails_parsing_when_commas_and_whitespaces_in_pos_args() {
455                    let cli_args = vec![
456                        OsString::from(env!("CARGO_PKG_NAME")),
457                        OsString::from("r ,"),
458                    ];
459                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
460
461                    let actual_error = parsed_args.as_ref().err();
462                    let expected_error = ProgramExit {
463                        message: load_expectation_file_as_string(
464                            "comma_whitespace_pos_args_error",
465                        ),
466                        exit_status: constant::exit_status::GENERIC,
467
468                        styled_message: Some(load_expectation_file_as_string(
469                            "ansi_comma_whitespace_pos_args_error",
470                        )),
471                        kind: ExitKind::Error,
472                    };
473                    let expected_error = Some(&expected_error);
474
475                    assert!(actual_error.is_some());
476                    assert_eq!(actual_error, expected_error);
477                }
478
479                #[test]
480                fn it_fails_parsing_when_server_url_but_no_pos_args() {
481                    let cli_args = parse_cli_args("-s https://test.com");
482                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
483
484                    let actual_error = parsed_args.as_ref().err();
485                    let expected_error = ProgramExit {
486                        message: load_expectation_file_as_string(
487                            "server_url_no_pos_args_error",
488                        ),
489                        exit_status: constant::exit_status::GENERIC,
490
491                        styled_message: Some(load_expectation_file_as_string(
492                            "ansi_server_url_no_pos_args_error",
493                        )),
494                        kind: ExitKind::Error,
495                    };
496                    let expected_error = Some(&expected_error);
497
498                    assert!(actual_error.is_some());
499                    assert_eq!(actual_error, expected_error);
500                }
501
502                #[test]
503                fn it_fails_parsing_when_endpoint_uri_but_no_pos_args() {
504                    let cli_args = parse_cli_args("-e /test/api");
505                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
506
507                    let actual_error = parsed_args.as_ref().err();
508                    let expected_error = ProgramExit {
509                        message: load_expectation_file_as_string(
510                            "endpoint_uri_no_pos_args_error",
511                        ),
512                        exit_status: constant::exit_status::GENERIC,
513
514                        styled_message: Some(load_expectation_file_as_string(
515                            "ansi_endpoint_uri_no_pos_args_error",
516                        )),
517                        kind: ExitKind::Error,
518                    };
519                    let expected_error = Some(&expected_error);
520
521                    assert!(actual_error.is_some());
522                    assert_eq!(actual_error, expected_error);
523                }
524
525                #[test]
526                fn it_fails_parsing_when_inexistent_cli_option() {
527                    let cli_args = parse_cli_args("-x");
528                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
529
530                    let actual_error = parsed_args.as_ref().err();
531                    let expected_error = ProgramExit {
532                        message: load_expectation_file_as_string(
533                            "unexpected_argument_error",
534                        ),
535                        exit_status: constant::exit_status::GENERIC,
536                        styled_message: Some(load_expectation_file_as_string(
537                            "ansi_unexpected_argument_error",
538                        )),
539                        kind: ExitKind::Error,
540                    };
541                    let expected_error = Some(&expected_error);
542
543                    assert!(actual_error.is_some());
544                    assert_eq!(actual_error, expected_error);
545                }
546            }
547        }
548
549        mod parse {
550            use super::*;
551
552            mod success {
553                use super::*;
554
555                #[test]
556                fn it_parses_given_cli_options() {
557                    let cli_args = parse_cli_args("rust python -s test -e foo");
558
559                    let actual_result = ClapArgsParser::new().parse(cli_args);
560                    let expected_result = Args::default()
561                        .with_template_names(make_string_vec("rust python"))
562                        .with_server_url("test")
563                        .with_endpoint_uri("foo");
564
565                    assert_eq!(actual_result, expected_result);
566                }
567            }
568        }
569    }
570}