gitignore_template_generator/parser/
api.rs1use clap::Parser;
2
3use crate::{
4 constant,
5 validator::{CliArgsValidator, DefaultCliArgsValidator},
6};
7
8use std::ffi::OsString;
9
10use crate::ProgramExit;
11pub use crate::parser::impls::DefaultArgsParser;
12
13#[derive(Parser, Debug, PartialEq, Default)]
19#[command(version, author, long_about = None)]
20#[command(about = constant::parser_infos::ABOUT)]
21#[command(help_template = "\
22{before-help}
23{usage-heading} {usage}
24
25{about-with-newline}
26{all-args}{after-help}
27
28Version: {version}
29Author: {author}
30")]
31#[command(disable_help_flag = true, disable_version_flag = true)]
32pub struct Args {
33 #[arg(
38 required_unless_present_any = vec!["author", "version", "help"],
39 value_parser = DefaultCliArgsValidator::has_no_commas,
40 help = constant::help_messages::TEMPLATE_NAMES
41 )]
42 pub template_names: Vec<String>,
43
44 #[arg(
51 short = constant::cli_options::SERVER_URL.short,
52 long = constant::cli_options::SERVER_URL.long,
53 help = constant::help_messages::SERVER_URL,
54 default_value = constant::template_generator::BASE_URL
55 )]
56 pub server_url: String,
57
58 #[arg(
64 id = "help",
65 short = constant::cli_options::HELP.short,
66 long = constant::cli_options::HELP.long,
67 action = clap::ArgAction::SetTrue,
68 help = constant::help_messages::HELP
69 )]
70 pub show_help: bool,
71
72 #[arg(
78 id = "version",
79 short = constant::cli_options::VERSION.short,
80 long = constant::cli_options::VERSION.long,
81 action = clap::ArgAction::SetTrue,
82 help = constant::help_messages::VERSION
83 )]
84 pub show_version: bool,
85
86 #[arg(
92 id = "author",
93 short = constant::cli_options::AUTHOR.short,
94 long = constant::cli_options::AUTHOR.long,
95 action = clap::ArgAction::SetTrue,
96 help = constant::help_messages::AUTHOR
97 )]
98 pub show_author: bool,
99}
100
101impl Args {
102 pub fn with_template_names(mut self, template_names: Vec<String>) -> Self {
115 self.template_names = template_names;
116 self
117 }
118
119 pub fn with_server_url(mut self, server_url: &str) -> Self {
132 self.server_url = server_url.to_string();
133 self
134 }
135}
136
137pub trait ArgsParser {
139 fn parse(args: impl IntoIterator<Item = OsString>) -> Args;
154
155 fn try_parse(
173 args: impl IntoIterator<Item = OsString>,
174 ) -> Result<Args, ProgramExit>;
175}
176
177#[cfg(test)]
178mod tests {
179 use rstest::*;
180
181 use super::*;
182 use crate::helper::*;
183
184 mod default_args_parser {
185 use super::*;
186
187 mod try_parse {
188 use super::*;
189
190 mod success {
191 use crate::{ExitKind, constant};
192
193 use super::*;
194
195 #[rstest]
196 #[case("-V")]
197 #[case("--version")]
198 #[case("-V rust")]
199 #[case("rust -V")]
200 #[case("rust -s foo -V")]
201 #[case("-aV")]
202 fn it_parses_version_cli_option(#[case] cli_args: &str) {
203 let cli_args = parse_cli_args(cli_args);
204 let parsed_args = DefaultArgsParser::try_parse(cli_args);
205
206 let actual_error = parsed_args.as_ref().err();
207 let expected_error = ProgramExit {
208 message: format!(
209 "{} {}",
210 env!("CARGO_PKG_NAME"),
211 env!("CARGO_PKG_VERSION")
212 ),
213 exit_status: 0,
214 styled_message: None,
215 kind: ExitKind::VersionInfos,
216 };
217 let expected_error = Some(&expected_error);
218
219 assert!(actual_error.is_some());
220 assert_eq!(actual_error, expected_error);
221 }
222
223 #[rstest]
224 #[case("-h")]
225 #[case("--help")]
226 #[case("-h rust")]
227 #[case("rust -h")]
228 #[case("rust -s foo -h")]
229 #[case("-aVh")]
230 fn it_parses_help_cli_option(#[case] cli_args: &str) {
231 let cli_args = parse_cli_args(cli_args);
232 let parsed_args = DefaultArgsParser::try_parse(cli_args);
233
234 let actual_error = parsed_args.as_ref().err();
235 let expected_error = ProgramExit {
236 message: get_help_message(),
237 exit_status: 0,
238 styled_message: Some(get_ansi_help_message()),
239 kind: ExitKind::HelpInfos,
240 };
241 let expected_error = Some(&expected_error);
242
243 assert!(actual_error.is_some());
244 assert_eq!(actual_error, expected_error);
245 }
246
247 #[rstest]
248 #[case("-a")]
249 #[case("--author")]
250 #[case("-a rust")]
251 #[case("rust -a")]
252 #[case("rust -s foo -a")]
253 fn it_parses_author_cli_option_preemptively(
254 #[case] cli_args: &str,
255 ) {
256 let cli_args = parse_cli_args(cli_args);
257 let parsed_args = DefaultArgsParser::try_parse(cli_args);
258
259 let actual_error = parsed_args.as_ref().err();
260 let expected_error = ProgramExit {
261 message: env!("CARGO_PKG_AUTHORS").to_string(),
262 exit_status: 0,
263 styled_message: None,
264 kind: ExitKind::AuthorInfos,
265 };
266 let expected_error = Some(&expected_error);
267
268 assert!(actual_error.is_some());
269 assert_eq!(actual_error, expected_error);
270 }
271
272 #[rstest]
273 #[case("rust")]
274 #[case("rust python node")]
275 fn it_parses_pos_args_without_server_url_cli_option(
276 #[case] cli_options: &str,
277 ) {
278 let cli_args = parse_cli_args(cli_options);
279 let parsed_args = DefaultArgsParser::try_parse(cli_args);
280
281 let actual_result = parsed_args.as_ref().ok();
282 let expected_result = Args::default()
283 .with_template_names(make_string_vec(cli_options))
284 .with_server_url(
285 constant::template_generator::BASE_URL,
286 );
287 let expected_result = Some(&expected_result);
288
289 assert!(actual_result.is_some());
290 assert_eq!(actual_result, expected_result);
291 }
292
293 #[rstest]
294 #[case("rust -s https://test.com")]
295 #[case("rust --server-url https://test.com")]
296 fn it_parses_pos_args_with_server_url_cli_option(
297 #[case] cli_args: &str,
298 ) {
299 let cli_args = parse_cli_args(cli_args);
300 let parsed_args = DefaultArgsParser::try_parse(cli_args);
301
302 let actual_result = parsed_args.as_ref().ok();
303 let expected_result = Args::default()
304 .with_template_names(make_string_vec("rust"))
305 .with_server_url("https://test.com");
306 let expected_result = Some(&expected_result);
307
308 assert!(actual_result.is_some());
309 assert_eq!(actual_result, expected_result);
310 }
311 }
312
313 mod failure {
314 use crate::{ExitKind, constant};
315
316 use super::*;
317
318 #[test]
319 fn it_fails_parsing_when_no_pos_args_given() {
320 let cli_args = parse_cli_args("");
321 let parsed_args = DefaultArgsParser::try_parse(cli_args);
322
323 let actual_error = parsed_args.as_ref().err();
324 let expected_error = ProgramExit {
325 message: load_expectation_file_as_string(
326 "no_pos_args_error",
327 ),
328 exit_status: constant::exit_status::GENERIC,
329
330 styled_message: Some(load_expectation_file_as_string(
331 "ansi_no_pos_args_error",
332 )),
333 kind: ExitKind::Error,
334 };
335 let expected_error = Some(&expected_error);
336
337 assert!(actual_error.is_some());
338 assert_eq!(actual_error, expected_error);
339 }
340
341 #[test]
342 fn it_fails_parsing_when_commas_in_pos_args() {
343 let cli_args = parse_cli_args("python,java");
344 let parsed_args = DefaultArgsParser::try_parse(cli_args);
345
346 let actual_error = parsed_args.as_ref().err();
347 let expected_error = ProgramExit {
348 message: load_expectation_file_as_string(
349 "comma_pos_args_error",
350 ),
351 exit_status: constant::exit_status::GENERIC,
352
353 styled_message: Some(load_expectation_file_as_string(
354 "ansi_comma_pos_args_error",
355 )),
356 kind: ExitKind::Error,
357 };
358 let expected_error = Some(&expected_error);
359
360 assert!(actual_error.is_some());
361 assert_eq!(actual_error, expected_error);
362 }
363
364 #[test]
365 fn it_fails_parsing_when_server_url_but_no_pos_args() {
366 let cli_args = parse_cli_args("-s https://test.com");
367 let parsed_args = DefaultArgsParser::try_parse(cli_args);
368
369 let actual_error = parsed_args.as_ref().err();
370 let expected_error = ProgramExit {
371 message: load_expectation_file_as_string(
372 "server_url_no_pos_args_error",
373 ),
374 exit_status: constant::exit_status::GENERIC,
375
376 styled_message: Some(load_expectation_file_as_string(
377 "ansi_server_url_no_pos_args_error",
378 )),
379 kind: ExitKind::Error,
380 };
381 let expected_error = Some(&expected_error);
382
383 assert!(actual_error.is_some());
384 assert_eq!(actual_error, expected_error);
385 }
386
387 #[test]
388 fn it_fails_parsing_when_inexistent_cli_option() {
389 let cli_args = parse_cli_args("-x");
390 let parsed_args = DefaultArgsParser::try_parse(cli_args);
391
392 let actual_error = parsed_args.as_ref().err();
393 let expected_error = ProgramExit {
394 message: load_expectation_file_as_string(
395 "unexpected_argument_error",
396 ),
397 exit_status: constant::exit_status::GENERIC,
398 styled_message: Some(load_expectation_file_as_string(
399 "ansi_unexpected_argument_error",
400 )),
401 kind: ExitKind::Error,
402 };
403 let expected_error = Some(&expected_error);
404
405 assert!(actual_error.is_some());
406 assert_eq!(actual_error, expected_error);
407 }
408 }
409 }
410 }
411}