lib/jsonterm/decoder.ex

defmodule Nap.JsonTerm.Decoder do
  use Nap.JsonTerm.Types

  def decode!(json) do
    [@json_term_type, json] = json
    do_decode(json)
  end

  defp do_decode(json)
       when is_number(json) or is_boolean(json) or is_nil(json) or is_binary(json),
       do: json

  defp do_decode([@list_type, json]), do: Enum.map(json, &do_decode/1)
  defp do_decode([@atom_type, atom]), do: String.to_atom(atom)
  defp do_decode([@tuple_type, tuple]), do: tuple |> Enum.map(&do_decode/1) |> List.to_tuple()
  defp do_decode([@binary_type, binary]), do: Base.decode64!(binary)
  defp do_decode([@date_type, date]), do: Date.from_iso8601!(date)
  defp do_decode([@time_type, time]), do: Time.from_iso8601!(time)
  defp do_decode([@naive_datetime_type, ndt]), do: NaiveDateTime.from_iso8601!(ndt)
  # defp do_decode([@decimal_type, decimal]), do: Decimal.new(decimal)
  defp do_decode([@set_type, set]), do: MapSet.new(set)
  defp do_decode([@range_type, [b, e]]), do: b..e

  defp do_decode([@datetime_type, dt]) do
    case DateTime.from_iso8601(dt) do
      {:ok, datetime, _offset} -> datetime
      {:error, error} -> raise error
    end
  end

  defp do_decode([@struct_type, struct, fields]) do
    struct = String.to_existing_atom(struct)
    fields = Enum.map(fields, fn {k, v} -> {String.to_atom(k), do_decode(v)} end)
    # struct!(struct, fields)
    ## we make struct handling less restrictive, to be able to serialize non-existing structs
    Enum.into(fields, %{}) |> Map.put(:__struct__, struct)
  end

  defp do_decode([@map_type, fields]) do
    Map.new(fields, fn {k, v} -> {String.to_atom(k), do_decode(v)} end)
  end

  defp do_decode([@dictionary_type, fields]) do
    Map.new(fields, fn [k, v] -> {do_decode(k), do_decode(v)} end)
  end

  defp do_decode([@term_type, term]) do
    term
    |> Base.decode64!()
    |> :erlang.binary_to_term()
  end
end