|
| 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 |
0 commit comments