Skip to main content

lib/musubi/dsl/attr.ex

defmodule Musubi.DSL.Attr do
  @moduledoc "Compile-time `attr` declarations for parent-supplied Musubi assigns."

  @no_default {:__musubi_no_default__, __MODULE__}

  @doc """
  Returns the sentinel stored in attr metadata when no `default:` option was declared.

  ## Examples

      iex> Musubi.DSL.Attr.no_default()
      {:__musubi_no_default__, Musubi.DSL.Attr}
  """
  @spec no_default() :: term()
  def no_default, do: @no_default

  @doc """
  Declares a parent-supplied assign and stores its reflection metadata.

  Imported by `Musubi.Store`; metadata accumulates onto the `:__musubi_attrs__`
  module attribute and is exposed via `__musubi__(:attrs)` by
  `Musubi.Plugin.Reflection`.

  ## Examples

      defmodule ExampleStore do
        use Musubi.Store

        attr :title, String.t(), required: true
      end
  """
  defmacro attr(name, type, opts \\ []) do
    required = Keyword.get(opts, :required, false)
    validate_required!(name, required)

    default =
      if Keyword.has_key?(opts, :default) do
        Keyword.fetch!(opts, :default)
      else
        @no_default
      end

    metadata =
      Macro.escape(%{
        name: name,
        type: type,
        required: required,
        default: default
      })

    quote do
      @__musubi_attrs__ unquote(metadata)
    end
  end

  defp validate_required!(name, required) when is_atom(name) and is_boolean(required), do: :ok

  defp validate_required!(name, required) do
    raise ArgumentError,
          "attr/3 expects an atom name and boolean :required option, got name=#{inspect(name)} required=#{inspect(required)}"
  end
end