Skip to content

Commit a893201

Browse files
authored
Merge pull request #27 from ExpressApp/typespecs
add typespecs generation
2 parents c8f9702 + eb69edb commit a893201

8 files changed

Lines changed: 226 additions & 16 deletions

File tree

.dialyzer_ignore.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
# fix for `Function ExUnit.CaseTemplate.__proxy__/2 does not exist.`
3+
{"", :unknown_function, 0},
4+
# fix for `The pattern can never match the type.`
5+
{"lib/construct.ex", :pattern_match, 447},
6+
]

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
# Where 3rd-party dependencies like ExDoc output generated docs.
1515
/doc
1616

17+
# Plts
18+
/priv/*.plt
19+
/priv/*.plt.hash
20+
1721
# If the VM crashes, it generates a dump, let's ignore it too.
1822
erl_crash.dump
1923

lib/construct.ex

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ defmodule Construct do
6161

6262
unquote(pre_ast)
6363

64-
@type t :: %__MODULE__{}
65-
6664
def make(params \\ %{}, opts \\ []) do
6765
Construct.Cast.make(__MODULE__, params, Keyword.merge(opts, unquote(opts)))
6866
end
@@ -104,7 +102,8 @@ defmodule Construct do
104102

105103
Module.eval_quoted __ENV__, {:__block__, [], [
106104
Construct.__defstruct__(@construct_fields, @construct_fields_enforce),
107-
Construct.__types__(@fields)]}
105+
Construct.__types__(@fields),
106+
Construct.__typespecs__(@fields)]}
108107
end
109108
end
110109

@@ -114,7 +113,7 @@ defmodule Construct do
114113
If included structure is invalid for some reason — this macro throws an
115114
`Struct.DefinitionError` exception with detailed reason.
116115
"""
117-
@spec include(t) :: :ok
116+
@spec include(t) :: Macro.t()
118117
defmacro include(struct) do
119118
quote do
120119
module = unquote(struct)
@@ -154,7 +153,7 @@ defmodule Construct do
154153
155154
By default this option is unset. Notice that you can't use functions as a default value.
156155
"""
157-
@spec field(atom, Construct.Type.t, Keyword.t) :: :ok
156+
@spec field(atom, Construct.Type.t, Keyword.t) :: Macro.t()
158157
defmacro field(name, type \\ :string, opts \\ [])
159158
defmacro field(name, opts, [do: _] = contents) do
160159
make_nested_field(name, contents, opts)
@@ -176,7 +175,7 @@ defmodule Construct do
176175
@doc """
177176
Alias to `c:make/2`, but raises an `Construct.MakeError` exception if params have errors.
178177
"""
179-
@callback make!(params :: map, opts :: Keyword.t) :: {:ok, t} | {:error, term}
178+
@callback make!(params :: map, opts :: Keyword.t) :: t
180179

181180
@doc """
182181
Alias to `c:make/2`, used to follow `c:Construct.Type.cast/1` callback.
@@ -264,6 +263,42 @@ defmodule Construct do
264263
end
265264
end
266265

266+
@doc false
267+
def __typespecs__(fields) do
268+
typespecs =
269+
Enum.map(fields, fn({name, type, opts}) ->
270+
type = Construct.Type.spec(type)
271+
272+
type =
273+
case Keyword.fetch(opts, :default) do
274+
{:ok, default} ->
275+
typeof_default = Construct.Type.typeof(default)
276+
277+
if type == typeof_default do
278+
type
279+
else
280+
quote do: unquote(type) | unquote(typeof_default)
281+
end
282+
283+
:error ->
284+
type
285+
end
286+
287+
{name, type}
288+
end)
289+
290+
modulespec =
291+
{:%, [],
292+
[
293+
{:__MODULE__, [], Elixir},
294+
{:%{}, [], typespecs}
295+
]}
296+
297+
quote do
298+
@type t :: unquote(modulespec)
299+
end
300+
end
301+
267302
@doc false
268303
def __field__(mod, name, type, opts) do
269304
check_field_name!(name)

lib/construct/exceptions.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ end
55
defmodule Construct.MakeError do
66
defexception [:message]
77

8-
@spec exception(struct_error | String.t | term) :: struct
9-
when struct_error: %{reason: map, params: map}
108
def exception(%{reason: reason, params: params}) when is_map(reason) do
119
%__MODULE__{message: inspect(traverse_errors(reason, params))}
1210
end

lib/construct/type.ex

Lines changed: 169 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ defmodule Construct.Type do
121121
:error
122122
123123
"""
124-
@spec cast(t, term, options) :: cast_ret
124+
@spec cast(t, term, options) :: cast_ret | any
125125
when options: [make_map: boolean]
126126

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

157157
def cast(type, term)
158158

@@ -336,10 +336,7 @@ defmodule Construct.Type do
336336
defp cast_naive_datetime(%{} = map) do
337337
with {:ok, date} <- cast_date(map),
338338
{:ok, time} <- cast_time(map) do
339-
case NaiveDateTime.new(date, time) do
340-
{:ok, _} = ok -> ok
341-
{:error, _} -> :error
342-
end
339+
NaiveDateTime.new(date, time)
343340
end
344341
end
345342
defp cast_naive_datetime(_) do
@@ -377,6 +374,172 @@ defmodule Construct.Type do
377374
end
378375
end
379376

377+
## Typespecs
378+
379+
@doc """
380+
Returns typespec AST for given type
381+
382+
iex> spec([CommaList, {:array, :integer}]) |> Macro.to_string()
383+
"list(:integer)"
384+
385+
iex> spec({:array, :string}) |> Macro.to_string()
386+
"list(String.t())"
387+
388+
iex> spec({:map, CustomType}) |> Macro.to_string()
389+
"%{optional(term) => CustomType.t()}"
390+
391+
iex> spec(:string) |> Macro.to_string()
392+
"String.t()"
393+
394+
iex> spec(CustomType) |> Macro.to_string()
395+
"CustomType.t()"
396+
"""
397+
@spec spec(t) :: Macro.t()
398+
399+
def spec(type) when is_list(type) do
400+
type |> List.last() |> spec()
401+
end
402+
403+
def spec({:array, type}) do
404+
quote do
405+
list(unquote(spec(type)))
406+
end
407+
end
408+
409+
def spec({:map, type}) do
410+
quote do
411+
%{optional(term) => unquote(spec(type))}
412+
end
413+
end
414+
415+
def spec(:string) do
416+
quote do
417+
String.t()
418+
end
419+
end
420+
421+
def spec(:decimal) do
422+
quote do
423+
Decimal.t()
424+
end
425+
end
426+
427+
def spec(:utc_datetime) do
428+
quote do
429+
DateTime.t()
430+
end
431+
end
432+
433+
def spec(:naive_datetime) do
434+
quote do
435+
NaiveDateTime.t()
436+
end
437+
end
438+
439+
def spec(:date) do
440+
quote do
441+
Date.t()
442+
end
443+
end
444+
445+
def spec(:time) do
446+
quote do
447+
Time.t()
448+
end
449+
end
450+
451+
def spec(type) when type in @builtin do
452+
type
453+
end
454+
455+
def spec(type) when is_atom(type) do
456+
quote do
457+
unquote(type).t()
458+
end
459+
end
460+
461+
def spec(type) do
462+
type
463+
end
464+
465+
@doc """
466+
Returns typespec AST for given term
467+
468+
iex> typeof(nil) |> Macro.to_string()
469+
"nil"
470+
471+
iex> typeof(1.42) |> Macro.to_string()
472+
"float()"
473+
474+
iex> typeof("string") |> Macro.to_string()
475+
"String.t()"
476+
477+
iex> typeof(CustomType) |> Macro.to_string()
478+
"CustomType.t()"
479+
480+
iex> typeof(&NaiveDateTime.utc_now/0) |> Macro.to_string()
481+
"NaiveDateTime.t()"
482+
"""
483+
@spec spec(t) :: Macro.t()
484+
485+
def typeof(term) when is_nil(term) do
486+
nil
487+
end
488+
489+
def typeof(term) when is_integer(term) do
490+
{:integer, [], []}
491+
end
492+
493+
def typeof(term) when is_float(term) do
494+
{:float, [], []}
495+
end
496+
497+
def typeof(term) when is_boolean(term) do
498+
{:boolean, [], []}
499+
end
500+
501+
def typeof(term) when is_binary(term) do
502+
quote do
503+
String.t()
504+
end
505+
end
506+
507+
def typeof(term) when is_pid(term) do
508+
{:pid, [], []}
509+
end
510+
511+
def typeof(term) when is_reference(term) do
512+
{:reference, [], []}
513+
end
514+
515+
def typeof(%{__struct__: struct}) when is_atom(struct) do
516+
quote do
517+
unquote(struct).t()
518+
end
519+
end
520+
521+
def typeof(term) when is_map(term) do
522+
{:map, [], []}
523+
end
524+
525+
def typeof(term) when is_atom(term) do
526+
quote do
527+
unquote(term).t()
528+
end
529+
end
530+
531+
def typeof(term) when is_list(term) do
532+
{:list, [], []}
533+
end
534+
535+
def typeof(term) when is_function(term, 0) do
536+
term.() |> typeof()
537+
end
538+
539+
def typeof(_) do
540+
{:term, [], []}
541+
end
542+
380543
## Helpers
381544

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

425-
defp map(_, _, _, _, _), do: :error
426-
427588
defp to_i(nil), do: nil
428589
defp to_i(int) when is_integer(int), do: int
429590
defp to_i(bin) when is_binary(bin) do

mix.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ defmodule Construct.Mixfile do
88
elixir: "~> 1.4",
99
deps: deps(),
1010
elixirc_paths: elixirc_paths(Mix.env),
11+
dialyzer: [
12+
plt_file: {:no_warn, "priv/dialyzer.plt"}
13+
],
1114

1215
# Hex
1316
description: description(),
@@ -32,6 +35,7 @@ defmodule Construct.Mixfile do
3235
[
3336
{:decimal, "~> 1.5", only: [:dev, :test]},
3437
{:benchfella, "~> 0.3", only: [:dev, :test]},
38+
{:dialyxir, "~> 1.0.0-rc.7", only: [:dev, :test], runtime: false},
3539
{:earmark, "~> 1.2", only: :dev},
3640
{:ex_doc, "~> 0.19", only: :dev},
3741
{:jason, "~> 1.1", only: :test}

mix.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
%{
22
"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"},
33
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
4+
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
45
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
6+
"erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"},
57
"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"},
68
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
79
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},

priv/.keep

Whitespace-only changes.

0 commit comments

Comments
 (0)