Skip to content

Commit cd0cd49

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

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

lib/vex/validators/type.ex

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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(1, is: :integer)
34+
:ok
35+
iex> Vex.Validators.Type.validate("foo"", is: :binary)
36+
:ok
37+
iex> Vex.Validators.Type.validate([1, 2, 3], is: [list: :integer])
38+
:ok
39+
iex> Vex.Validators.Type.validate(%{:a => 1, "b" => 2, 3 => 4}, is: :map)
40+
:ok
41+
iex> Vex.Validators.Type.validate(%{:a => 1, "b" => 2}, is: [map: {[:binary, :atom], :any}])
42+
:ok
43+
iex> Vex.Validators.Type.validate(%{"b" => 2, 3 => 4}, is: [map: {[:binary, :atom], :any}])
44+
{:error, "must be of type {:map, {[:binary, :atom], :any}}"}
45+
46+
## Custom Error Messages
47+
48+
Custom error messages (in EEx format), provided as :message, can use the following values:
49+
50+
iex> Vex.Validators.Type.__validator__(:message_fields)
51+
[value: "The bad value"]
52+
53+
An example:
54+
55+
iex> Vex.Validators.Type.validate([1], is: :binary, message: "<%= inspect value %> is not a string")
56+
{:error, "[1] is not a string"}
57+
"""
58+
use Vex.Validator
59+
60+
@message_fields [value: "The bad value"]
61+
def validate(value, options) when is_list(options) do
62+
unless_skipping(value, options) do
63+
acceptable_types = Keyword.get(options, :is, [])
64+
if do_validate(value, acceptable_types) do
65+
:ok
66+
else
67+
message = "must be of type #{acceptable_type_str(acceptable_types)}"
68+
{:error, message(options, message, value: value)}
69+
end
70+
end
71+
end
72+
73+
# Allow any type, useful for composed types
74+
defp do_validate(_value, :any), do: true
75+
76+
# Simple types
77+
defp do_validate(value, :atom) when is_atom(value), do: true
78+
defp do_validate(value, :number) when is_number(value), do: true
79+
defp do_validate(value, :integer) when is_integer(value), do: true
80+
defp do_validate(value, :float) when is_float(value), do: true
81+
defp do_validate(value, :binary) when is_binary(value), do: true
82+
defp do_validate(value, :bitstring) when is_bitstring(value), do: true
83+
defp do_validate(value, :tuple) when is_tuple(value), do: true
84+
defp do_validate(value, :list) when is_list(value), do: true
85+
defp do_validate(value, :map) when is_map(value), do: true
86+
defp do_validate(value, :function) when is_function(value), do: true
87+
defp do_validate(value, :reference) when is_reference(value), do: true
88+
defp do_validate(value, :port) when is_port(value), do: true
89+
defp do_validate(value, :pid) when is_pid(value), do: true
90+
91+
92+
# Complex types
93+
defp do_validate(value, function: arity) when is_function(value, arity), do: true
94+
95+
defp do_validate(list, list: type) when is_list(list) do
96+
Enum.all?(list, &(do_validate(&1, type)))
97+
end
98+
defp do_validate(value, map: {key_type, value_type}) when is_map(value) do
99+
Enum.all? value, fn {k, v} ->
100+
do_validate(k, key_type) && do_validate(v, value_type)
101+
end
102+
end
103+
defp do_validate(tuple, tuple: types)
104+
when is_tuple(tuple) and is_tuple(types) and tuple_size(tuple) == tuple_size(types) do
105+
Enum.all? Enum.zip(Tuple.to_list(tuple), Tuple.to_list(types)), fn {value, type} ->
106+
do_validate(value, type)
107+
end
108+
end
109+
110+
# Accept multiple types
111+
defp do_validate(value, acceptable_types) when is_list(acceptable_types) do
112+
Enum.any?(acceptable_types, &(do_validate(value, &1)))
113+
end
114+
115+
# Fail if nothing above matched
116+
defp do_validate(_value, _type), do: false
117+
118+
119+
defp acceptable_type_str([acceptable_type]), do: inspect(acceptable_type)
120+
defp acceptable_type_str(acceptable_types) when is_list(acceptable_types) do
121+
last_type = acceptable_types |> List.last |> inspect
122+
but_last =
123+
acceptable_types
124+
|> Enum.take(Enum.count(acceptable_types) - 1)
125+
|> Enum.map(&inspect/1)
126+
|> Enum.join(", ")
127+
"#{but_last} or #{last_type}"
128+
end
129+
defp acceptable_type_str(acceptable_type), do: inspect(acceptable_type)
130+
end

test/validations/type_test.exs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
defmodule TypeTest do
2+
use ExUnit.Case
3+
4+
test "simple types" do
5+
port = Port.list |> List.first
6+
valid_cases = [
7+
{1, :any},
8+
{"a", :any},
9+
{1, :number},
10+
{1, :integer},
11+
{"a", :binary},
12+
{"a", :bitstring},
13+
{1.1, :float},
14+
{1.1, :number},
15+
{:foo, :atom},
16+
{&self/0, :function},
17+
{{1, 2}, :tuple},
18+
{[1, 2], :list},
19+
{%{a: 1}, :map},
20+
{self, :pid},
21+
{make_ref, :reference},
22+
{port, :port},
23+
{1, [:binary, :integer]},
24+
{"a", [:binary, :atom]},
25+
{:a, [:binary, :atom]}
26+
]
27+
invalid_cases = [
28+
{1, :binary},
29+
{1, :float},
30+
{1.1, :integer},
31+
{self, :reference},
32+
{{1, 2}, :list},
33+
{{1, 2}, :map},
34+
{[1, 2], :tuple},
35+
{%{a: 2}, :list},
36+
{:a, [:binary, :integer]}
37+
]
38+
39+
run_cases(valid_cases, invalid_cases)
40+
end
41+
42+
test "complex types" do
43+
valid_cases = [
44+
{&self/0, function: 0},
45+
{[1, 2], list: :integer},
46+
{[1, 2], list: [:binary, :number]},
47+
{[a: 1, b: 2], list: [tuple: {:atom, :number}]},
48+
{%{:a => "a", "b" => 1}, map: {[:atom, :binary], [:binary, :integer]}}
49+
]
50+
invalid_cases = [
51+
{[a: 1, b: "a"], list: [tuple: {:atom, :number}]},
52+
{%{1 => "a", "b" => 1}, map: {[:atom, :binary], [:binary, :integer]}},
53+
{%{:a => 1.1, "b" => 1}, map: {[:atom, :binary], [:binary, :integer]}}
54+
]
55+
run_cases(valid_cases, invalid_cases)
56+
end
57+
58+
test "deeply nested type" do
59+
valid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, 3]]}]]}}
60+
invalid_value = %{a: %{1 => [a: [%{"a" => [a: [1, 2.2, "3"]]}]]}}
61+
type = [map: {:atom, [map: {:integer, [list: [tuple: {:atom, [list: [map: {:binary, [list: [tuple: {:atom, [list: [:integer, :float]]}]]}]]}]]}]}]
62+
run_cases([{valid_value, type}], [{invalid_value, type}])
63+
end
64+
65+
defp run_cases(valid_cases, invalid_cases) do
66+
Enum.each valid_cases, fn {value, type} ->
67+
assert Vex.valid?([foo: value], foo: [type: [is: type]])
68+
end
69+
70+
Enum.each invalid_cases, fn {value, type} ->
71+
refute Vex.valid?([foo: value], foo: [type: [is: type]])
72+
end
73+
end
74+
end

0 commit comments

Comments
 (0)