lib/attr_reader.ex

defmodule AttrReader do
  @moduledoc """
  Can define module attributes getter automatically.
  """
  # TODO: 1.20.0  ~ Module.reserved_attributes()に切り替え
  @reserved_attributes [
    :after_compile,
    :before_compile,
    :behaviour,
    :callback,
    :compile,
    :deprecated,
    :derive,
    :dialyzer,
    :doc,
    :enforce_keys,
    :external_resource,
    :file,
    :impl,
    :macrocallback,
    :moduledoc,
    :on_definition,
    :on_load,
    :opaque,
    :optional_callbacks,
    :spec,
    :type,
    :typedoc,
    :typep,
    :vsn
  ]

  @doc """
  Defines getters for all custom module attributes if used.
  And writes getter docs.
  ## Examples
      iex> defmodule UseAttrReaderForDoc do
      ...>   @foo "foo"
      ...>   use AttrReader
      ...>   @bar :bar
      ...> end
      iex> UseAttrReaderForDoc.foo()
      "foo"
      iex> UseAttrReaderForDoc.bar()
      :bar

      iex> defmodule UseAttrReaderForDoc do
      ...>   @foo "foo"
      ...>   use AttrReader, only: [:foo]
      ...>   @bar :bar
      ...> end
      iex> UseAttrReaderForDoc.foo()
      "foo"
      iex> UseAttrReaderForDoc.bar()
      ** (UndefinedFunctionError) function AttrReaderTest.UseAttrReaderForDoc.bar/0 is undefined or private

      iex> defmodule UseAttrReaderForDoc do
      ...>   @foo "foo"
      ...>   use AttrReader, except: [:foo]
      ...>   @bar :bar
      ...> end
      iex> UseAttrReaderForDoc.bar()
      :bar
      iex> UseAttrReaderForDoc.foo()
      ** (UndefinedFunctionError) function AttrReaderTest.UseAttrReaderForDoc.foo/0 is undefined or private
  """
  defmacro __using__(opts \\ []) do
    only = opts |> Keyword.get(:only)
    except = opts |> Keyword.get(:except)

    quote do
      @before_compile_opts [only: unquote(only), except: unquote(except)]
      @before_compile AttrReader
    end
  end

  defmacro __before_compile__(env) do
    opts = Module.get_attribute(env.module, :before_compile_opts)
    only = opts |> Keyword.get(:only)
    except = opts |> Keyword.get(:except)

    attributes =
      attributes_in(env.module)
      |> Enum.reject(&(&1 in (@reserved_attributes ++ [:before_compile_opts])))

    cond do
      only -> attributes |> Enum.filter(&(&1 in only))
      except -> attributes |> Enum.reject(&(&1 in except))
      true -> attributes
    end
    |> Enum.map(fn attribute ->
      quote do
        @doc """
        Gets @#{unquote(attribute)}.
        ## Examples
            iex> #{unquote(env.module)}.#{unquote(attribute)}()
            #{unquote(Module.get_attribute(env.module, attribute)) |> inspect()}
        """
        def unquote(attribute)() do
          unquote(Module.get_attribute(env.module, attribute))
        end
      end
    end)
  end

  @doc """
  Sets module attribute and defines getter.
  And writes getter docs.
  ## Examples
      iex> defmodule AttrReaderMacroForDoc do
      ...>   AttrReader.define @foo
      ...>   AttrReader.define @bar, "bar"
      ...>   AttrReader.define @baz, :baz
      ...> end
      iex> AttrReaderMacroForDoc.foo()
      nil
      iex> AttrReaderMacroForDoc.bar()
      "bar"
      iex> AttrReaderMacroForDoc.baz()
      :baz
  """
  defmacro define(attribute, value \\ nil) do
    [first | _] = elem(attribute, 2)
    attr_key = first |> elem(0)

    quote do
      @attar_value unquote(value)
      @doc """
      Gets @#{unquote(attr_key)}.
      ## Examples
          iex> #{unquote(__MODULE__)}.#{unquote(attr_key)}()
          #{unquote(value) |> inspect()}
      """
      def unquote(attr_key)() do
        @attar_value
      end
    end
  end

  defp attributes_in(module) when is_atom(module) do
    {set, _} = :elixir_module.data_tables(module)
    :ets.select(set, [{{:"$1", :_, :_}, [{:is_atom, :"$1"}], [:"$1"]}])
  end
end