lib/horizon/mapping.ex

defmodule Stellar.Horizon.Mapping do
  @moduledoc """
  Takes a result map or list of maps from Horizon response and returns a struct
  (e.g. `%Horizon.Transaction{}`) or list of structs.
  """

  @type attr_value :: any()
  @type attr_type ::
          :integer
          | :float
          | :date_time
          | {:map, Keyword.t()}
          | {:struct, module()}
          | {:list, atom(), Keyword.t() | module()}

  @spec build(module :: struct(), attrs :: map()) :: struct()
  def build(module, attrs) do
    attrs =
      module
      |> Map.from_struct()
      |> Map.keys()
      |> (&Map.take(attrs, &1)).()

    struct!(module, attrs)
  end

  @spec parse(module :: struct(), mapping :: Keyword.t()) :: struct()
  def parse(module, []), do: module

  def parse(module, [{attr, type} | attrs]) do
    value = Map.get(module, attr)
    parsed_value = do_parse(type, value)

    module
    |> Map.put(attr, parsed_value)
    |> parse(attrs)
  end

  @spec do_parse(type :: attr_type(), value :: attr_value()) :: attr_value()
  defp do_parse(:integer, value) when is_bitstring(value), do: String.to_integer(value)

  defp do_parse(:float, value) when is_bitstring(value), do: String.to_float(value)

  defp do_parse(:date_time, value) when is_bitstring(value) do
    case DateTime.from_iso8601(value) do
      {:ok, date_time, _offset} -> date_time
      _error -> value
    end
  end

  defp do_parse({:map, mapping}, value) when is_map(value) do
    Enum.reduce(mapping, value, fn {key, type}, acc ->
      acc
      |> Map.get(key)
      |> (&do_parse(type, &1)).()
      |> (&Map.put(acc, key, &1)).()
    end)
  end

  defp do_parse({:struct, module}, value) when not is_nil(value), do: module.new(value)

  defp do_parse({:list, :map, mapping}, values) when is_list(values) do
    Enum.map(values, &do_parse({:map, mapping}, &1))
  end

  defp do_parse({:list, :struct, module}, values) when is_list(values) do
    Enum.map(values, &module.new(&1))
  end

  defp do_parse(_type, value), do: value
end