Skip to content

Commit

Permalink
Return :key and :value for errors (dashbitco#48)
Browse files Browse the repository at this point in the history
Provides programmatic access, to enable intelligent handling of
errors (with one example being providing a fallback)

Fixes dashbitco#46
  • Loading branch information
axelson authored Aug 2, 2020
1 parent 3b50b84 commit 1aa5664
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 28 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog for NimbleOptions

## Unreleased

* Return `:key` and `:value` on `%NimbleOptions.ValidationError{}` to allow programmatic use of errors.

## v0.3.0

* **Breaking change**: return `{:error, %NimbleOptions.ValidationError{}}` tuples when there's a validation error in `NimbleOptions.validate/2` instead of `{:error, message}` (with `message` being a string). You can use `Exception.message/1` to turn the `NimbleOptions.ValidationError` struct into a string.
Expand Down
42 changes: 24 additions & 18 deletions lib/nimble_options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ defmodule NimbleOptions do
:ok

keys ->
error_tuple("unknown options #{inspect(keys)}, valid options are: #{inspect(valid_opts)}")
error_tuple(keys, nil, "unknown options #{inspect(keys)}, valid options are: #{inspect(valid_opts)}")
end
end

Expand Down Expand Up @@ -330,6 +330,8 @@ defmodule NimbleOptions do

Keyword.get(schema, :required, false) ->
error_tuple(
key,
nil,
"required option #{inspect(key)} not found, received options: " <>
inspect(Keyword.keys(opts))
)
Expand All @@ -340,41 +342,41 @@ defmodule NimbleOptions do
end

defp validate_type(:integer, key, value) when not is_integer(value) do
error_tuple("expected #{inspect(key)} to be an integer, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be an integer, got: #{inspect(value)}")
end

defp validate_type(:non_neg_integer, key, value) when not is_integer(value) or value < 0 do
error_tuple("expected #{inspect(key)} to be a non negative integer, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be a non negative integer, got: #{inspect(value)}")
end

defp validate_type(:pos_integer, key, value) when not is_integer(value) or value < 1 do
error_tuple("expected #{inspect(key)} to be a positive integer, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be a positive integer, got: #{inspect(value)}")
end

defp validate_type(:atom, key, value) when not is_atom(value) do
error_tuple("expected #{inspect(key)} to be an atom, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be an atom, got: #{inspect(value)}")
end

defp validate_type(:timeout, key, value)
when not (value == :infinity or (is_integer(value) and value >= 0)) do
error_tuple(
"expected #{inspect(key)} to be non-negative integer or :infinity, got: #{inspect(value)}"
key, value, "expected #{inspect(key)} to be non-negative integer or :infinity, got: #{inspect(value)}"
)
end

defp validate_type(:string, key, value) when not is_binary(value) do
error_tuple("expected #{inspect(key)} to be an string, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be an string, got: #{inspect(value)}")
end

defp validate_type(:boolean, key, value) when not is_boolean(value) do
error_tuple("expected #{inspect(key)} to be an boolean, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be an boolean, got: #{inspect(value)}")
end

defp validate_type(:keyword_list, key, value) do
if keyword_list?(value) do
:ok
else
error_tuple("expected #{inspect(key)} to be a keyword list, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be a keyword list, got: #{inspect(value)}")
end
end

Expand All @@ -383,6 +385,8 @@ defmodule NimbleOptions do
:ok
else
error_tuple(
key,
value,
"expected #{inspect(key)} to be a non-empty keyword list, got: #{inspect(value)}"
)
end
Expand All @@ -393,23 +397,23 @@ defmodule NimbleOptions do
end

defp validate_type(:pid, key, value) do
error_tuple("expected #{inspect(key)} to be a pid, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be a pid, got: #{inspect(value)}")
end

defp validate_type(:mfa, _key, {m, f, args}) when is_atom(m) and is_atom(f) and is_list(args) do
:ok
end

defp validate_type(:mfa, key, value) when not is_nil(value) do
error_tuple("expected #{inspect(key)} to be a tuple {Mod, Fun, Args}, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be a tuple {Mod, Fun, Args}, got: #{inspect(value)}")
end

defp validate_type(:mod_arg, _key, {m, _arg}) when is_atom(m) do
:ok
end

defp validate_type(:mod_arg, key, value) do
error_tuple("expected #{inspect(key)} to be a tuple {Mod, Arg}, got: #{inspect(value)}")
error_tuple(key, value, "expected #{inspect(key)} to be a tuple {Mod, Arg}, got: #{inspect(value)}")
end

defp validate_type({:fun, arity}, key, value) do
Expand All @@ -421,17 +425,17 @@ defmodule NimbleOptions do
:ok

{:arity, fun_arity} ->
error_tuple(expected <> "got: function of arity #{inspect(fun_arity)}")
error_tuple(key, value, expected <> "got: function of arity #{inspect(fun_arity)}")
end
else
error_tuple(expected <> "got: #{inspect(value)}")
error_tuple(key, value, expected <> "got: #{inspect(value)}")
end
end

defp validate_type({:custom, mod, fun, args}, _key, value) do
defp validate_type({:custom, mod, fun, args}, key, value) do
case apply(mod, fun, [value | args]) do
{:ok, value} -> {:ok, value}
{:error, message} when is_binary(message) -> error_tuple(message)
{:error, message} when is_binary(message) -> error_tuple(key, value, message)
end
end

Expand All @@ -440,6 +444,8 @@ defmodule NimbleOptions do
:ok
else
error_tuple(
key,
value,
"expected #{inspect(key)} to be one of #{inspect(choices)}, got: #{inspect(value)}"
)
end
Expand Down Expand Up @@ -509,7 +515,7 @@ defmodule NimbleOptions do
end
end

defp error_tuple(message) do
{:error, %ValidationError{message: message}}
defp error_tuple(key, value, message) do
{:error, %ValidationError{key: key, message: message, value: value}}
end
end
11 changes: 8 additions & 3 deletions lib/nimble_options/validation_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ defmodule NimbleOptions.ValidationError do
@moduledoc """
An error that is returned (or raised) when options are invalid.
The contents of this exception are not public and you should not rely on
its fields.
Only these documented fields are considered public. All other fields are
considered private and should not be referenced:
* key: The key that did not successfully validate
* keys_path: If the key is nested, this is the path to the key
* value: The value that failed to validate. Is set to `nil` if there was no
value provided.
Since this is an exception, you can either raise it directly with `raise/1`
or turn it into a message string with `Exception.message/1`.
"""

@type t() :: %__MODULE__{}

defexception [:message, keys_path: []]
defexception [:message, :key, :value, keys_path: []]

@impl true
def message(%__MODULE__{message: message, keys_path: keys_path}) do
Expand Down
Loading

0 comments on commit 1aa5664

Please sign in to comment.