lib/forage/codec/encoder.ex

defmodule Forage.Codec.Encoder do
  @moduledoc """
  Functionality to encode a `Forage.Plan` into a Phoenix `param` map
  for use with the `ApplicationWeb.Router.Helpers`.
  """
  alias Forage.ForagePlan

  alias Forage.ForagePlan.{
    Sort,
    Pagination,
    Filter
  }

  @doc """
  Adds extra filters to a `params` map in the form
  of a list of pairs (either a list of the form `[{"key", value}]` or
  a keyword list `[key: value]`).

  This function trades explicitness for succintness.
  It only suppotrs `equal_to` filters.
  """
  def with_extra_filters(params, []), do: params

  def with_extra_filters(params, new_filters) do
    filters = Map.get(params, "_filter", %{})

    new_filters =
      Enum.reduce(new_filters, filters, fn {field, value}, current_filters ->
        stringified_field = to_string(field)
        updated_filters =
          Map.put(
            current_filters,
            stringified_field,
            %{"op" => "equal_to", "val" => value}
          )

          updated_filters
      end)

    Map.put(params, "_filter", new_filters)
  end

  @doc """
  Encodes a forage plan into a params map.

  This function doesn't need to take the schema as an argument
  because it will never have to convert a string into an atom
  (the params map contains only strings and never atoms)
  """
  def encode(%ForagePlan{} = plan) do
    # Each of the forage components (filter, sort and pagination) will be encoded as maps,
    # so that they can simply be merged together
    filter_map = encode_filter(plan)
    sort_map = encode_sort(plan)
    pagination_map = encode_pagination(plan)
    # Merge the three maps
    filter_map |> Map.merge(sort_map) |> Map.merge(pagination_map)
  end

  @doc """
  Encode the "filter" part of a forage plan. Returns a map.
  """
  def encode_filter(%ForagePlan{filter: %Filter{field: nil, operator: nil, value: nil}} = _plan) do
    %{}
  end

  def encode_filter(%ForagePlan{filter: filter} = _plan) do
    filter_value =
      for filter_filter <- filter, into: %{} do
        field_name =
          case filter_filter.field do
            {:simple, name} when is_atom(name) ->
              Atom.to_string(name)

            {:assoc, {_schema, local, remote}} when is_atom(local) and is_atom(remote) ->
              local_string = Atom.to_string(local)
              remote_string = Atom.to_string(remote)
              local_string <> "." <> remote_string
          end

        # Return key-value pair
        {field_name,
         %{
           "op" => filter_filter.operator,
           "val" => filter_filter.value
         }}
      end

    %{"_filter" => filter_value}
  end

  @doc """
  Encode the "sort" part of a forage plan. Returns a map.
  """
  def encode_sort(%ForagePlan{sort: %Sort{field: nil, direction: nil}} = _plan) do
    %{}
  end

  def encode_sort(%ForagePlan{sort: sort} = _plan) do
    sort_value =
      for sort_column <- sort, into: %{} do
        field_name = Atom.to_string(sort_column.field)
        direction_name = Atom.to_string(sort_column.direction)
        # Return key-value pair
        {field_name, %{"direction" => direction_name}}
      end

    %{"_sort" => sort_value}
  end

  @doc """
  Encode the "pagination" part of a forage plan. Returns a map.
  """
  def encode_pagination(%ForagePlan{pagination: %Pagination{} = pagination} = _plan) do
    encoded_after =
      case pagination.after do
        nil-> %{}
        value -> %{"after" => value}
      end

    encoded_before =
      case pagination.before do
        nil -> %{}
        value -> %{"before" => value}
      end

    Map.merge(encoded_after, encoded_before)
  end
end