Skip to content

Commit d1b98ad

Browse files
author
Daniel Perez
committed
Add type validator.
1 parent 704c31b commit d1b98ad

2 files changed

Lines changed: 242 additions & 0 deletions

File tree

lib/vex/validators/type.ex

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
defmodule Vex.Validators.Type do
2+
@moduledoc """
3+
Ensure the value has the correct type.
4+
5+
The type can be provided in the following form:
6+
7+
* `type`: An atom representing the type.
8+
It can be any of the `TYPE` in Elixir `is_TYPE` functions.
9+
`:any` is treated as a special case and accepts any type.
10+
* `[type]`: A list of types as described above. When a list is passed,
11+
the value will be valid if it any of the types in the list.
12+
* `type: inner_type`: Type should be either `map`, `list`, `tuple`, or `function`.
13+
The usage are as follow
14+
15+
* `function: arity`: checks if the function has the correct arity.
16+
* `map: {key_type, value_type}`: checks keys and value in the map with the provided types.
17+
* `list: type`: checks every element in the list for the given types.
18+
* `tuple: {type_a, type_b}`: check each element of the tuple with the provided types,
19+
the types tuple should be the same size as the tuple itself.
20+
21+
## Options
22+
23+
* `:is`: Required. The type of the value, in the format described above.
24+
* `:message`: Optional. A custom error message. May be in EEx format
25+
and use the fields described in "Custom Error Messages," below.
26+
27+
## Examples
28+
29+
iex> Vex.Validators.Type.validate(1, is: :binary)
30+
{:error, "must be of type :binary"}
31+
iex> Vex.Validators.Type.validate(1, is: :number)
32+
:ok
33+
iex> Vex.Validators.Type.validate(nil, is: nil)
34+
:ok
35+
iex> Vex.Validators.Type.validate(1, is: :integer)
36+
:ok
37+
iex> Vex.Validators.Type.validate("foo"", is: :binary)
38+
:ok
39+
iex> Vex.Validators.Type.validate([1, 2, 3], is: [list: :integer])
40+
:ok
41+
iex> Vex.Validators.Type.validate(%{:a => 1, "b" => 2, 3 => 4}, is: :map)
42+
:ok
43+
iex> Vex.Validators.Type.validate(%{:a => 1, "b" => 2}, is: [map: {[:binary, :atom], :any}])
44+
:ok
45+
iex> Vex.Validators.Type.validate(%{"b" => 2, 3 => 4}, is: [map: {[:binary, :atom], :any}])
46+
{:error, "must be of type {:map, {[:binary, :atom], :any}}"}
47+
48+
## Custom Error Messages
49+
50+
Custom error messages (in EEx format), provided as :message, can use the following values:
51+
52+
iex> Vex.Validators.Type.__validator__(:message_fields)
53+
[value: "The bad value"]
54+
55+
An example:
56+
57+
iex> Vex.Validators.Type.validate([1], is: :binary, message: "<%= inspect value %> is not a string")
58+
{:error, "[1] is not a string"}
59+
"""
60+
use Vex.Validator
61+
62+
@message_fields [value: "The bad value"]
63+
64+
@doc """
65+
Validates the value against the given type.
66+
See the module documentation for more info.
67+
"""
68+
@spec validate(any, Keyword.t) :: :ok | {:error, String.t}
69+
def validate(value, options) when is_list(options) do
70+
acceptable_types = Keyword.get(options, :is, [])
71+
if do_validate(value, acceptable_types) do
72+
:ok
73+
else
74+
message = "must be of type #{acceptable_type_str(acceptable_types)}"
75+
{:error, message(options, message, value: value)}
76+
end
77+
end
78+
79+
# Allow any type, useful for composed types
80+
defp do_validate(_value, :any), do: true
81+
82+
# Handle nil
83+
defp do_validate(nil, nil), do: true
84+
defp do_validate(nil, :atom), do: false
85+
86+
# Simple types
87+
defp do_validate(value, :atom) when is_atom(value), do: true
88+
defp do_validate(value, :number) when is_number(value), do: true
89+
defp do_validate(value, :integer) when is_integer(value), do: true
90+
defp do_validate(value, :float) when is_float(value), do: true
91+
defp do_validate(value, :binary) when is_binary(value), do: true
92+
defp do_validate(value, :bitstring) when is_bitstring(value), do: true
93+
defp do_validate(value, :tuple) when is_tuple(value), do: true
94+
defp do_validate(value, :list) when is_list(value), do: true
95+
defp do_validate(value, :map) when is_map(value), do: true
96+
defp do_validate(value, :function) when is_function(value), do: true
97+
defp do_validate(value, :reference) when is_reference(value), do: true
98+
defp do_validate(value, :port) when is_port(value), do: true
99+
defp do_validate(value, :pid) when is_pid(value), do: true
100+
defp do_validate(%{__struct__: module}, module), do: true
101+
102+
# Complex types
103+
defp do_validate(value, :string) when is_binary(value) do
104+
String.valid?(value)
105+
end
106+
107+
defp do_validate(value, function: arity) when is_function(value, arity), do: true
108+
109+
defp do_validate(list, list: type) when is_list(list) do
110+
Enum.all?(list, &(do_validate(&1, type)))
111+
end
112+
defp do_validate(value, map: {key_type, value_type}) when is_map(value) do
113+
Enum.all? value, fn {k, v} ->
114+
do_validate(k, key_type) && do_validate(v, value_type)
115+
end
116+
end
117+
defp do_validate(tuple, tuple: types)
118+
when is_tuple(tuple) and is_tuple(types) and tuple_size(tuple) == tuple_size(types) do
119+
Enum.all? Enum.zip(Tuple.to_list(tuple), Tuple.to_list(types)), fn {value, type} ->
120+
do_validate(value, type)
121+
end
122+
end
123+
124+
# Accept multiple types
125+
defp do_validate(value, acceptable_types) when is_list(acceptable_types) do
126+
Enum.any?(acceptable_types, &(do_validate(value, &1)))
127+
end
128+
129+
# Fail if nothing above matched
130+
defp do_validate(_value, _type), do: false
131+
132+
133+
defp acceptable_type_str([acceptable_type]), do: inspect(acceptable_type)
134+
defp acceptable_type_str(acceptable_types) when is_list(acceptable_types) do
135+
last_type = acceptable_types |> List.last |> inspect
136+
but_last =
137+
acceptable_types
138+
|> Enum.take(Enum.count(acceptable_types) - 1)
139+
|> Enum.map(&inspect/1)
140+
|> Enum.join(", ")
141+
"#{but_last} or #{last_type}"
142+
end
143+
defp acceptable_type_str(acceptable_type), do: inspect(acceptable_type)
144+
end

