lib/plug.ex

defmodule Request.Validator.Plug do
  alias Plug.Conn
  alias Request.Validator

  import Plug.Conn

  @doc ~S"""
  Init the Request.Validator.Plug with an optional error callback
  and handlers with their corresponding request validator module.
  ```elixir
  plug Request.Validator.Plug,
    register: App.Requests.RegisterRequest,
    on_error: fn conn, errors -> json_resp(conn, "Handle your errors: #{inspect errors}") end
  ```
  """
  def init(opts) when is_map(opts), do: init(Keyword.new(opts))

  def init(opts) do
    opts
    |> Keyword.put_new(:on_error, &Validator.Plug.on_error/2)
  end

  @doc ~S"""
  The default callback to be invoked when there is a param that fails validation.
  """
  def on_error(conn, errors) do
    json_resp(conn, 422, %{message: "Unprocessable entity", errors: errors}) |> halt()
  end

  defp unauthorized(conn) do
    json_resp(conn, 403, %{message: "Forbidden"}) |> halt
  end

  @doc ~S"""
  Performs validations on `conn.params`
  If all validations are successful returns the connection struct
  Otherwise returns an error map in the following structure: `%{param: ["some error", ...]}`
  Will call the given `on_error` callback in case some validation failed
  """
  def call(conn, opts) do
    with action <- Map.get(conn.private, :phoenix_action),
         module <- get_validator(opts, action),
         false <- is_nil(module),
         {:authorized, true} <- {:authorized, module.authorize(conn)},
         :ok <- module.validate(Conn.fetch_query_params(conn)) do
      conn
    else
      {:authorized, false} ->
        unauthorized(conn)

      {:error, errors} when is_map(errors) ->
        opts[:on_error].(conn, errors)

      _ ->
        conn
    end
  end

  defp get_validator(opt, key) when is_map(opt), do: Map.get(opt, key)
  defp get_validator(opt, key) when is_list(opt), do: Keyword.get(opt, key)

  defp json_resp(conn, status, body) do
    conn
    |> put_resp_header("content-type", "application/json")
    |> send_resp(status, json_library().encode_to_iodata!(body))
  end

  defp json_library do
    Application.get_env(:request_validator, :json_library, Jason)
  end
end