View Source Derives the Enumerable for struct

We use the TypedStructor.Plugin.after_definition/2 callback to generate the Enumerable implementation for the struct. We implement Enumerable callbacks exclusively using the fields that are defined.

Let's start!

Implementation

defmodule Guides.Plugins.Enumerable do
  use TypedStructor.Plugin

  @impl TypedStructor.Plugin
  defmacro after_definition(definition, _opts) do
    quote bind_quoted: [definition: definition] do
      keys = Enum.map(definition.fields, &Keyword.fetch!(&1, :name))

      defimpl Enumerable do
        def count(enumerable), do: {:ok, Enum.count(unquote(keys))}
        def member?(enumerable, element), do: {:ok, Enum.member?(unquote(keys), element)}

        def reduce(enumerable, acc, fun) do
          # The order of fields is guaranteed to align with the sequence in which they are defined.
          unquote(keys)
          |> Enum.map(fn key -> {key, Map.fetch!(enumerable, key)} end)
          |> Enumerable.List.reduce(acc, fun)
        end

        # We don't support this
        def slice(_enumerable), do: {:error, __MODULE__}
      end
    end
  end
end

Usage

defmodule User do
  use TypedStructor

  typed_structor enforce: true do
    plugin Guides.Plugins.Enumerable

    field :name, String.t()
    field :age, Integer.t()
  end
end
iex> user = %User{name: "Phil", age: 20}
%User{name: "Phil", age: 20}
# the order of fields is deterministic
iex> Enum.map(user, fn {key, _value} -> key end)
[:name, :age]
# we got a deterministic ordered Keyword list
iex> Enum.to_list(user)
[name: "Phil", age: 20]

Bonus

Additionally, we gain the bonus of having a deterministic order of fields, determined by the sequence in which they are defined.