1
votes

I am trying to write helper macro that will allow me to write bunch of Elixir structures without lot of boilerplate, so I have written macro:

defmodule Events do
  @moduledoc false

  defmacro defevent(module, fields \\ []) do
    keys = Keyword.keys(fields)

    quote do
      defmodule unquote(module) do
        @type t :: %__MODULE__{unquote_splicing(fields)}

        defstruct unquote(keys)
      end
    end
  end
end

Which can be used as follows:

defmofule Event do
  import Events

  defevent Foo, foo: String.t(), bar: number()
end

However I would like to add possibility to add documentation to such module via @doc module attribute:

defmodule Events do
  @moduledoc false

  defmacro defevent(module, fields \\ []) do
    keys = Keyword.keys(fields)

    quote do
      docs = Module.delete_attribute(__MODULE__, :doc)

      defmodule unquote(module) do
        for doc <- docs do
          @moduledoc doc
        end

        @type t :: %__MODULE__{unquote_splicing(fields)}

        defstruct unquote(keys)
      end
    end
  end
end

However it will not work as Module.get_attribute(__MODULE__, :doc) returns either nil when there is no documentation or {integer(), String.t()} if there is docstring. However it miss all tags, so writing code like:

defmofule Event do
  import Events

  @doc "Foo"
  @doc deprecated: "Bar"
  defevent Foo, foo: String.t(), bar: number()
end

Will contain only "Foo" string without specified tags. My current workaround is using custom attribute, but this isn't prettiest solution out there as it requires non-standard parameter and defining custom module attribute (which makes me dependant on use).

defmodule Events do
  @moduledoc false

  defmacro __using__(_) do
    quote do
      import unquote(__MODULE__), only: [defevent: 1, defevent: 2]

      Module.register_attribute(__MODULE__, :eventdoc, accumulate: true)
    end
  end

  defmacro defevent(module, fields \\ []) do
    keys = Keyword.keys(fields)

    quote do
      docs = Module.delete_attribute(__MODULE__, :eventdoc)

      defmodule unquote(module) do
        for doc <- docs do
          @moduledoc doc
        end

        @type t :: %__MODULE__{unquote_splicing(fields)}

        defstruct unquote(keys)
      end
    end
  end
end

Is there any way to get values of the current @doc with all attributes assigned?

1

1 Answers

2
votes

Very biased humble opinion: there is no one single Elixir developer who did not try to enhance structs. They are cool, succinct and vivid as they are, and your code just overcomplicates things.

I believe, Elixir core team agrees with this point of view, that is why this functionality is not exposed. Whether you are brave enough to tackle it nevertheless, here you go.

Here is how documentation is built by Elixir. Please note the leading comment:

@doc false  
# Used internally to compile documentation.  
# This function is private and must be used only internally.

Are you still in? OK.

Digging into the Module.compile_definition_attributes/6 further, we get into Module.compile_doc_meta/5.

We are one step from the solution. Beware! Private functions area!

You are to reimplement Module.get_doc_meta/2 yourself:

case :ets.take(set, {:doc, :meta}) do
  [{{:doc, :meta}, metadata, _}] -> Map.merge(existing_meta, metadata)
  [] -> existing_meta
end

That said, you are to read the {:doc, :meta} key from ETS where set is defined here.