From 3399d7ba69b8bb43e92b704bfbf153e6ee678675 Mon Sep 17 00:00:00 2001 From: Luper Rouch Date: Wed, 31 Aug 2022 18:25:49 +0200 Subject: [PATCH 1/2] feat: add parameters option Allows to add paramameters to the generated struct type. For example: typedstruct parameters: [a, b] do field :a, a field :b, b | nil field :c, integer() end Generates the type: @type t(a, b) :: %__MODULE__{ a: a, b: b | nil, c: integer() } --- lib/typed_struct.ex | 51 +++++++++++++++++++++++++++++++++++--- test/typed_struct_test.exs | 37 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/lib/typed_struct.ex b/lib/typed_struct.ex index 371b64a..d893d0a 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 + put_in(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 + put_in(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 From 74bc582515c2aeea63d0d37fa436f65530295926 Mon Sep 17 00:00:00 2001 From: Luper Rouch Date: Thu, 1 Sep 2022 08:54:47 +0200 Subject: [PATCH 2/2] fixup! feat: add parameters option Change examples because put_in() is not properly analyzed by dialyzer. --- lib/typed_struct.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/typed_struct.ex b/lib/typed_struct.ex index d893d0a..f004f5f 100644 --- a/lib/typed_struct.ex +++ b/lib/typed_struct.ex @@ -97,7 +97,7 @@ defmodule TypedStruct do @spec start_repair(t(new())) :: t(in_progress()) def start_repair(order) do - put_in(order.state, %{status: :in_progress, progress: 0}) + %{order | state: %{status: :in_progress, progress: 0}} end @spec advance_repair(t(in_progress()), integer()) :: t(in_progress()) @@ -107,7 +107,7 @@ defmodule TypedStruct do @spec finish_repair(t(in_progress())) :: t(completed()) def finish_repair(order) do - put_in(order.state, %{status: :completed}) + %{order | state: %{status: :completed}} end end