From c42d4ad2f28a31140ba0e3eda91d8f6b4a7f34d4 Mon Sep 17 00:00:00 2001 From: Sebastien Rousseau Date: Tue, 26 Mar 2024 12:59:03 +0000 Subject: [PATCH] refactor(libmake): :art: Enhance arg parsing, validation, error handling and dep updates Improve argument parsing using a macro, handle errors gracefully, and enhance validation logic for manual generation parameters. --- Cargo.toml | 7 +- src/args.rs | 130 ++++++++++++++++++++------------------ src/macros/file_macros.rs | 17 +++++ src/macros/mod.rs | 3 + tests/test_args.rs | 18 +++--- 5 files changed, 100 insertions(+), 75 deletions(-) create mode 100644 src/macros/file_macros.rs diff --git a/Cargo.toml b/Cargo.toml index 3a09c88..aa9f3a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,17 +54,18 @@ debug = true [dependencies] anyhow = "1.0.81" assert_cmd = "2.0.14" -clap = "4.5.3" +clap = "4.5.4" configparser = "3.0.4" csv = "1.3.0" dtt = "0.0.5" env_logger = "0.11.3" figlet-rs = "0.1.5" log = {version="0.4.21", features = ["std"] } -reqwest = { version = "0.12.1", features = ["blocking"] } +regex = "1.10.4" +reqwest = { version = "0.12.2", features = ["blocking"] } rlg = "0.0.3" serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.114" +serde_json = "1.0.115" serde_yaml = "0.9.33" serde_ini = "0.2.0" tempfile = "3.10.1" diff --git a/src/args.rs b/src/args.rs index 8484e06..86d47a0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -4,7 +4,9 @@ // Copyright © 2024 LibMake. All rights reserved. use super::{ - extract_param, generator::generate_files, + generate_file, + extract_param, + generator::generate_files, generators::csv::generate_from_csv, generators::ini::generate_from_ini, generators::json::generate_from_json, @@ -13,6 +15,7 @@ use super::{ models::model_params::FileGenerationParams, }; use clap::ArgMatches; +use regex::Regex; use std::error::Error; /// Processes the command line arguments provided to the program. @@ -21,56 +24,40 @@ use std::error::Error; /// /// # Arguments /// -/// * `matches` - An instance of `clap::ArgMatches` containing the -/// parsed command line arguments. +/// * `matches` - An instance of `clap::ArgMatches` containing the parsed command line arguments. /// /// # Errors /// /// This function will return an error if there is an issue with processing the command line arguments or generating files. -/// -/// # Panics -/// -/// This function may panic if a required command line argument is not provided. -pub fn process_arguments( - matches: &ArgMatches, -) -> Result<(), Box> { +pub fn process_arguments(matches: &ArgMatches) -> Result<(), Box> { match matches.subcommand() { Some(("file", file_matches)) => { - let file_types = ["csv", "ini", "json", "yaml", "toml"]; - - for file_type in file_types.iter() { - if let Some(value) = - file_matches.get_one::(file_type) - { - match *file_type { - "csv" if !value.trim().is_empty() => { - generate_from_csv(value)? - } - "ini" if !value.trim().is_empty() => { - generate_from_ini(value)? - } - "json" if !value.trim().is_empty() => { - generate_from_json(value)? - } - "yaml" if !value.trim().is_empty() => { - generate_from_yaml(value)? - } - "toml" if !value.trim().is_empty() => { - generate_from_toml(value)? - } - _ => {} - } - } + if let Some(value) = file_matches.get_one::("csv") { + generate_file!("csv", value, generate_from_csv); + } + if let Some(value) = file_matches.get_one::("ini") { + generate_file!("ini", value, generate_from_ini); + } + if let Some(value) = file_matches.get_one::("json") { + generate_file!("json", value, generate_from_json); + } + if let Some(value) = file_matches.get_one::("yaml") { + generate_file!("yaml", value, generate_from_yaml); + } + if let Some(value) = file_matches.get_one::("toml") { + generate_file!("toml", value, generate_from_toml); } } Some(("manual", manual_matches)) => { let params = extract_manual_params(manual_matches)?; - generate_files(params)?; - println!("Template files generated successfully!"); + if let Err(err) = generate_files(params) { + eprintln!("Error generating template files: {}", err); + } else { + println!("Template files generated successfully!"); + } } _ => { eprintln!("No valid subcommand was used. Please use '--help' for usage information."); - std::process::exit(1); } } @@ -105,59 +92,76 @@ pub fn extract_manual_params( } /// Validates the manual generation parameters. -pub fn validate_params( - params: &FileGenerationParams, -) -> Result<(), Box> { +pub fn validate_params(params: &FileGenerationParams) -> Result<(), Box> { if params.name.is_none() { return Err("The name of the library is required for manual generation.".into()); } if params.output.is_none() { - return Err( - "The output directory is required for manual generation." - .into(), - ); + return Err("The output directory is required for manual generation.".into()); } if let Some(edition) = ¶ms.edition { - if edition != "2015" && edition != "2018" && edition != "2021" { - return Err(format!("Invalid edition: {}. Supported editions are 2015, 2018, and 2021.", edition).into()); + let valid_editions = ["2015", "2018", "2021"]; + if !valid_editions.contains(&edition.as_str()) { + return Err(format!( + "Invalid edition: {}. Supported editions are: {}.", + edition, + valid_editions.join(", ") + ) + .into()); } } if let Some(rustversion) = ¶ms.rustversion { - if !rustversion.starts_with("1.") { - return Err(format!("Invalid Rust version: {}. Rust version should start with '1.'.", rustversion).into()); + let version_regex = Regex::new(r"^1\.\d+\.\d+$").unwrap(); + if !version_regex.is_match(rustversion) { + return Err(format!( + "Invalid Rust version: {}. Rust version should be in the format '1.x.y'.", + rustversion + ) + .into()); } } if let Some(email) = ¶ms.email { - if !email.contains('@') { - return Err(format!("Invalid email address: {}. Email address should contain '@'.", email).into()); + let email_regex = Regex::new(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$").unwrap(); + if !email_regex.is_match(email) { + return Err(format!("Invalid email address: {}.", email).into()); } } if let Some(repository) = ¶ms.repository { - if !repository.starts_with("https://") - && !repository.starts_with("git://") - { - return Err(format!("Invalid repository URL: {}. Repository URL should start with 'https://' or 'git://'.", repository).into()); + let repo_regex = + Regex::new(r"^(https://|git://|ssh://|git@).+\.git$").unwrap(); + if !repo_regex.is_match(repository) { + return Err(format!( + "Invalid repository URL: {}. Repository URL should be a valid Git URL.", + repository + ) + .into()); } } if let Some(homepage) = ¶ms.homepage { - if !homepage.starts_with("http://") - && !homepage.starts_with("https://") - { - return Err(format!("Invalid homepage URL: {}. Homepage URL should start with 'http://' or 'https://'.", homepage).into()); + let url_regex = Regex::new(r"^(http://|https://).+$").unwrap(); + if !url_regex.is_match(homepage) { + return Err(format!( + "Invalid homepage URL: {}. Homepage URL should start with 'http://' or 'https://'.", + homepage + ) + .into()); } } if let Some(documentation) = ¶ms.documentation { - if !documentation.starts_with("http://") - && !documentation.starts_with("https://") - { - return Err(format!("Invalid documentation URL: {}. Documentation URL should start with 'http://' or 'https://'.", documentation).into()); + let url_regex = Regex::new(r"^(http://|https://).+$").unwrap(); + if !url_regex.is_match(documentation) { + return Err(format!( + "Invalid documentation URL: {}. Documentation URL should start with 'http://' or 'https://'.", + documentation + ) + .into()); } } diff --git a/src/macros/file_macros.rs b/src/macros/file_macros.rs new file mode 100644 index 0000000..346d11c --- /dev/null +++ b/src/macros/file_macros.rs @@ -0,0 +1,17 @@ +// Copyright notice and licensing information. +// These lines indicate the copyright of the software and its licensing terms. +// SPDX-License-Identifier: Apache-2.0 OR MIT indicates dual licensing under Apache 2.0 or MIT licenses. +// Copyright © 2024 LibMake. All rights reserved. + +/// Macro to simplify the match logic for file generation. +#[macro_export] +macro_rules! generate_file { + ($file_type:expr, $value:expr, $generator:expr) => { + if !$value.trim().is_empty() { + if let Err(err) = $generator($value) { + eprintln!("Error generating {} file: {}", $file_type, err); + } + } + }; +} + diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 7002750..9b82164 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -5,6 +5,9 @@ pub mod ascii_macros; /// operations. pub mod directory_macros; +/// The `file_macros` module contains macros related to file operations. +pub mod file_macros; + /// The `generator_macros` module contains macros related to generating /// templates from JSON, YAML, and CSV files, and custom logging functionality. pub mod generator_macros; diff --git a/tests/test_args.rs b/tests/test_args.rs index 9a2af70..fad405a 100644 --- a/tests/test_args.rs +++ b/tests/test_args.rs @@ -1,7 +1,7 @@ use clap::{Arg, Command}; use libmake::{ args::{extract_manual_params, process_arguments, validate_params}, - models::model_params::FileGenerationParams + models::model_params::FileGenerationParams, }; // Tests the process_arguments function with valid arguments @@ -88,7 +88,7 @@ fn test_validate_params_invalid_edition() { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - "Invalid edition: 2023. Supported editions are 2015, 2018, and 2021.".to_string() + "Invalid edition: 2023. Supported editions are: 2015, 2018, 2021.".to_string() ); } @@ -148,7 +148,7 @@ fn test_validate_params_invalid_email() { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - "Invalid email address: . Email address should contain '@'.".to_string() + "Invalid email address: .".to_string() ); } @@ -208,7 +208,7 @@ fn test_validate_params_invalid_repository() { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - "Invalid repository URL: 123. Repository URL should start with 'https://' or 'git://'.".to_string() + "Invalid repository URL: 123. Repository URL should be a valid Git URL.".to_string() ); } @@ -240,7 +240,7 @@ fn test_validate_params_invalid_rustversion() { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - "Invalid Rust version: 2.0. Rust version should start with '1.'.".to_string() + "Invalid Rust version: 2.0. Rust version should be in the format '1.x.y'.".to_string() ); } @@ -427,7 +427,7 @@ fn test_extract_manual_params_all_fields() { .long("repository") .value_name("REPOSITORY") .default_value( - "https://github.com/test/test_lib", + "https://github.com/test/test_lib.git", ), ) .arg( @@ -479,7 +479,7 @@ fn test_extract_manual_params_all_fields() { "--readme", "README.md", "--repository", - "https://github.com/test/test_lib", + "https://github.com/test/test_lib.git", "--rustversion", "1.60.0", "--version", @@ -489,7 +489,7 @@ fn test_extract_manual_params_all_fields() { ]); let result = extract_manual_params( - matches.subcommand_matches("manual").unwrap() + matches.subcommand_matches("manual").unwrap(), ); assert!(result.is_ok()); @@ -518,7 +518,7 @@ fn test_extract_manual_params_all_fields() { assert_eq!(params.readme, Some("README.md".to_string())); assert_eq!( params.repository, - Some("https://github.com/test/test_lib".to_string()) + Some("https://github.com/test/test_lib.git".to_string()) ); assert_eq!(params.rustversion, Some("1.60.0".to_string())); assert_eq!(params.version, Some("0.1.0".to_string()));