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 super::*;
175                use crate::{ExitKind, constant};
176
177                #[rstest]
178                #[case("-V")]
179                #[case("--version")]
180                #[case("-V rust")]
181                #[case("rust -V")]
182                #[case("rust -s foo -V")]
183                #[case("rust -e bar -V")]
184                #[case("-aV")]
185                fn it_parses_version_cli_option(#[case] cli_args: &str) {
186                    let cli_args = parse_cli_args(cli_args);
187                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
188
189                    let actual_error = parsed_args.as_ref().err();
190                    let expected_error = ProgramExit {
191                        message: format!(
192                            "{} {}",
193                            env!("CARGO_PKG_NAME"),
194                            env!("CARGO_PKG_VERSION")
195                        ),
196                        exit_status: 0,
197                        styled_message: None,
198                        kind: ExitKind::VersionInfos,
199                    };
200                    let expected_error = Some(&expected_error);
201
202                    assert!(actual_error.is_some());
203                    assert_eq!(actual_error, expected_error);
204                }
205
206                #[rstest]
207                #[case("-h")]
208                #[case("--help")]
209                #[case("-h rust")]
210                #[case("rust -h")]
211                #[case("rust -s foo -h")]
212                #[case("rust -e bar -h")]
213                #[case("-aVh")]
214                fn it_parses_help_cli_option(#[case] cli_args: &str) {
215                    let cli_args = parse_cli_args(cli_args);
216                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
217
218                    let actual_error = parsed_args.as_ref().err();
219                    let expected_error = ProgramExit {
220                        message: get_help_message(),
221                        exit_status: 0,
222                        styled_message: Some(get_ansi_help_message()),
223                        kind: ExitKind::HelpInfos,
224                    };
225                    let expected_error = Some(&expected_error);
226
227                    assert!(actual_error.is_some());
228                    assert_eq!(actual_error, expected_error);
229                }
230
231                #[rstest]
232                #[case("-a")]
233                #[case("--author")]
234                #[case("-a rust")]
235                #[case("rust -a")]
236                #[case("rust -s foo -a")]
237                #[case("rust -e bar -a")]
238                fn it_parses_author_cli_option_preemptively(
239                    #[case] cli_args: &str,
240                ) {
241                    let cli_args = parse_cli_args(cli_args);
242                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
243
244                    let actual_error = parsed_args.as_ref().err();
245                    let expected_error = ProgramExit {
246                        message: env!("CARGO_PKG_AUTHORS").to_string(),
247                        exit_status: 0,
248                        styled_message: None,
249                        kind: ExitKind::AuthorInfos,
250                    };
251                    let expected_error = Some(&expected_error);
252
253                    assert!(actual_error.is_some());
254                    assert_eq!(actual_error, expected_error);
255                }
256
257                #[rstest]
258                #[case("rust")]
259                #[case("rust python node")]
260                fn it_parses_pos_args_without_server_url_cli_option(
261                    #[case] cli_options: &str,
262                ) {
263                    let cli_args = parse_cli_args(cli_options);
264                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
265
266                    let actual_result = parsed_args.as_ref().ok();
267                    let expected_result = Args::default()
268                        .with_template_names(make_string_vec(cli_options))
269                        .with_server_url(constant::template_generator::BASE_URL)
270                        .with_endpoint_uri(constant::template_generator::URI);
271                    let expected_result = Some(&expected_result);
272
273                    println!("{:?}", parsed_args);
274                    assert!(actual_result.is_some());
275                    assert_eq!(actual_result, expected_result);
276                }
277
278                #[rstest]
279                #[case("rust -s https://test.com")]
280                #[case("rust --server-url https://test.com")]
281                fn it_parses_pos_args_with_server_url_cli_option(
282                    #[case] cli_args: &str,
283                ) {
284                    let cli_args = parse_cli_args(cli_args);
285                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
286
287                    let actual_result = parsed_args.as_ref().ok();
288                    let expected_result = Args::default()
289                        .with_template_names(make_string_vec("rust"))
290                        .with_server_url("https://test.com")
291                        .with_endpoint_uri(constant::template_generator::URI);
292                    let expected_result = Some(&expected_result);
293
294                    assert!(actual_result.is_some());
295                    assert_eq!(actual_result, expected_result);
296                }
297
298                #[rstest]
299                #[case("rust -e /test/api")]
300                #[case("rust --endpoint-uri /test/api")]
301                fn it_parses_pos_args_with_endpoint_uri_cli_option(
302                    #[case] cli_args: &str,
303                ) {
304                    let cli_args = parse_cli_args(cli_args);
305                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
306
307                    let actual_result = parsed_args.as_ref().ok();
308                    let expected_result = Args::default()
309                        .with_template_names(make_string_vec("rust"))
310                        .with_server_url(constant::template_generator::BASE_URL)
311                        .with_endpoint_uri("/test/api");
312                    let expected_result = Some(&expected_result);
313
314                    assert!(actual_result.is_some());
315                    assert_eq!(actual_result, expected_result);
316                }
317            }
318
319            mod failure {
320                use super::*;
321                use crate::{ExitKind, constant};
322
323                #[test]
324                fn it_fails_parsing_when_no_pos_args_given() {
325                    let cli_args = parse_cli_args("");
326                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
327
328                    let actual_error = parsed_args.as_ref().err();
329                    let expected_error = ProgramExit {
330                        message: load_expectation_file_as_string(
331                            "no_pos_args_error",
332                        ),
333                        exit_status: constant::exit_status::GENERIC,
334
335                        styled_message: Some(load_expectation_file_as_string(
336                            "ansi_no_pos_args_error",
337                        )),
338                        kind: ExitKind::Error,
339                    };
340                    let expected_error = Some(&expected_error);
341
342                    assert!(actual_error.is_some());
343                    assert_eq!(actual_error, expected_error);
344                }
345
346                #[test]
347                fn it_fails_parsing_when_commas_in_pos_args() {
348                    let cli_args = parse_cli_args("python,java");
349                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
350
351                    let actual_error = parsed_args.as_ref().err();
352                    let expected_error = ProgramExit {
353                        message: load_expectation_file_as_string(
354                            "comma_pos_args_error",
355                        ),
356                        exit_status: constant::exit_status::GENERIC,
357
358                        styled_message: Some(load_expectation_file_as_string(
359                            "ansi_comma_pos_args_error",
360                        )),
361                        kind: ExitKind::Error,
362                    };
363                    let expected_error = Some(&expected_error);
364
365                    assert!(actual_error.is_some());
366                    assert_eq!(actual_error, expected_error);
367                }
368
369                #[test]
370                fn it_fails_parsing_when_whitespaces_in_pos_args() {
371                    let cli_args = vec![
372                        OsString::from(env!("CARGO_PKG_NAME")),
373                        OsString::from("r "),
374                    ];
375                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
376
377                    let actual_error = parsed_args.as_ref().err();
378                    let expected_error = ProgramExit {
379                        message: load_expectation_file_as_string(
380                            "whitespace_pos_args_error",
381                        ),
382                        exit_status: constant::exit_status::GENERIC,
383
384                        styled_message: Some(load_expectation_file_as_string(
385                            "ansi_whitespace_pos_args_error",
386                        )),
387                        kind: ExitKind::Error,
388                    };
389                    let expected_error = Some(&expected_error);
390
391                    assert!(actual_error.is_some());
392                    assert_eq!(actual_error, expected_error);
393                }
394
395                #[test]
396                fn it_fails_parsing_when_commas_and_whitespaces_in_pos_args() {
397                    let cli_args = vec![
398                        OsString::from(env!("CARGO_PKG_NAME")),
399                        OsString::from("r ,"),
400                    ];
401                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
402
403                    let actual_error = parsed_args.as_ref().err();
404                    let expected_error = ProgramExit {
405                        message: load_expectation_file_as_string(
406                            "comma_whitespace_pos_args_error",
407                        ),
408                        exit_status: constant::exit_status::GENERIC,
409
410                        styled_message: Some(load_expectation_file_as_string(
411                            "ansi_comma_whitespace_pos_args_error",
412                        )),
413                        kind: ExitKind::Error,
414                    };
415                    let expected_error = Some(&expected_error);
416
417                    assert!(actual_error.is_some());
418                    assert_eq!(actual_error, expected_error);
419                }
420
421                #[test]
422                fn it_fails_parsing_when_server_url_but_no_pos_args() {
423                    let cli_args = parse_cli_args("-s https://test.com");
424                    let parsed_args = ClapArgsParser::new().try_parse(cli_args);
425
426                    let actual_error = parsed_args.as_ref().err();
427                    let expected_error = ProgramExit {
428                        message: load_expectation_file_as_string(
429                            "server_url_no_pos_args_error",
430                        ),
431                        exit_status: constant::exit_status::GENERIC,
432
433                        styled_message: Some(load_expectation_file_as_string(
434                            "ansi_server_url_no_pos_args_error",
435                        )),
436                        kind: ExitKind::Error,
437                    };
438                    let expected_error = Some(&expected_error);
439
440                    assert!(actual_error.is_some());
441                    assert_eq!(actual_error, expected_error);
442                }
443
444                #[test]
445                fn it_fails_parsing_when_endpoint_uri_but_no_pos_args() {
446                    let cli_args = parse_cli_args("-e /test/api");
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                            "endpoint_uri_no_pos_args_error",
453                        ),
454                        exit_status: constant::exit_status::GENERIC,
455
456                        styled_message: Some(load_expectation_file_as_string(
457                            "ansi_endpoint_uri_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_inexistent_cli_option() {
469                    let cli_args = parse_cli_args("-x");
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                            "unexpected_argument_error",
476                        ),
477                        exit_status: constant::exit_status::GENERIC,
478                        styled_message: Some(load_expectation_file_as_string(
479                            "ansi_unexpected_argument_error",
480                        )),
481                        kind: ExitKind::Error,
482                    };
483                    let expected_error = Some(&expected_error);
484
485                    assert!(actual_error.is_some());
486                    assert_eq!(actual_error, expected_error);
487                }
488            }
489        }
490    }
491}