lib/web3/types/bytes.ex

defmodule Web3.Type.Bytes do
  @moduledoc """
  Fixed length binary, 1 to 32 bytes.
  """

  use Ecto.Type

  defstruct [
    :bytes,
    :size
  ]

  @type t :: %__MODULE__{
          bytes: binary(),
          size: 1..32
        }

  @impl Ecto.Type
  @spec type() :: :bytea
  def type, do: :bytea

  @impl Ecto.Type
  @spec load(term()) :: {:ok, t()} | :error
  def load(<<"0x", hex_string::bytes()>>), do: cast_hex(hex_string)
  def load(<<bytes::bytes()>>), do: {:ok, %__MODULE__{bytes: bytes, size: byte_size(bytes)}}
  def load(_), do: :error

  @impl Ecto.Type
  @spec dump(t()) :: {:ok, binary()} | :error
  def dump(%__MODULE__{bytes: <<bytes::bytes()>>}), do: {:ok, bytes}
  def dump(_), do: :error

  @impl Ecto.Type
  @spec cast(term()) :: {:ok, t()} | :error
  def cast(<<"0x", hex_string::bytes()>>), do: cast_hex(hex_string)
  def cast(<<bytes::bytes()>>), do: {:ok, %__MODULE__{bytes: bytes, size: byte_size(bytes)}}
  def cast(%__MODULE__{bytes: <<_::bytes()>>, size: size} = term) when size in 1..32, do: {:ok, term}
  def cast(_term), do: :error

  defp cast_hex(hex_string) do
    case Base.decode16(hex_string, case: :mixed) do
      {:ok, bytes} ->
        {:ok, %__MODULE__{bytes: bytes, size: byte_size(bytes)}}

      _ ->
        :error
    end
  end

  def to_string(%__MODULE__{bytes: nil}) do
    "0x"
  end

  def to_string(%__MODULE__{bytes: <<bytes::bytes()>>}) do
    "0x" <> Base.encode16(bytes, case: :lower)
  end

  def to_inspect(%{size: size} = bytes) do
    "Bytes#{size}<#{__MODULE__.to_string(bytes)}>"
  end

  defimpl String.Chars do
    def to_string(bytes) do
      @for.to_string(bytes)
    end
  end

  defimpl Inspect do
    def inspect(bytes, _opts) do
      @for.to_inspect(bytes)
    end
  end

  defimpl Jason.Encoder do
    alias Jason.Encode

    def encode(bytes, opts) do
      bytes
      |> to_string()
      |> Encode.string(opts)
    end
  end
end