surgeventures/surgex

View on GitHub
lib/surgex/parser/parsers/sort_parser.ex

Summary

Maintainability
Test Coverage
defmodule Surgex.Parser.SortParser do
  @moduledoc """
  Parses the JSON API's sort parameter according to the
  [JSON API spec](http://jsonapi.org/format/#fetching-sorting).

  Produces a `{direction, column}` tuple, in which `direction` is either `:asc` or `:desc` and
  `column` is a safely atomized and underscored column name.
  """

  @doc false
  @spec call(term(), [atom]) :: {:ok, {:asc | :desc, atom} | nil} | {:error, :invalid_sort_column}
  def call(nil, _allowed_columns), do: {:ok, nil}
  def call("", _allowed_columns), do: {:ok, nil}

  def call(input, allowed_columns) when is_binary(input) do
    case input do
      "-" <> column ->
        validate_allowed_columns(column, allowed_columns, :desc)

      column ->
        validate_allowed_columns(column, allowed_columns, :asc)
    end
  end

  def call(_input, _), do: {:error, :invalid_sort_column}

  defp validate_allowed_columns(column, allowed_columns, direction) do
    column_atom = atomize_maybe_dasherized(column)

    if column_atom && column_atom in allowed_columns do
      {:ok, {direction, column_atom}}
    else
      {:error, :invalid_sort_column}
    end
  end

  defp atomize_maybe_dasherized(string) do
    atomize(string) ||
      string
      |> String.replace("-", "_")
      |> atomize
  end

  defp atomize(string) do
    String.to_existing_atom(string)
  rescue
    ArgumentError -> nil
  end

  @doc """
  Flattens the result of the parser (sort tuple) into `*_by` and `*_direction` keys.

  ## Examples

      iex> SortParser.flatten({:ok, sort: {:asc, :col}}, :sort)
      {:ok, sort_by: :col, sort_direction: :asc}

  """
  @spec flatten({:ok, Keyword.t()}, atom) :: {:ok, {:asc | :desc, atom}}
  def flatten({:ok, opts}, key) do
    case Keyword.pop(opts, key) do
      {nil, _} ->
        {:ok, opts}

      {{direction, column}, rem_opts} ->
        final_opts = Keyword.merge(rem_opts, sort_by: column, sort_direction: direction)

        {:ok, final_opts}
    end
  end

  def flatten(input, _key), do: input
end