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}