lib/plugs/pagination_params_validator.ex

defmodule Solicit.Plugs.Validation.PaginationParams do
  @moduledoc """
  Check to make sure for an endpoint that allows for pagination that both the `offset` and `page` query parameter
  cannot be provided in the same call. It can only be 1 or the other.

  Also that the provided `offset`, `page`, and `limit` are valid values
  """

  alias Plug.Conn
  alias Solicit.Response

  @spec init(keyword()) :: keyword()
  def init(opts \\ []), do: opts

  @spec call(Conn.t(), keyword()) :: Plug.Conn.t()
  def call(%{query_params: params} = conn, opts) do
    max_limit = get_max_limit(opts)
    offset = Map.get(params, "offset")
    page = Map.get(params, "page")
    limit = Map.get(params, "limit")

    if pagination_params_valid?(offset, page) do
      if is_valid_limit_value?(limit, max_limit) do
        conn
      else
        struct(conn,
          query_params: Map.put(conn.query_params, "limit", max_limit),
          params: Map.put(conn.params, "limit", max_limit)
        )
      end
    else
      Response.unprocessable_entity(conn)
    end
  end

  @spec pagination_params_valid?(any(), any()) :: boolean
  defp pagination_params_valid?(offset, page),
    do:
      (is_nil(offset) or is_nil(page)) and
        (is_valid_offset_value?(offset) and is_valid_page_value?(page))

  @spec is_valid_offset_value?(any) :: boolean()
  defp is_valid_offset_value?(offset_value) when not is_nil(offset_value) do
    case Integer.parse(offset_value) do
      {parsed_number, ""} ->
        parsed_number > -1

      :error ->
        false
    end
  end

  defp is_valid_offset_value?(_), do: true

  @spec is_valid_page_value?(any) :: boolean()
  defp is_valid_page_value?(page_value) when not is_nil(page_value) do
    case Integer.parse(page_value) do
      {parsed_number, ""} ->
        parsed_number > 0

      :error ->
        false
    end
  end

  defp is_valid_page_value?(_), do: true

  @spec is_valid_limit_value?(any(), pos_integer()) :: boolean()
  defp is_valid_limit_value?(limit, max_limit) when not is_nil(limit) do
    case Integer.parse(limit) do
      {parsed_number, ""} ->
        parsed_number <= max_limit and parsed_number > -1

      :error ->
        false
    end
  end

  defp is_valid_limit_value?(_, _), do: true

  @spec get_max_limit(keyword()) :: number()
  defp get_max_limit(opts),
    do: Keyword.get(opts, :max_limit, Application.get_env(:solicit, :pagination_max_limit, 1000))
end