From 868947a107594bf6d0c243e35ef62337c38148f0 Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Sun, 27 Mar 2016 16:03:57 +0900 Subject: [PATCH] Add type validator. --- lib/vex/validators/type.ex | 142 +++++++++++++++++++++++++++++++++ test/validations/type_test.exs | 88 ++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 lib/vex/validators/type.ex create mode 100644 test/validations/type_test.exs diff --git a/lib/vex/validators/type.ex b/lib/vex/validators/type.ex new file mode 100644 index 0000000..d6c952b --- /dev/null +++ b/lib/vex/validators/type.ex @@ -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 diff --git a/test/validations/type_test.exs b/test/validations/type_test.exs new file mode 100644 index 0000000..6e50b28 --- /dev/null +++ b/test/validations/type_test.exs @@ -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