lib/maverick.ex

defmodule Maverick do
  @external_resource "README.md"
  @moduledoc "README.md"
             |> File.read!()
             |> String.split("<!-- MDOC !-->")
             |> Enum.fetch!(1)

  @type api :: module()
  @type otp_app :: atom()
  @type root_scope :: String.t()

  defmacro __using__(opts) do
    scope = Keyword.get(opts, :scope, "")

    quote location: :keep do
      use Plug.Builder

      require Logger

      Module.register_attribute(__MODULE__, :maverick_routes, accumulate: true)
      Module.put_attribute(__MODULE__, :maverick_route_scope, unquote(scope))

      @on_definition Maverick
      @before_compile Maverick

      def call(%Plug.Conn{private: %{maverick_route: route}} = conn, _opts) do
        conn = super(conn, route)
        arg = Maverick.Api.Generator.decode_arg_type(conn, route.args)
        response = apply(__MODULE__, route.function, [arg])

        Maverick.handle_response(response, conn)
      end
    end
  end

  def __on_definition__(%Macro.Env{module: module}, :def, name, _args, _guards, _body) do
    route_info = Module.get_attribute(module, :route) || :no_route

    unless route_info == :no_route do
      scope = Module.get_attribute(module, :maverick_route_scope)
      path = Keyword.fetch!(route_info, :path)
      arg_type = Keyword.get(route_info, :args, :params) |> validate_arg_type()
      success_code = Keyword.get(route_info, :success, 200) |> parse_http_code()
      error_code = Keyword.get(route_info, :error, 404) |> parse_http_code()

      method =
        route_info
        |> Keyword.get(:method, "POST")
        |> to_string()
        |> String.upcase()

      raw_path =
        [scope, path]
        |> Enum.join("/")
        |> Maverick.Path.validate()

      path = Maverick.Path.parse(raw_path)

      Module.put_attribute(module, :maverick_routes, %Maverick.Route{
        module: module,
        function: name,
        args: arg_type,
        method: method,
        path: path,
        raw_path: raw_path,
        success_code: success_code,
        error_code: error_code
      })
    end

    Module.delete_attribute(module, :route)
  end

  def __on_definition__(env, _kind, _name, _args, _guards, _body) do
    route_info = Module.get_attribute(env.module, :route) || :no_route

    unless route_info == :no_route do
      Module.delete_attribute(env.module, :route)
    end
  end

  defmacro __before_compile__(env) do
    routes = Module.get_attribute(env.module, :maverick_routes, [])
    Module.delete_attribute(env.module, :maverick_routes)

    contents =
      quote do
        def routes() do
          unquote(Macro.escape(routes))
        end
      end

    env.module
    |> Module.concat(Maverick.Router)
    |> Module.create(contents, Macro.Env.location(__ENV__))

    []
  end

  def handle_response(%Plug.Conn{} = conn, _) do
    conn
  end

  def handle_response(term, conn) do
    response = Maverick.Response.handle(term, conn)
    handle_response(response, conn)
  end

  defp parse_http_code(code) when is_integer(code), do: code

  defp parse_http_code(code) when is_binary(code) do
    {code, _} = Integer.parse(code)
    code
  end

  defp validate_arg_type({:required_params, list}),
    do: {:required_params, Enum.map(list, &to_string/1)}

  defp validate_arg_type(:params), do: :params
  defp validate_arg_type(:conn), do: :conn
end