Skip to content

Commit

Permalink
Add type validator.
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Perez committed Mar 27, 2016
1 parent 704c31b commit 868947a
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
142 changes: 142 additions & 0 deletions lib/vex/validators/type.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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"]
def validate(value, options) when is_list(options) do
acceptable_types = Keyword.get(options, :is, [])
if do_validate(value, acceptable_types) do
: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

# Complex types
defp do_validate(value, :string) when is_binary(value) do
String.valid?(value)
end

defp do_validate(value, :regex) do
Regex.regex?(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}"
end
defp acceptable_type_str(acceptable_type), do: inspect(acceptable_type)
end
88 changes: 88 additions & 0 deletions test/validations/type_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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

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
end

0 comments on commit 868947a

Please sign in to comment.