diff --git a/lib/typed_struct.ex b/lib/typed_struct.ex index 371b64a..f004f5f 100644 --- a/lib/typed_struct.ex +++ b/lib/typed_struct.ex @@ -35,6 +35,7 @@ defmodule TypedStruct do individual fields. * `opaque` - if set to true, creates an opaque type for the struct. * `module` - if set, creates the struct in a submodule named `module`. + * `parameters` - if set, adds these parameters to the generated `t()`. ## Examples @@ -74,6 +75,42 @@ defmodule TypedStruct do field :field_four, atom(), default: :hey end end + + You can also add type parameters: + + defmodule RepairOrder do + use TypedStruct + + @type new :: %{status: :new} + @type in_progress :: %{status: :in_progress, progress: integer()} + @type completed :: %{status: :completed} + + typedstruct parameters: [state] do + field :state, state() + customer: String.t() + end + + @spec new(String.t()) :: t(new()) + def new(customer) do + %{state: %{status: :new}, customer: customer} + end + + @spec start_repair(t(new())) :: t(in_progress()) + def start_repair(order) do + %{order | state: %{status: :in_progress, progress: 0}} + end + + @spec advance_repair(t(in_progress()), integer()) :: t(in_progress()) + def advance_repair(order, progress) do + put_in(order.state.progress, progress) + end + + @spec finish_repair(t(in_progress())) :: t(completed()) + def finish_repair(order) do + %{order | state: %{status: :completed}} + end + end + """ defmacro typedstruct(opts \\ [], do: block) do ast = TypedStruct.__typedstruct__(block, opts) @@ -116,13 +153,19 @@ defmodule TypedStruct do @doc false defmacro __type__(types, opts) do + type_parameters = Keyword.get(opts, :parameters, []) + if Keyword.get(opts, :opaque, false) do - quote bind_quoted: [types: types] do - @opaque t() :: %__MODULE__{unquote_splicing(types)} + quote bind_quoted: [types: types, type_parameters: type_parameters] do + @opaque t(unquote_splicing(type_parameters)) :: %__MODULE__{ + unquote_splicing(types) + } end else - quote bind_quoted: [types: types] do - @type t() :: %__MODULE__{unquote_splicing(types)} + quote bind_quoted: [types: types, type_parameters: type_parameters] do + @type t(unquote_splicing(type_parameters)) :: %__MODULE__{ + unquote_splicing(types) + } end end end diff --git a/test/typed_struct_test.exs b/test/typed_struct_test.exs index d5fc420..61f2cb8 100644 --- a/test/typed_struct_test.exs +++ b/test/typed_struct_test.exs @@ -60,9 +60,21 @@ defmodule TypedStructTest do end end + {:module, _name, bytecode_parameterized, _exports} = + defmodule ParameterizedTestStruct do + use TypedStruct + + typedstruct parameters: [a, b] do + field :a, a + field :b, b | nil + field :c, integer() + end + end + @bytecode bytecode @bytecode_opaque bytecode_opaque @bytecode_noalias bytecode_noalias + @bytecode_parameterized bytecode_parameterized # Standard struct name used when comparing generated types. @standard_struct_name TypedStructTest.TestStruct @@ -156,6 +168,31 @@ defmodule TypedStructTest do assert type1 == type2 end + test "generates parameterized types" do + # Define a second struct with the type expected for TestStruct. + {:module, _name, bytecode2, _exports} = + defmodule TestStruct4 do + defstruct [:a, :b, :c] + + @type t(a, b) :: %__MODULE__{ + a: a, + b: b | nil, + c: integer() + } + end + + # Get both types and standardise them (remove line numbers and rename + # the second struct with the name of the first one). + type1 = @bytecode_parameterized |> extract_first_type() |> standardise() + + type2 = + bytecode2 + |> extract_first_type() + |> standardise(TypedStructTest.TestStruct4) + + assert type1 == type2 + end + test "generates the struct in a submodule if `module: ModuleName` is set" do assert TestModule.Struct.__struct__() == %TestModule.Struct{field: nil} end