-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Daniel Perez
committed
Mar 27, 2016
1 parent
704c31b
commit 868947a
Showing
2 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |