lib/maru/params/type_builder.ex

defmodule Maru.Params.TypeBuilder do
  defmacro __using__(options) do
    derive = Keyword.get(options, :derive, [])

    quote do
      require Protocol
      use Maru.Params.Builder
      Module.register_attribute(__MODULE__, :types, accumulate: true)
      import unquote(__MODULE__)
      @struct_derive unquote(derive)
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro type(module, do: block) do
    type = Maru.Params.Builder.expand_alias(module, __CALLER__)

    quote do
      unquote(block)
      @types {unquote(type), :type, Maru.Params.Builder.pop_params(__ENV__)}
    end
  end

  defmacro type(module, options, do: block) do
    type = Maru.Params.Builder.expand_alias(module, __CALLER__)
    struct? = options == :struct || Keyword.get(options, :struct, false)
    struct_or_type = (struct? && :struct) || :type

    quote do
      unquote(block)
      @types {unquote(type), unquote(struct_or_type), Maru.Params.Builder.pop_params(__ENV__)}
    end
  end

  defmacro __before_compile__(%Macro.Env{module: module}) do
    derive = Module.get_attribute(module, :struct_derive)

    module
    |> Module.get_attribute(:types)
    |> Enum.map(fn
      {type, :type, params} ->
        params_runtime = Enum.map(params, &Map.get(&1, :runtime))
        maru_type_module = Module.concat(Maru.Params.Types, type)

        quote do
          defmodule unquote(maru_type_module) do
            use Maru.Params.Type
            def parser_arguments, do: [:options]

            def parse(input, args) do
              options = Map.get(args, :options, [])

              unquote(params_runtime)
              |> Maru.Params.Runtime.parse_params(input, options)
              |> then(&{:ok, &1})
            end
          end
        end

      {type, :struct, params} ->
        params_runtime = Enum.map(params, &Map.get(&1, :runtime))
        maru_type_module = Module.concat(Maru.Params.Types, type)

        attributes =
          Enum.map(params, fn param -> param |> Map.get(:info) |> Keyword.get(:name) end)

        quote do
          defmodule unquote(type) do
            @derive unquote(derive)
            defstruct unquote(attributes)
          end

          defmodule unquote(maru_type_module) do
            use Maru.Params.Type
            def parser_arguments, do: [:options]

            def parse(input, args) do
              options = Map.get(args, :options, [])

              unquote(params_runtime)
              |> Maru.Params.Runtime.parse_params(input, options)
              |> then(&{:ok, struct(unquote(type), &1)})
            end
          end
        end
    end)
  end
end