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