lib/on_flow/json_cdc.ex

defmodule OnFlow.JSONCDC do
  @moduledoc """
  Decodes the JSON-CDC event values into Elixir terms.
  """

  @byte_sizes ~w(8 16 32 64 256)
  @integer_types for(type <- ~w(UInt Int Word), size <- @byte_sizes, do: type <> size)
  @float_types ~w(Fix64 UFix64)
  @literal_types ~w(String Bool Address)
  @composite_types ~w(Struct Resource Event Contract Enum)

  # Initialize atoms
  Enum.map(@composite_types, &(String.downcase(&1) |> String.to_atom()))

  @type encodable() :: String.t() | %{required(String.t()) => nil | String.t()}
  @spec decode!(encodable()) :: term()
  def decode!(event) do
    case decode(event) do
      {:ok, decoded_event} -> decoded_event
      :error -> raise "Could not decode: #{inspect(event)}"
    end
  end

  @spec decode(encodable()) :: {:ok, term()} | :error
  def decode(json) when is_binary(json) do
    case Jason.decode(json) do
      {:ok, encodable} ->
        case decode(encodable) do
          :error -> :error
          decoded -> {:ok, decoded}
        end

      _ ->
        :error
    end
  end

  def decode(%{"value" => nil}), do: nil
  def decode(%{"type" => "Void"}), do: nil
  def decode(%{"type" => "Optional", "value" => value}), do: decode(value)

  def decode(%{"type" => type, "value" => value}) when type in @literal_types,
    do: value

  def decode(%{"type" => type, "value" => value}) when type in @integer_types,
    do: String.to_integer(value)

  def decode(%{"type" => type, "value" => value}) when type in @float_types,
    do: String.to_float(value)

  def decode(%{"type" => "Array", "value" => value}), do: Enum.map(value, &decode/1)

  def decode(%{"type" => "Dictionary", "value" => values}) do
    for %{"key" => key, "value" => value} <- values, into: %{} do
      {decode(key), decode(value)}
    end
  end

  def decode(%{"type" => type, "value" => value}) when type in @composite_types do
    # "Struct" -> :struct
    type = type |> String.downcase() |> String.to_existing_atom()
    %{"fields" => fields} = value

    fields =
      for %{"name" => name, "value" => value} <- fields, into: %{} do
        {name, decode(value)}
      end

    {type, %{value | "fields" => fields}}
  end

  def decode(%{"type" => "Path", "value" => %{"domain" => domain, "identifier" => identifier}}),
    do: {:path, domain, identifier}

  def decode(%{"type" => "Type", "value" => %{"staticType" => type}}),
    do: {:type, type}

  def decode(%{"type" => "Capability", "value" => value}) do
    %{"path" => path, "address" => address, "borrowType" => borrow_type} = value
    {:capability, %{path: path, address: address, borrow_type: borrow_type}}
  end

  def decode(_event) do
    :error
  end
end