Current section
Files
Jump to
Current section
Files
lib/hugs/type_def.ex
defmodule Hugs.TypeDef do
alias Hugs.StructDefinition
alias Hugs.FieldDefinition
alias Hugs.PropsDefinition
alias __MODULE__, as: T
@enforce_keys [:kind, :t]
defstruct [:kind, :t, :tk]
@builtin_types ~w(integer map binary atom boolean any)a
def builtin_types(), do: @builtin_types
defp _t(kind, t, rest \\ []) when is_list(rest) do
struct!(%T{kind: kind, t: t}, rest)
end
def from_spec(t) when t in @builtin_types, do: _t(:builtin, t)
def from_spec(:string), do: from_spec(:binary)
def from_spec({:list, t}), do: _t(:list, from_spec(t))
def from_spec({:enum, values}) when is_list(values), do: _t(:enum, values)
def from_spec({:map, t_key, t_val}), do: _t(:typed_map, from_spec(t_val), tk: from_spec(t_key))
def from_spec(%StructDefinition{} = s), do: s
def from_spec(%PropsDefinition{} = ps), do: ps
def from_spec(%FieldDefinition{} = f), do: f
def from_spec({:embed, module}) when is_atom(module), do: _t(:embed, module)
# struct without a type name will use %module{} typespec
def from_spec({:struct, module}) when is_atom(module), do: _t(:struct, module)
# struct with a specified typename will use that type name in typespecs
def from_spec({:struct, module, typename}) when is_atom(module) and is_atom(typename),
do: _t(:struct, module, tk: typename)
# A single module as type is considered as a hugs module that exports a t()
# type.
def from_spec(module) when is_atom(module) do
case Atom.to_string(module) do
"Elixir." <> _ ->
:ok
_other ->
IO.warn(
"using atom #{inspect(module)} as embedded module. It looks like it is not an Elixir module"
)
end
from_spec({:embed, module})
end
def value_typed?(%T{kind: :builtin, t: t}, value),
do: builtin_typed?(t, value)
def value_typed?(%T{kind: :embed, t: module}, value),
do: is_struct(value, module)
def value_typed?(%T{kind: :list, t: t}, value) do
is_list(value) and
Enum.all?(value, &value_typed?(t, &1))
end
# for typed maps, the function is only called to validate default values
def value_typed?(%T{kind: :typed_map, t: v_type, tk: k_type}, value) when is_map(value) do
Enum.all?(value, fn {k, v} -> value_typed?(v_type, v) && value_typed?(k_type, k) end)
end
defp builtin_typed?(:any, _), do: true
defp builtin_typed?(:integer, val) when is_integer(val), do: true
defp builtin_typed?(:map, val) when is_map(val), do: true
defp builtin_typed?(:atom, val) when is_atom(val), do: true
defp builtin_typed?(:string, val) when is_binary(val), do: true
defp builtin_typed?(:binary, val) when is_binary(val), do: true
defp builtin_typed?(:boolean, true), do: true
defp builtin_typed?(:boolean, false), do: true
defp builtin_typed?(_, _), do: false
def to_quoted_kvs(%Hugs.StructDefinition{props: props}),
do: to_quoted_kvs(props)
def to_quoted_kvs(%Hugs.PropsDefinition{fields: fields}) when map_size(fields) == 0 do
quote do
Keyword.t()
end
end
def to_quoted_kvs(%Hugs.PropsDefinition{fields: fields}) do
fields
|> Enum.map(fn {key, %FieldDefinition{type: t}} -> {key, to_quoted(t)} end)
|> Enum.sort()
|> :lists.reverse()
|> Enum.reduce("e(do: unquote(&1) | unquote(&2)))
end
@doc """
Returns the quoted for for internal types. Internal types refers to types
representation using tagged tuples.
"""
def to_quoted(type_def)
def to_quoted(%Hugs.StructDefinition{module: mod, props: props}) do
{:%, [],
[
mod,
# {:__aliases__, [alias: false], [mod]},
to_quoted(props)
]}
end
def to_quoted(%PropsDefinition{fields: fields}) do
map_fields =
Enum.map(fields, fn
{key, %FieldDefinition{optional: true} = field} -> {{:optional, key}, field}
{key, %FieldDefinition{optional: false} = field} -> {{:always, key}, field}
end)
fields =
Enum.map(map_fields, fn
{{:always, key}, tdef} ->
{key, to_quoted(tdef)}
{{:optional, key}, tdef} ->
{{:optional, [], [key]}, to_quoted(tdef)}
end)
{:%{}, [], fields}
end
def to_quoted(%FieldDefinition{type: t}),
do: to_quoted(t)
def to_quoted(%T{kind: :builtin, t: t}),
do: {t, [], :"Elixir"}
def to_quoted(%T{kind: :typed_map, t: t, tk: tk}),
do: {:%{}, [], [{{:optional, [], [to_quoted(tk)]}, to_quoted(t)}]}
def to_quoted(%T{kind: :list, t: t}),
do: [to_quoted(t)]
def to_quoted(%T{kind: :embed, t: module}),
do: {{:., [], [module, :t]}, [no_parens: false], []}
def to_quoted(%T{kind: :struct, t: module, tk: nil}),
do: {:%, [], [module, {:%{}, [], []}]}
def to_quoted(%T{kind: :struct, t: module, tk: tname}),
do: {{:., [], [module, tname]}, [], []}
def to_quoted(%T{kind: :enum, t: values, tk: nil}) when is_list(values) do
values = :lists.reverse(values)
case Enum.split_with(values, &is_atom/1) do
{atoms, []} ->
Enum.reduce(atoms, "e(do: unquote(&1) | unquote(&2)))
{atoms, other} ->
types =
other
|> Enum.map(&to_literal_spec/1)
|> Enum.uniq()
|> Enum.map(&literal_spec_as_quoted/1)
Enum.reduce(types ++ atoms, "e(do: unquote(&1) | unquote(&2)))
end
end
defp to_literal_spec(bin) when is_binary(bin), do: :binary
defp to_literal_spec(int) when is_integer(int), do: int
defp literal_spec_as_quoted(:binary), do: quote(do: binary())
defp literal_spec_as_quoted(int) when is_integer(int), do: int
end
defimpl String.Chars, for: Hugs.TypeDef do
def to_string(tdef) do
tdef
|> Hugs.TypeDef.to_quoted()
|> Macro.to_string()
end
end