Skip to content

Commit

Permalink
Merge pull request #35 from jakedeichert/validate-numerical-flag
Browse files Browse the repository at this point in the history
Add support for type=number in option flags for numerical validation
  • Loading branch information
jacobdeichert authored Sep 25, 2019
2 parents 5963191 + f7300e6 commit f46b08e
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 17 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ Important to note that `mask` auto injects a very common `boolean` flag called `
* desc: Which port to serve on

~~~sh
# Set a fallback port
PORT=${port:-8080}
PORT=${port:-8080} # Set a fallback port if not supplied

if [[ "$verbose" == "true" ]]; then
echo "Starting an http server on PORT: $PORT"
Expand All @@ -123,6 +122,27 @@ python -m SimpleHTTPServer $PORT
~~~
```

You can also make your flag expect a numerical value by setting its `type` to `number`. This means `mask` will automatically validate it as a number for you. If it fails to validate, `mask` will exit with a helpful error message.

**Example:**

```markdown
## purchase (price)

> Calculate the total price of something.

**OPTIONS**
* tax
* flags: -t --tax
* type: number
* desc: What's the tax?

~~~sh
TAX=${tax:-1} # Fallback to 1 if not supplied
echo "Total: $(($price * $TAX))"
~~~
```

### Subcommands

Nested command structures can easily be created since they are simply defined by the level of markdown heading. H2 (`##`) is where you define your top-level commands. Every level after that is a subcommand. The only requirement is that subcommands must have all ancestor commands present in their heading.
Expand Down Expand Up @@ -341,7 +361,6 @@ This variable is an absolute path to the maskfile's parent directory. Having the

* [ ] [Optional (non-required) positional arguments][2]
* [ ] [Infinite positional args](https://github.com/jakedeichert/mask/issues/4)
* [ ] [Option flag `number` type for input validation purposes](https://github.com/jakedeichert/mask/issues/3)



Expand Down
11 changes: 7 additions & 4 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ impl Command {
long: "verbose".to_string(),
multiple: false,
takes_value: false,
validate_as_number: false,
val: "".to_string(),
});
}
Expand All @@ -62,10 +63,11 @@ impl RequiredArg {
pub struct OptionFlag {
pub name: String,
pub desc: String,
pub short: String, // v (used as -v)
pub long: String, // verbose (used as --verbose)
pub multiple: bool, // Can it have multiple values? (-vvv OR -i one -i two)
pub takes_value: bool, // Does it take a value? (-i value)
pub short: String, // v (used as -v)
pub long: String, // verbose (used as --verbose)
pub multiple: bool, // Can it have multiple values? (-vvv OR -i one -i two)
pub takes_value: bool, // Does it take a value? (-i value)
pub validate_as_number: bool, // Should we validate it as a number?
pub val: String,
}

Expand All @@ -78,6 +80,7 @@ impl OptionFlag {
long: "".to_string(),
multiple: false,
takes_value: false,
validate_as_number: false,
val: "".to_string(),
}
}
Expand Down
18 changes: 16 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,25 @@ fn get_command_options(mut cmd: Command, matches: &ArgMatches) -> Command {
for flag in &mut cmd.option_flags {
flag.val = if flag.takes_value {
// Extract the value
matches
let raw_value = matches
.value_of(flag.name.clone())
.or(Some(""))
.unwrap()
.to_string()
.to_string();

if flag.validate_as_number && raw_value != "" {
// Try converting to an integer or float to validate it
if raw_value.parse::<isize>().is_err() && raw_value.parse::<f32>().is_err() {
eprintln!(
"{} flag `{}` expects a numerical value",
"ERROR:".red(),
flag.name
);
std::process::exit(1);
}
}

raw_value
} else {
// Check if the boolean flag is present and set to "true".
// It's a string since it's set as an environment variable.
Expand Down
7 changes: 5 additions & 2 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,14 @@ pub fn build_command_structure(maskfile_contents: String) -> Command {
let val = config_split.next().unwrap_or("").trim();
match param {
"desc" => current_option_flag.desc = val.to_string(),
// TODO: allow "number" type for input validation purposes https://github.com/jakedeichert/mask/issues/3
"type" => {
if val == "string" {
if val == "string" || val == "number" {
current_option_flag.takes_value = true;
}

if val == "number" {
current_option_flag.validate_as_number = true;
}
}
// Parse out the short and long flag names
"flags" => {
Expand Down
128 changes: 128 additions & 0 deletions tests/arguments_and_flags_test.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use assert_cmd::prelude::*;
use clap::{crate_name, crate_version};
use colored::*;
use predicates::str::contains;

mod common;
Expand Down Expand Up @@ -85,6 +86,133 @@ fi
.success();
}

mod numerical_option_flag {
use super::*;

#[test]
fn properly_validates_flag_with_type_number() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## integer
**OPTIONS**
* val
* flags: --val
* type: number
~~~bash
echo "Value: $val"
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("integer --val 1111112222")
.assert()
.stdout(contains("Value: 1111112222"))
.success();
}

#[test]
fn properly_validates_negative_numbers() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## negative
**OPTIONS**
* val
* flags: --val
* type: number
~~~bash
echo "Value: $val"
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("negative --val -123")
.assert()
.stdout(contains("Value: -123"))
.success();
}

#[test]
fn properly_validates_decimal_numbers() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## decimal
**OPTIONS**
* val
* flags: --val
* type: number
~~~bash
echo "Value: $val"
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("decimal --val 123.3456789")
.assert()
.stdout(contains("Value: 123.3456789"))
.success();
}

#[test]
fn errors_when_value_is_not_a_number() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## notanumber
**OPTIONS**
* val
* flags: --val
* type: number
~~~bash
echo "This shouldn't render"
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("notanumber --val a234")
.assert()
.stderr(contains(format!(
"{} flag `val` expects a numerical value",
"ERROR:".red()
)))
.failure();
}

#[test]
fn ignores_the_option_if_not_supplied() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## nooption
**OPTIONS**
* val
* flags: --val
* type: number
~~~bash
echo "No arg this time"
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("nooption")
.assert()
.stdout(contains("No arg this time"))
.success();
}
}

mod version_flag {
use super::*;

Expand Down
10 changes: 4 additions & 6 deletions tests/subcommands_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,15 @@ mod when_entering_negative_numbers {
fn allows_entering_negative_numbers_as_values() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## math
### math add (a) (b)
## add (a) (b)
~~~bash
echo $(($a + $b))
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("math add -1 -3")
.cli("add -1 -3")
.assert()
.stdout(contains("-4"))
.success();
Expand All @@ -81,8 +80,7 @@ echo $(($a + $b))
fn allows_entering_negative_numbers_as_flag_values() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## math
### math add
## add
**OPTIONS**
* a
Expand All @@ -99,7 +97,7 @@ echo $(($a + $b))
);

common::run_mask(&maskfile_path)
.cli("math add --a -33 --b 17")
.cli("add --a -33 --b 17")
.assert()
.stdout(contains("-16"))
.success();
Expand Down

0 comments on commit f46b08e

Please sign in to comment.