Skip to main content

lib/paysafe/param_encoder.ex

defmodule Paysafe.ParamEncoder do
  @moduledoc """
  Shared helpers for converting idiomatic Elixir param maps (snake_case atom
  keys) into the camelCase JSON shape the Paysafe API expects, with `nil`
  values stripped recursively.

  Used internally by every API module. Not typically called directly, but
  public so that custom extensions can reuse the same conventions.
  """

  @doc """
  Recursively convert snake_case atom (or string) keys to camelCase string keys.

  ## Examples

      iex> Paysafe.ParamEncoder.camelize_keys(%{card_expiry: %{month: 12, year: 2030}})
      %{"cardExpiry" => %{"month" => 12, "year" => 2030}}

  """
  @spec camelize_keys(term()) :: term()
  def camelize_keys(map) when is_map(map) and not is_struct(map) do
    Map.new(map, fn {k, v} -> {camelize(k), camelize_keys(v)} end)
  end

  def camelize_keys(list) when is_list(list), do: Enum.map(list, &camelize_keys/1)
  def camelize_keys(value), do: value

  @doc """
  Convert a single snake_case atom or string key to a camelCase string.

  ## Examples

      iex> Paysafe.ParamEncoder.camelize(:merchant_ref_num)
      "merchantRefNum"

  """
  @spec camelize(atom() | String.t()) :: String.t()
  def camelize(key) when is_atom(key), do: key |> Atom.to_string() |> camelize()

  def camelize(key) when is_binary(key) do
    case String.split(key, "_") do
      [single] ->
        single

      [first | rest] ->
        rest_camelized =
          Enum.map(rest, fn part ->
            {head, tail} = String.split_at(part, 1)
            String.upcase(head) <> tail
          end)

        Enum.join([first | rest_camelized])
    end
  end

  @doc """
  Recursively strip `nil` values from maps and lists, leaving the rest intact.

  ## Examples

      iex> Paysafe.ParamEncoder.deep_clean(%{"a" => 1, "b" => nil})
      %{"a" => 1}

  """
  @spec deep_clean(term()) :: term()
  def deep_clean(map) when is_map(map) and not is_struct(map) do
    map
    |> Enum.reject(fn {_k, v} -> is_nil(v) end)
    |> Enum.map(fn {k, v} -> {k, deep_clean(v)} end)
    |> Map.new()
  end

  def deep_clean(list) when is_list(list), do: Enum.map(list, &deep_clean/1)
  def deep_clean(value), do: value

  @doc """
  Convenience: camelize keys then strip nils in one pass.

  This is the function nearly every API module calls before sending a
  request body to `Paysafe.Client`.
  """
  @spec encode(map()) :: map()
  def encode(params) when is_map(params) do
    params
    |> camelize_keys()
    |> deep_clean()
  end

  @doc """
  Camelize keys of a keyword list (used for query string params).
  """
  @spec encode_query(keyword()) :: String.t()
  def encode_query(opts) do
    opts
    |> Enum.reject(fn {_k, v} -> is_nil(v) end)
    |> Enum.map(fn {k, v} -> {camelize(k), v} end)
    |> URI.encode_query()
  end
end