-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add type validator. #24
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
defmodule Vex.Validators.Type do | ||
@moduledoc """ | ||
Ensure the value has the correct type. | ||
|
||
The type can be provided in the following form: | ||
|
||
* `type`: An atom representing the type. | ||
It can be any of the `TYPE` in Elixir `is_TYPE` functions. | ||
`:any` is treated as a special case and accepts any type. | ||
* `[type]`: A list of types as described above. When a list is passed, | ||
the value will be valid if it any of the types in the list. | ||
* `type: inner_type`: Type should be either `map`, `list`, `tuple`, or `function`. | ||
The usage are as follow | ||
|
||
* `function: arity`: checks if the function has the correct arity. | ||
* `map: {key_type, value_type}`: checks keys and value in the map with the provided types. | ||
* `list: type`: checks every element in the list for the given types. | ||
* `tuple: {type_a, type_b}`: check each element of the tuple with the provided types, | ||
the types tuple should be the same size as the tuple itself. | ||
|
||
## Options | ||
|
||
* `:is`: Required. The type of the value, in the format described above. | ||
* `:message`: Optional. A custom error message. May be in EEx format | ||
and use the fields described in "Custom Error Messages," below. | ||
|
||
## Examples | ||
|
||
iex> Vex.Validators.Type.validate(1, is: :binary) | ||
{:error, "must be of type :binary"} | ||
iex> Vex.Validators.Type.validate(1, is: :number) | ||
:ok | ||
iex> Vex.Validators.Type.validate(nil, is: nil) | ||
:ok | ||
iex> Vex.Validators.Type.validate(1, is: :integer) | ||
:ok | ||
iex> Vex.Validators.Type.validate("foo"", is: :binary) | ||
:ok | ||
iex> Vex.Validators.Type.validate([1, 2, 3], is: [list: :integer]) | ||
:ok | ||
iex> Vex.Validators.Type.validate(%{:a => 1, "b" => 2, 3 => 4}, is: :map) | ||
:ok | ||
iex> Vex.Validators.Type.validate(%{:a => 1, "b" => 2}, is: [map: {[:binary, :atom], :any}]) | ||
:ok | ||
iex> Vex.Validators.Type.validate(%{"b" => 2, 3 => 4}, is: [map: {[:binary, :atom], :any}]) | ||
{:error, "must be of type {:map, {[:binary, :atom], :any}}"} | ||
|
||
## Custom Error Messages | ||
|
||
Custom error messages (in EEx format), provided as :message, can use the following values: | ||
|
||
iex> Vex.Validators.Type.__validator__(:message_fields) | ||
[value: "The bad value"] | ||
|
||
An example: | ||
|
||
iex> Vex.Validators.Type.validate([1], is: :binary, message: "<%= inspect value %> is not a string") | ||
{:error, "[1] is not a string"} | ||
""" | ||
use Vex.Validator | ||
|
||
@message_fields [value: "The bad value"] | ||
|
||
@doc """ | ||
Validates the value against the given type. | ||
See the module documentation for more info. | ||
""" | ||
@spec validate(any, Keyword.t) :: :ok | {:error, String.t} | ||
def validate(value, options) when is_list(options) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you |
||
acceptable_types = Keyword.get(options, :is, []) | ||
if do_validate(value, acceptable_types) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convention would suggest to name this with a trailing |
||
:ok | ||
else | ||
message = "must be of type #{acceptable_type_str(acceptable_types)}" | ||
{:error, message(options, message, value: value)} | ||
end | ||
end | ||
|
||
# Allow any type, useful for composed types | ||
defp do_validate(_value, :any), do: true | ||
|
||
# Handle nil | ||
defp do_validate(nil, nil), do: true | ||
defp do_validate(nil, :atom), do: false | ||
|
||
# Simple types | ||
defp do_validate(value, :atom) when is_atom(value), do: true | ||
defp do_validate(value, :number) when is_number(value), do: true | ||
defp do_validate(value, :integer) when is_integer(value), do: true | ||
defp do_validate(value, :float) when is_float(value), do: true | ||
defp do_validate(value, :binary) when is_binary(value), do: true | ||
defp do_validate(value, :bitstring) when is_bitstring(value), do: true | ||
defp do_validate(value, :tuple) when is_tuple(value), do: true | ||
defp do_validate(value, :list) when is_list(value), do: true | ||
defp do_validate(value, :map) when is_map(value), do: true | ||
defp do_validate(value, :function) when is_function(value), do: true | ||
defp do_validate(value, :reference) when is_reference(value), do: true | ||
defp do_validate(value, :port) when is_port(value), do: true | ||
defp do_validate(value, :pid) when is_pid(value), do: true | ||
defp do_validate(%{__struct__: module}, module), do: true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is missing from the Vex.Validators.Type.validate(%MyStruct{}, is: MyStruct) |
||
|
||
# Complex types | ||
defp do_validate(value, :string) when is_binary(value) do | ||
String.valid?(value) | ||
end | ||
|
||
defp do_validate(value, function: arity) when is_function(value, arity), do: true | ||
|
||
defp do_validate(list, list: type) when is_list(list) do | ||
Enum.all?(list, &(do_validate(&1, type))) | ||
end | ||
defp do_validate(value, map: {key_type, value_type}) when is_map(value) do | ||
Enum.all? value, fn {k, v} -> | ||
do_validate(k, key_type) && do_validate(v, value_type) | ||
end | ||
end | ||
defp do_validate(tuple, tuple: types) | ||
when is_tuple(tuple) and is_tuple(types) and tuple_size(tuple) == tuple_size(types) do | ||
Enum.all? Enum.zip(Tuple.to_list(tuple), Tuple.to_list(types)), fn {value, type} -> | ||
do_validate(value, type) | ||
end | ||
end | ||
|
||
# Accept multiple types | ||
defp do_validate(value, acceptable_types) when is_list(acceptable_types) do | ||
Enum.any?(acceptable_types, &(do_validate(value, &1))) | ||
end | ||
|
||
# Fail if nothing above matched | ||
defp do_validate(_value, _type), do: false | ||
|
||
|
||
defp acceptable_type_str([acceptable_type]), do: inspect(acceptable_type) | ||
defp acceptable_type_str(acceptable_types) when is_list(acceptable_types) do | ||
last_type = acceptable_types |> List.last |> inspect | ||
but_last = | ||
acceptable_types | ||
|> Enum.take(Enum.count(acceptable_types) - 1) | ||
|> Enum.map(&inspect/1) | ||
|> Enum.join(", ") | ||
"#{but_last} or #{last_type}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate a bit about what this function is doing? I get what the result is but the method you need to achieve it is confusing to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have some suggestions: {last_type, but_last_types} =
acceptable_types
|> Enum.map(&inspect/1)
|> List.pop_at(-1)
but_last = Enum.join(but_last_types, ", ")
"#{but_last} or #{last_type}" or alternatively: defp acceptable_type_str([type]) do
inspect(type)
end
defp acceptable_type_str([type, last_type]) do
"#{inspect(type)} or #{inspect(last_type)}"
end
defp acceptable_type_str([type | tail]) do
"#{inspect(type)}, " <> acceptable_type_string(tail)
end
defp acceptable_type_str(type) do
inspect(type)
end or: defp acceptable_type_str([type | tail]) do
"#{inspect(type)}" <>
case tail do
[] -> ""
[last] -> " or #{inspect(last)}"
more -> ", " <> acceptable_type_str(more)
end
end
defp acceptable_type_str(type) do
inspect(type)
end I might have a went a bit overboard here... 😄 |
||
end | ||
defp acceptable_type_str(acceptable_type), do: inspect(acceptable_type) | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
defmodule TypeTest do | ||
use ExUnit.Case | ||
|
||
defmodule Dummy do | ||
defstruct [:value] | ||
end | ||
|
||
test "simple types" do | ||
port = Port.list |> List.first | ||
valid_cases = [ | ||
{1, :any}, | ||
{"a", :any}, | ||
{1, :number}, | ||
{1, :integer}, | ||
{nil, nil}, | ||
{"a", :binary}, | ||
{"a", :bitstring}, | ||
{1.1, :float}, | ||
{1.1, :number}, | ||
{:foo, :atom}, | ||
{&self/0, :function}, | ||
{{1, 2}, :tuple}, | ||
{[1, 2], :list}, | ||
{%{a: 1}, :map}, | ||
{self, :pid}, | ||
{make_ref, :reference}, | ||
{port, :port}, | ||
{1, [:binary, :integer]}, | ||
{nil, [nil, :integer]}, | ||
{"a", [:binary, :atom]}, | ||
{:a, [:binary, :atom]}, | ||
{"hello", :string}, | ||
{~r/foo/, Regex}, | ||
{%Dummy{}, Dummy} | ||
] | ||
invalid_cases = [ | ||
{1, :binary}, | ||
{1, :float}, | ||
{1, nil}, | ||
{nil, :atom}, | ||
{1.1, :integer}, | ||
{self, :reference}, | ||
{{1, 2}, :list}, | ||
{{1, 2}, :map}, | ||
{[1, 2], :tuple}, | ||
{%{a: 2}, :list}, | ||
{<<239, 191, 191>>, :string}, | ||
{~r/foo/, :string}, | ||
{:a, [:binary, :integer]} | ||
] | ||
|
||
run_cases(valid_cases, invalid_cases) | ||
end | ||
|
||
test "complex types" do | ||
valid_cases = [ | ||
{&self/0, function: 0}, | ||
{[1, 2,], list: :integer}, | ||
{[1, 2, nil], list: [nil, :number]}, | ||
{[a: 1, b: 2], list: [tuple: {:atom, :number}]}, | ||
{%{:a => "a", "b" => nil}, map: {[:atom, :binary], [:binary, nil]}} | ||
] | ||
invalid_cases = [ | ||
{[a: 1, b: "a"], list: [tuple: {:atom, :number}]}, | ||
{%{1 => "a", "b" => 1}, map: {[:atom, :binary], [:binary, :integer]}}, | ||
{%{:a => 1.1, "b" => 1}, map: {[:atom, :binary], [:binary, :integer]}} | ||
] | ||
run_cases(valid_cases, invalid_cases) | ||
end | ||
|
||
test "deeply nested type" do | ||
valid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, 3]], "b" => nil}]]}} | ||
invalid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, "3"]]}]]}} | ||
other_invalid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, nil]]}]]}} | ||
type = [map: {:atom, [map: {:integer, [list: [tuple: {:atom, [list: [map: {:binary, [nil, [list: [tuple: {:atom, [list: [:integer, :float]]}]]]}]]}]]}]}] | ||
run_cases([{valid_value, type}], [{invalid_value, type}, {other_invalid_value, type}]) | ||
end | ||
|
||
test "default message" do | ||
expected = [{:error, :foo, :type, "must be of type :integer"}] | ||
assert Vex.errors(%{foo: "bar"}, foo: [type: [is: :integer]]) == expected | ||
expected = [{:error, :foo, :type, "must be of type :atom, :string or :list"}] | ||
assert Vex.errors(%{foo: 1}, foo: [type: [is: [:atom, :string, :list]]]) == expected | ||
expected = [{:error, :foo, :type, "value 1 is not a string"}] | ||
message = "value <%= value %> is not a string" | ||
assert Vex.errors(%{foo: 1}, foo: [type: [is: :string, message: message]]) == expected | ||
end | ||
|
||
defp run_cases(valid_cases, invalid_cases) do | ||
Enum.each valid_cases, fn {value, type} -> | ||
assert Vex.valid?([foo: value], foo: [type: [is: type]]) | ||
end | ||
|
||
Enum.each invalid_cases, fn {value, type} -> | ||
refute Vex.valid?([foo: value], foo: [type: [is: type]]) | ||
end | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a test that covers the error message output? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed |
||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should
implies that it's merely recommended to be the same size but the validator enforces them to be the same. I would suggestmust
.