lib/line_drive/structable.ex

defmodule LineDrive.Structable do
  @moduledoc """
  This module can be imported/used by other modules that have structs defined
  and need to process the input of a map or nested maps in order to construct
  the struct(s). It provides helper fucntions to atomize the map keys from
  expected struct fields (without blanket atomizing everything in the input
  map). It also provides some date/time and other utility funtions for parsing
  data out of maps built from json responses.
  """

  defmacro __using__(_) do
    quote do
      import LineDrive.Structable

      def new(map), do: new_from_map(map)

      def new_from_map(map) when is_map(map) do
        map
        |> atomize_keys()
        |> handle_transform(map)
        |> then(&struct(__MODULE__, &1))
      end

      def new_from_map(nil), do: nil

      defp atomize_keys(map) do
        struct_keys()
        |> Enum.reduce(%{}, fn key, acc ->
          Map.put(
            acc,
            key,
            Map.get_lazy(map, key, fn -> Map.get(map, Atom.to_string(key), nil) end)
          )
        end)
      end

      defp struct_keys do
        Map.keys(__MODULE__.__struct__())
        |> List.delete(:__struct__)
      end

      defp parse_date(date_str) when is_binary(date_str) do
        case Date.from_iso8601(date_str) do
          {:ok, date} -> date
          _ -> nil
        end
      end

      defp parse_date(date), do: date

      defp parse_datetime(nil), do: nil

      defp parse_datetime(date_str) do
        case NaiveDateTime.from_iso8601(date_str) do
          {:ok, date} -> date
          _ -> nil
        end
      end

      defp parse_time(val) when is_binary(val) do
        [hour, minute, second] =
          val
          |> String.split(":")
          |> Enum.map(&String.to_integer/1)

        case Time.new(hour, minute, second) do
          {:ok, date} -> date
          _ -> nil
        end
      end

      defp parse_time(_), do: nil

      defp get_nested_value(nil, _key), do: nil

      defp get_nested_value(map, key), do: Map.get(map, key, nil)

      defp parse_integer(visible_to) when is_binary(visible_to), do: String.to_integer(visible_to)

      defp parse_integer(visible_to), do: visible_to

      defp handle_transform(atomized_map, _original_map), do: atomized_map

      defoverridable new: 1, handle_transform: 2
    end
  end
end