Current section
Files
Jump to
Current section
Files
lib/optimal/type.ex
defmodule Ashton.Type do
@moduledoc """
Exposes functions for validating types, and determining if a value matches a type.
"""
@scalar_types [
:any,
:atom,
:binary,
:bitstring,
:boolean,
:float,
:function,
:int,
:integer,
:keyword,
:list,
:map,
nil,
:number,
:pid,
:port,
:reference,
:regex,
:string,
:struct,
:tuple
]
@spec validate_types(
types :: Keyword.t(),
field_name :: atom,
opts :: Keyword.t(),
schema :: Ashton.schema()
) :: [
{atom, String.t()}
]
def validate_types(types, field_name, _opts, _schema) do
Enum.reduce(types, [], fn field_and_type, errors ->
type =
case field_and_type do
{_field, type} -> type
_ -> :any
end
if valid_type?(type) do
errors
else
[{field_name, "No such Ashton type: #{inspect(type)}"}]
end
end)
end
@spec matches_type?(type :: term, value :: term) :: boolean
def matches_type?(types, value) when is_list(types),
do: Enum.any?(types, &matches_type?(&1, value))
def matches_type?(:any, _), do: true
def matches_type?({:keyword, value_type}, value) do
matches_type?(:keyword, value) and
Enum.all?(value, fn {_k, v} -> matches_type?(value_type, v) end)
end
def matches_type?({:list, type}, value) do
matches_type?(:list, value) and Enum.all?(value, &matches_type?(type, &1))
end
def matches_type?({:tuple, size, types}, value) when is_list(types) do
matches_type?(:tuple, value) and tuple_size(value) == size and
value |> Tuple.to_list() |> Enum.all?(&matches_type?(types, &1))
end
def matches_type?({:tuple, size, type}, value) do
types = List.duplicate(type, size)
matches_type?({:tuple, size, types}, value)
end
def matches_type?({:tuple, size}, value) when is_integer(size),
do: tuple_size(value) == size and matches_type?(:tuple, value)
def matches_type?({:tuple, types}, value) when is_list(types),
do: matches_type?({:tuple, tuple_size(value), types}, value)
def matches_type?({:tuple, types}, value) when is_tuple(types) do
if matches_type?(:tuple, value) and tuple_size(types) == tuple_size(value) do
value = Tuple.to_list(value)
types = Tuple.to_list(types)
[types, value]
|> List.zip()
|> Enum.reduce(true, fn {type, value}, acc ->
acc and matches_type?(type, value)
end)
else
false
end
end
def matches_type?({:tuple, type}, value) do
matches_type?(:tuple, value) and
value |> Tuple.to_list() |> Enum.all?(&matches_type?(type, &1))
end
def matches_type?({:function, arity}, value) when is_function(value, arity), do: true
def matches_type?({:function, _}, _), do: false
def matches_type?({:struct, struct}, %struct{}), do: true
def matches_type?({:struct, _}, _), do: false
def matches_type?(%struct{}, %struct{}), do: true
def matches_type?(%_{}, _), do: false
def matches_type?({:enum, list}, value), do: value in list
# Below this line is only scalar types. Do not move things below/above this line.
def matches_type?(type, _) when not (type in @scalar_types),
do: raise("Unreachable: no type #{inspect(type)}")
def matches_type?(:int, value) when is_integer(value), do: true
def matches_type?(:integer, value) when is_integer(value), do: true
def matches_type?(:bitstring, value) when is_bitstring(value), do: true
def matches_type?(:string, value) when is_binary(value), do: true
def matches_type?(:binary, value) when is_binary(value), do: true
def matches_type?(:float, value) when is_float(value), do: true
def matches_type?(:keyword, value), do: Keyword.keyword?(value)
def matches_type?(:list, value) when is_list(value), do: true
def matches_type?(:boolean, value) when is_boolean(value), do: true
def matches_type?(:atom, value) when is_atom(value), do: true
def matches_type?(nil, nil), do: true
def matches_type?(:function, value) when is_function(value), do: true
def matches_type?(:map, value) when is_map(value), do: true
def matches_type?(:number, value) when is_number(value), do: true
def matches_type?(:pid, value) when is_pid(value), do: true
def matches_type?(:port, value) when is_port(value), do: true
def matches_type?(:reference, value) when is_reference(value), do: true
def matches_type?(:tuple, value) when is_tuple(value), do: true
def matches_type?(:struct, %_{}), do: true
def matches_type?(:regex, %Regex{}), do: true
def matches_type?(_, _), do: false
@spec valid_type?(term) :: boolean
def valid_type?(:any), do: true
def valid_type?(types) when is_list(types), do: Enum.all?(types, &valid_type?/1)
def valid_type?({:function, i}) when is_integer(i) and i >= 0, do: true
def valid_type?({:keyword, type}), do: valid_type?(type)
def valid_type?({:list, type}), do: valid_type?(type)
def valid_type?({:struct, module}) when is_atom(module), do: true
def valid_type?({:enum, values}) when is_list(values), do: true
def valid_type?({:tuple, size, types})
when is_integer(size) and is_list(types) and length(types) == size,
do: Enum.all?(types, &valid_type?/1)
def valid_type?({:tuple, _size, type}), do: valid_type?(type)
def valid_type?({:tuple, size}) when is_integer(size) and size >= 0, do: true
def valid_type?({:tuple, types}) when is_list(types), do: Enum.all?(types, &valid_type?/1)
def valid_type?({:tuple, types}) when is_tuple(types),
do: types |> Tuple.to_list() |> Enum.all?(&valid_type?/1)
def valid_type?({:tuple, type}), do: valid_type?(type)
def valid_type?(%_{}), do: true
def valid_type?(type) when type in @scalar_types, do: true
def valid_type?(_), do: false
# There may be missing cases here.
@spec merge(left :: term, right :: term) :: term
def merge(same, same), do: same
def merge({:enum, left}, {:enum, right}), do: {:enum, left ++ right}
def merge(left, right), do: [left, right]
end