lib/jexon/encoder.ex

defmodule Jexon.Encoder do
  @moduledoc """
  Responsible for encoding to JSON
  """

  @type json :: String.t()

  @spec encode(data :: any()) :: {:ok, json()} | {:error, Jason.EncodeError.t()}
  def encode(data) do
    data
    |> prepare_non_natives()
    |> Jason.encode()
  end

  defp prepare_non_natives(data) when is_struct(data) do
    struct = data.__struct__
    data
    |> Map.from_struct()
    |> Map.merge(%{"__atom__:__struct__" => struct})
    |> prepare_non_natives()
  end
  defp prepare_non_natives(data) when is_map(data) do
    Enum.map(data, fn
      {key, value} -> {prepare_key(key), prepare_non_natives(value)}
    end)
    |> Map.new
  end

  defp prepare_non_natives(data) when is_list(data) do
    Enum.map(data, & prepare_non_natives/1)
  end
  defp prepare_non_natives(data) when is_tuple(data) do
    prepared_data =
      data
      |> Tuple.to_list()
      |> prepare_non_natives()

    ~w(__tuple__) ++ prepared_data
  end
  defp prepare_non_natives(data) when is_atom(data) do
      ~w(__atom__) ++ [to_string(data)]
  end
  defp prepare_non_natives(data) do
    data
  end

  defp prepare_key(key) when is_atom(key) do
    "__atom__:#{key}"
  end

  defp prepare_key(key) when is_list(key) do
    "__list__:" <> (
      key
      |> Enum.map(& prepare_key/1)
      |> Enum.join(",")
    )
  end

  defp prepare_key(key) when is_tuple(key) do
    "__tuple__:" <> (
      key
      |> Tuple.to_list
      |> Enum.map(& prepare_key/1)
      |> Enum.join(",")
    )
  end

  defp prepare_key(key) when is_map(key) do
    raise "Maps as keys are not supported"
  end

  defp prepare_key(key) when is_struct(key) do
    raise "Structs as keys are not supported"
  end

  defp prepare_key(key) do
    key
  end
end
# {
#   \"__atom__:__struct__\":[\"__atom__\",\"Elixir.DateTime\"],
#   \"__atom__:calendar\":[\"__atom__\",\"Elixir.Calendar.ISO\"],
#   \"__atom__:day\":9,
#   \"__atom__:hour\":8,
#   \"__atom__:microsecond\":[\"__tuple__\",44490,6],
#   \"__atom__:minute\":24,
#   \"__atom__:month\":8,
#   \"__atom__:second\":14,
#   \"__atom__:std_offset\":0,
#   \"__atom__:time_zone\":\"Etc/UTC\",
#   \"__atom__:utc_offset\":0,
#   \"__atom__:year\":2023,
#   \"__atom__:zone_abbr\":\"UTC\"
# }