Skip to content

Commit

Permalink
refactor(libmake): 🎨 Enhance arg parsing, validation, error handling …
Browse files Browse the repository at this point in the history
…and dep updates

Improve argument parsing using a macro, handle errors gracefully, and enhance validation logic for manual generation parameters.
  • Loading branch information
sebastienrousseau committed Mar 26, 2024
1 parent 11eeae0 commit c42d4ad
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 75 deletions.
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
130 changes: 67 additions & 63 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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<dyn Error>> {
pub fn process_arguments(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
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::<String>(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::<String>("csv") {
generate_file!("csv", value, generate_from_csv);
}
if let Some(value) = file_matches.get_one::<String>("ini") {
generate_file!("ini", value, generate_from_ini);
}
if let Some(value) = file_matches.get_one::<String>("json") {
generate_file!("json", value, generate_from_json);
}
if let Some(value) = file_matches.get_one::<String>("yaml") {
generate_file!("yaml", value, generate_from_yaml);
}
if let Some(value) = file_matches.get_one::<String>("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);
}
}

Expand Down Expand Up @@ -105,59 +92,76 @@ pub fn extract_manual_params(
}

/// Validates the manual generation parameters.
pub fn validate_params(
params: &FileGenerationParams,
) -> Result<(), Box<dyn Error>> {
pub fn validate_params(params: &FileGenerationParams) -> Result<(), Box<dyn Error>> {
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) = &params.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) = &params.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) = &params.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) = &params.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) = &params.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) = &params.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());
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/macros/file_macros.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
};
}

3 changes: 3 additions & 0 deletions src/macros/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 9 additions & 9 deletions tests/test_args.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
);
}

Expand Down Expand Up @@ -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>. Email address should contain '@'.".to_string()
"Invalid email address: <EMAIL>.".to_string()
);
}

Expand Down Expand Up @@ -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()
);
}

Expand Down Expand Up @@ -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()
);
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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());

Expand Down Expand Up @@ -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()));
Expand Down

0 comments on commit c42d4ad

Please sign in to comment.