Skip to content

Commit

Permalink
Merge pull request #27 from ExpressApp/typespecs
Browse files Browse the repository at this point in the history
add typespecs generation
  • Loading branch information
artemeff authored Oct 31, 2019
2 parents c8f9702 + eb69edb commit a893201
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 16 deletions.
6 changes: 6 additions & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
# fix for `Function ExUnit.CaseTemplate.__proxy__/2 does not exist.`
{"", :unknown_function, 0},
# fix for `The pattern can never match the type.`
{"lib/construct.ex", :pattern_match, 447},
]
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc

# Plts
/priv/*.plt
/priv/*.plt.hash

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

Expand Down
47 changes: 41 additions & 6 deletions lib/construct.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ defmodule Construct do

unquote(pre_ast)

@type t :: %__MODULE__{}

def make(params \\ %{}, opts \\ []) do
Construct.Cast.make(__MODULE__, params, Keyword.merge(opts, unquote(opts)))
end
Expand Down Expand Up @@ -104,7 +102,8 @@ defmodule Construct do

Module.eval_quoted __ENV__, {:__block__, [], [
Construct.__defstruct__(@construct_fields, @construct_fields_enforce),
Construct.__types__(@fields)]}
Construct.__types__(@fields),
Construct.__typespecs__(@fields)]}
end
end

Expand All @@ -114,7 +113,7 @@ defmodule Construct do
If included structure is invalid for some reason — this macro throws an
`Struct.DefinitionError` exception with detailed reason.
"""
@spec include(t) :: :ok
@spec include(t) :: Macro.t()
defmacro include(struct) do
quote do
module = unquote(struct)
Expand Down Expand Up @@ -154,7 +153,7 @@ defmodule Construct do
By default this option is unset. Notice that you can't use functions as a default value.
"""
@spec field(atom, Construct.Type.t, Keyword.t) :: :ok
@spec field(atom, Construct.Type.t, Keyword.t) :: Macro.t()
defmacro field(name, type \\ :string, opts \\ [])
defmacro field(name, opts, [do: _] = contents) do
make_nested_field(name, contents, opts)
Expand All @@ -176,7 +175,7 @@ defmodule Construct do
@doc """
Alias to `c:make/2`, but raises an `Construct.MakeError` exception if params have errors.
"""
@callback make!(params :: map, opts :: Keyword.t) :: {:ok, t} | {:error, term}
@callback make!(params :: map, opts :: Keyword.t) :: t

@doc """
Alias to `c:make/2`, used to follow `c:Construct.Type.cast/1` callback.
Expand Down Expand Up @@ -264,6 +263,42 @@ defmodule Construct do
end
end

@doc false
def __typespecs__(fields) do
typespecs =
Enum.map(fields, fn({name, type, opts}) ->
type = Construct.Type.spec(type)

type =
case Keyword.fetch(opts, :default) do
{:ok, default} ->
typeof_default = Construct.Type.typeof(default)

if type == typeof_default do
type
else
quote do: unquote(type) | unquote(typeof_default)
end

:error ->
type
end

{name, type}
end)

modulespec =
{:%, [],
[
{:__MODULE__, [], Elixir},
{:%{}, [], typespecs}
]}

quote do
@type t :: unquote(modulespec)
end
end

@doc false
def __field__(mod, name, type, opts) do
check_field_name!(name)
Expand Down
2 changes: 0 additions & 2 deletions lib/construct/exceptions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ end
defmodule Construct.MakeError do
defexception [:message]

@spec exception(struct_error | String.t | term) :: struct
when struct_error: %{reason: map, params: map}
def exception(%{reason: reason, params: params}) when is_map(reason) do
%__MODULE__{message: inspect(traverse_errors(reason, params))}
end
Expand Down
177 changes: 169 additions & 8 deletions lib/construct/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ defmodule Construct.Type do
:error
"""
@spec cast(t, term, options) :: cast_ret
@spec cast(t, term, options) :: cast_ret | any
when options: [make_map: boolean]

def cast({:array, type}, term, opts) when is_list(term) do
Expand Down Expand Up @@ -152,7 +152,7 @@ defmodule Construct.Type do
@doc """
Behaves like `cast/3`, but without options provided to nested types.
"""
@spec cast(t, term) :: cast_ret
@spec cast(t, term) :: cast_ret | any

def cast(type, term)

Expand Down Expand Up @@ -336,10 +336,7 @@ defmodule Construct.Type do
defp cast_naive_datetime(%{} = map) do
with {:ok, date} <- cast_date(map),
{:ok, time} <- cast_time(map) do
case NaiveDateTime.new(date, time) do
{:ok, _} = ok -> ok
{:error, _} -> :error
end
NaiveDateTime.new(date, time)
end
end
defp cast_naive_datetime(_) do
Expand Down Expand Up @@ -377,6 +374,172 @@ defmodule Construct.Type do
end
end

## Typespecs

@doc """
Returns typespec AST for given type
iex> spec([CommaList, {:array, :integer}]) |> Macro.to_string()
"list(:integer)"
iex> spec({:array, :string}) |> Macro.to_string()
"list(String.t())"
iex> spec({:map, CustomType}) |> Macro.to_string()
"%{optional(term) => CustomType.t()}"
iex> spec(:string) |> Macro.to_string()
"String.t()"
iex> spec(CustomType) |> Macro.to_string()
"CustomType.t()"
"""
@spec spec(t) :: Macro.t()

def spec(type) when is_list(type) do
type |> List.last() |> spec()
end

def spec({:array, type}) do
quote do
list(unquote(spec(type)))
end
end

def spec({:map, type}) do
quote do
%{optional(term) => unquote(spec(type))}
end
end

def spec(:string) do
quote do
String.t()
end
end

def spec(:decimal) do
quote do
Decimal.t()
end
end

def spec(:utc_datetime) do
quote do
DateTime.t()
end
end

def spec(:naive_datetime) do
quote do
NaiveDateTime.t()
end
end

def spec(:date) do
quote do
Date.t()
end
end

def spec(:time) do
quote do
Time.t()
end
end

def spec(type) when type in @builtin do
type
end

def spec(type) when is_atom(type) do
quote do
unquote(type).t()
end
end

def spec(type) do
type
end

@doc """
Returns typespec AST for given term
iex> typeof(nil) |> Macro.to_string()
"nil"
iex> typeof(1.42) |> Macro.to_string()
"float()"
iex> typeof("string") |> Macro.to_string()
"String.t()"
iex> typeof(CustomType) |> Macro.to_string()
"CustomType.t()"
iex> typeof(&NaiveDateTime.utc_now/0) |> Macro.to_string()
"NaiveDateTime.t()"
"""
@spec spec(t) :: Macro.t()

def typeof(term) when is_nil(term) do
nil
end

def typeof(term) when is_integer(term) do
{:integer, [], []}
end

def typeof(term) when is_float(term) do
{:float, [], []}
end

def typeof(term) when is_boolean(term) do
{:boolean, [], []}
end

def typeof(term) when is_binary(term) do
quote do
String.t()
end
end

def typeof(term) when is_pid(term) do
{:pid, [], []}
end

def typeof(term) when is_reference(term) do
{:reference, [], []}
end

def typeof(%{__struct__: struct}) when is_atom(struct) do
quote do
unquote(struct).t()
end
end

def typeof(term) when is_map(term) do
{:map, [], []}
end

def typeof(term) when is_atom(term) do
quote do
unquote(term).t()
end
end

def typeof(term) when is_list(term) do
{:list, [], []}
end

def typeof(term) when is_function(term, 0) do
term.() |> typeof()
end

def typeof(_) do
{:term, [], []}
end

## Helpers

defp validate_decimal({:ok, %{__struct__: Decimal, coef: coef}}) when coef in [:inf, :qNaN, :sNaN],
Expand Down Expand Up @@ -422,8 +585,6 @@ defmodule Construct.Type do
{:ok, acc}
end

defp map(_, _, _, _, _), do: :error

defp to_i(nil), do: nil
defp to_i(int) when is_integer(int), do: int
defp to_i(bin) when is_binary(bin) do
Expand Down
4 changes: 4 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ defmodule Construct.Mixfile do
elixir: "~> 1.4",
deps: deps(),
elixirc_paths: elixirc_paths(Mix.env),
dialyzer: [
plt_file: {:no_warn, "priv/dialyzer.plt"}
],

# Hex
description: description(),
Expand All @@ -32,6 +35,7 @@ defmodule Construct.Mixfile do
[
{:decimal, "~> 1.5", only: [:dev, :test]},
{:benchfella, "~> 0.3", only: [:dev, :test]},
{:dialyxir, "~> 1.0.0-rc.7", only: [:dev, :test], runtime: false},
{:earmark, "~> 1.2", only: :dev},
{:ex_doc, "~> 0.19", only: :dev},
{:jason, "~> 1.1", only: :test}
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
%{
"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
Expand Down
Empty file added priv/.keep
Empty file.

0 comments on commit a893201

Please sign in to comment.