Skip to content

Commit

Permalink
Support post validation in CliOptions.parse/2
Browse files Browse the repository at this point in the history
  • Loading branch information
pnezis committed Oct 22, 2024
1 parent 0acfb9a commit 696ab3a
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 9 deletions.
38 changes: 38 additions & 0 deletions cli_options/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,46 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

* Support post validation of the parsed options in `CliOptions.parse/3` through an
optional `:post_validate` option.

```cli
schema = [
silent: [type: :boolean],
verbose: [type: :boolean]
]
# the flags --verbose and --silent should not be set together
post_validate =
fn {opts, args, extra} ->
if opts[:verbose] and opts[:silent] do
{:error, "flags --verbose and --silent cannot be set together"}
else
{:ok, {opts, args, extra}}
end
end
# without post_validate
CliOptions.parse(["--verbose", "--silent"], schema)
>>>
# with post_validate
CliOptions.parse(["--verbose", "--silent"], schema, post_validate: post_validate)
>>>
# if only one of the two is passed the validation returns :ok
CliOptions.parse(["--verbose"], schema, post_validate: post_validate)
>>>
```

## [v0.1.3](https://github.com/sportradar/elixir-workspace/tree/cli_options/v0.1.3) (2024-10-18)

### Added

* Support providing repeating arguments with a separator. If you set the `separator`
option for an argument's schema you can pass the values in the format `--arg value1<sep>value2`.
For example, for the following schema:
Expand Down
74 changes: 65 additions & 9 deletions cli_options/lib/cli_options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -454,19 +454,74 @@ defmodule CliOptions do
```cli
CliOptions.parse!(["--", "lib", "-n", "--", "bar"], [])
```
## Post validation
In some cases you may need to perform more complex validation on the provided
CLI arguments that cannot be performed by the parser itself. You could do it
directly in your codebase but for your convenience `CliOptions.parse/2` allows
you to pass an optional `:post_validate` argument. This is expected to be a
function having as input the parsed options and expected to return an
`{:ok, parsed_options()}` or an `{:error, String.t()}` tuple.
Let's see an example:
```cli
schema = [
silent: [type: :boolean],
verbose: [type: :boolean]
]
# the flags --verbose and --silent should not be set together
post_validate =
fn {opts, args, extra} ->
if opts[:verbose] and opts[:silent] do
{:error, "flags --verbose and --silent cannot be set together"}
else
{:ok, {opts, args, extra}}
end
end
# without post_validate
CliOptions.parse(["--verbose", "--silent"], schema)
>>>
# with post_validate
CliOptions.parse(["--verbose", "--silent"], schema, post_validate: post_validate)
>>>
# if only one of the two is passed the validation returns :ok
CliOptions.parse(["--verbose"], schema, post_validate: post_validate)
>>>
```
"""
@spec parse(argv :: argv(), schema :: keyword() | CliOptions.Schema.t()) ::
{:ok, parsed_options()} | {:error, String.t()}
def parse(argv, %CliOptions.Schema{} = schema) do
case CliOptions.Parser.parse(argv, schema) do
{:ok, options} -> {:ok, options}
{:error, reason} -> {:error, reason}
def parse(argv, schema, opts \\ [])

def parse(argv, %CliOptions.Schema{} = schema, opts) do
with {:ok, options} <- CliOptions.Parser.parse(argv, schema) do
post_validate(options, opts)
end
end

def parse(argv, schema) do
def parse(argv, schema, opts) do
schema = CliOptions.Schema.new!(schema)
parse(argv, schema)
parse(argv, schema, opts)
end

defp post_validate(options, opts) do
cond do
opts[:post_validate] && is_function(opts[:post_validate], 1) ->
opts[:post_validate].(options)

opts[:post_validate] ->
{:error,
"expected :post_validate to be a function of arity 1, got: #{inspect(opts[:post_validate])}"}

true ->
{:ok, options}
end
end

@doc """
Expand All @@ -483,9 +538,10 @@ defmodule CliOptions do
iex> CliOptions.parse!([], [file: [type: :string, required: true]])
** (CliOptions.ParseError) option :file is required
"""
@spec parse!(argv :: argv(), schema :: keyword() | CliOptions.Schema.t()) :: parsed_options()
def parse!(argv, schema) do
case parse(argv, schema) do
@spec parse!(argv :: argv(), schema :: keyword() | CliOptions.Schema.t(), opts :: Keyword.t()) ::
parsed_options()
def parse!(argv, schema, opts \\ []) do
case parse(argv, schema, opts) do
{:ok, options} -> options
{:error, reason} -> raise CliOptions.ParseError, reason
end
Expand Down
35 changes: 35 additions & 0 deletions cli_options/test/cli_options_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,41 @@ defmodule CliOptionsTest do
assert {:ok, {opts, [], []}} = CliOptions.parse(["-v", "-v", "--verbosity"], schema)
assert opts == [verbosity: 3]
end

test "with post_validate set" do
schema = [project: [type: :string, multiple: true], name: [type: :string]]

post_validate = fn {opts, args, extra} ->
cond do
opts[:project] != [] and is_nil(opts[:name]) ->
{:error, "name must be set if project set"}

true ->
{:ok, {Keyword.put(opts, :foo, 1), args, extra}}
end
end

assert {:error, message} =
CliOptions.parse(["--project", "foo"], schema, post_validate: post_validate)

assert message == "name must be set if project set"

assert {:ok, {opts, [], []}} =
CliOptions.parse(["--project", "foo", "--name", "name"], schema,
post_validate: post_validate
)

assert opts == [foo: 1, project: ["foo"], name: "name"]
end

test "error if post_validate is not a function" do
schema = [project: [type: :string]]

assert {:error, message} =
CliOptions.parse(["--project", "foo"], schema, post_validate: :invalid)

assert message == "expected :post_validate to be a function of arity 1, got: :invalid"
end
end

describe "parse!/2" do
Expand Down

0 comments on commit 696ab3a

Please sign in to comment.