test/validations/type_test.exs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
defmodule TypeTest do
2+
use ExUnit.Case
3+
4+
defmodule Dummy do
5+
defstruct [:value]
6+
end
7+
8+
test "simple types" do
9+
port = Port.list |> List.first
10+
valid_cases = [
11+
{1, :any},
12+
{"a", :any},
13+
{1, :number},
14+
{1, :integer},
15+
{nil, nil},
16+
{"a", :binary},
17+
{"a", :bitstring},
18+
{1.1, :float},
19+
{1.1, :number},
20+
{:foo, :atom},
21+
{&self/0, :function},
22+
{{1, 2}, :tuple},
23+
{[1, 2], :list},
24+
{%{a: 1}, :map},
25+
{self, :pid},
26+
{make_ref, :reference},
27+
{port, :port},
28+
{1, [:binary, :integer]},
29+
{nil, [nil, :integer]},
30+
{"a", [:binary, :atom]},
31+
{:a, [:binary, :atom]},
32+
{"hello", :string},
33+
{~r/foo/, Regex},
34+
{%Dummy{}, Dummy}
35+
]
36+
invalid_cases = [
37+
{1, :binary},
38+
{1, :float},
39+
{1, nil},
40+
{nil, :atom},
41+
{1.1, :integer},
42+
{self, :reference},
43+
{{1, 2}, :list},
44+
{{1, 2}, :map},
45+
{[1, 2], :tuple},
46+
{%{a: 2}, :list},
47+
{<<239, 191, 191>>, :string},
48+
{~r/foo/, :string},
49+
{:a, [:binary, :integer]}
50+
]
51+
52+
run_cases(valid_cases, invalid_cases)
53+
end
54+
55+
test "complex types" do
56+
valid_cases = [
57+
{&self/0, function: 0},
58+
{[1, 2,], list: :integer},
59+
{[1, 2, nil], list: [nil, :number]},
60+
{[a: 1, b: 2], list: [tuple: {:atom, :number}]},
61+
{%{:a => "a", "b" => nil}, map: {[:atom, :binary], [:binary, nil]}}
62+
]
63+
invalid_cases = [
64+
{[a: 1, b: "a"], list: [tuple: {:atom, :number}]},
65+
{%{1 => "a", "b" => 1}, map: {[:atom, :binary], [:binary, :integer]}},
66+
{%{:a => 1.1, "b" => 1}, map: {[:atom, :binary], [:binary, :integer]}}
67+
]
68+
run_cases(valid_cases, invalid_cases)
69+
end
70+
71+
test "deeply nested type" do
72+
valid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, 3]], "b" => nil}]]}}
73+
invalid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, "3"]]}]]}}
74+
other_invalid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, nil]]}]]}}
75+
type = [map: {:atom, [map: {:integer, [list: [tuple: {:atom, [list: [map: {:binary, [nil, [list: [tuple: {:atom, [list: [:integer, :float]]}]]]}]]}]]}]}]
76+
run_cases([{valid_value, type}], [{invalid_value, type}, {other_invalid_value, type}])
77+
end
78+
79+
test "default message" do
80+
expected = [{:error, :foo, :type, "must be of type :integer"}]
81+
assert Vex.errors(%{foo: "bar"}, foo: [type: [is: :integer]]) == expected
82+
expected = [{:error, :foo, :type, "must be of type :atom, :string or :list"}]
83+
assert Vex.errors(%{foo: 1}, foo: [type: [is: [:atom, :string, :list]]]) == expected
84+
expected = [{:error, :foo, :type, "value 1 is not a string"}]
85+
message = "value <%= value %> is not a string"
86+
assert Vex.errors(%{foo: 1}, foo: [type: [is: :string, message: message]]) == expected
87+
end
88+
89+
defp run_cases(valid_cases, invalid_cases) do
90+
Enum.each valid_cases, fn {value, type} ->
91+
assert Vex.valid?([foo: value], foo: [type: [is: type]])
92+
end
93+
94+
Enum.each invalid_cases, fn {value, type} ->
95+
refute Vex.valid?([foo: value], foo: [type: [is: type]])
96+
end
97+
end
98+
end

0 commit comments

Comments
 (0)