How to add a optional field to a defstruct depending on a opts from __using__ macro?

Hello, I want to create a struct State via defmacro __using__ that could have an optional_field depending on a option from use.

To do that, I can make it work this way:

defmodule State do
  defmacro __using__(opts) do
    optional_field = Keyword.get(opts, :optional_field, nil)

    quote do
        unquote(
        if optional_field == nil do
          quote do
            defstruct(field1: nil)
          end
        else
          quote do
            defstruct(field1: nil, optional_field: unquote(optional_field))
          end
        end)
    end
  end
end

defmodule NewState1 do
  use State, optional_field: %{}
end

defmodule NewState2 do
  use State
end

This way I get this when using these structs:

state1 = %NewState1{}
# %NewState1{field1: nil, optional_field: %{}}
state2 = %NewState2{}
# %NewState2{field1: nil}

This works great, but in the macro I need to repeat field1: nil in both defstruct. That’s ok in this case since it’s only this field, but if my struct is larger it would be harder to maintain.

So, my question is if there is another way to achieve this without duplicating the defstruct call, maybe some way to add a condition inside its call.

Ok, I found a solution myself:

defmodule State do
  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do
      %{name: name, value: value} = Keyword.get(opts, :optional_field, %{name: nil, value: nil})

      optional_field = if name != nil, do: Map.to_list(%{name => value}), else: []

      struct_fields = [field1: nil] ++ optional_field

      defstruct struct_fields
    end
  end

  def prepend_if_true(list, cond, extra) do
        if cond, do: extra ++ list, else: list
  end
end

defmodule NewState2 do
  use State
end

defmodule NewState1 do
  use State, optional_field: %{name: :field, value: %{}}
end

Now NewState1 will have a new field called :field with value %{}

1 Like