lib/chainweb/cut.ex

defmodule Kadena.Chainweb.Cut do
  @moduledoc """
  `Cut` struct definition.
  """
  @behaviour Kadena.Chainweb.Type

  @type hashes :: map() | list()
  @type height :: non_neg_integer()
  @type id :: String.t()
  @type instance :: String.t()
  @type origin :: map() | nil
  @type weight :: String.t()
  @type chain_id :: 0..19 | String.t() | atom()
  @type error :: {:error, Keyword.t()}
  @type result :: t() | error()
  @type validation :: {:ok, map() | chain_id()} | error()

  @type t :: %__MODULE__{
          hashes: hashes(),
          height: height(),
          weight: weight(),
          origin: origin(),
          id: id(),
          instance: instance()
        }

  defstruct [:height, :weight, :id, :instance, :origin, hashes: %{}]

  @impl true
  def new(args \\ [])
  def new(attrs) when is_map(attrs), do: struct(%__MODULE__{}, attrs)
  def new(attrs) when is_list(attrs), do: struct(%__MODULE__{}, attrs)
  def new(_attrs), do: {:error, [args: :invalid_format]}

  @spec set_hashes(cut :: t(), hashes :: hashes()) :: result()
  def set_hashes(%__MODULE__{} = cut, %{} = hashes) do
    keys = Map.keys(hashes)
    validate_hashes(cut, keys, hashes)
  end

  def set_hashes(%__MODULE__{}, _hashes), do: {:error, [hashes: :not_a_map]}

  @spec add_hash(cut :: t(), chain_id :: chain_id(), hash :: map()) :: result()
  def add_hash(
        %__MODULE__{hashes: hashes} = cut,
        chain_id,
        %{hash: hash_value, height: height} = hash
      ) do
    with {:ok, chain_id} <- validate_chain_id(chain_id),
         true <- is_binary(hash_value),
         true <- height >= 0 do
      %{cut | hashes: Map.put(hashes, chain_id, hash)}
    else
      _ -> {:error, [args: :invalid]}
    end
  end

  def add_hash(%__MODULE__{}, _chain_id, _hash), do: {:error, [args: :invalid]}

  @spec remove_hash(cut :: t(), chain_id :: chain_id()) :: result()
  def remove_hash(%__MODULE__{hashes: hashes} = cut, chain_id) when chain_id in 0..19,
    do: %{cut | hashes: Map.delete(hashes, String.to_atom("#{chain_id}"))}

  def remove_hash(_cut, _chain_id), do: {:error, [chain_id: :invalid]}

  @spec set_height(cut :: t(), height :: height()) :: result()
  def set_height(%__MODULE__{} = cut, height) when is_integer(height), do: %{cut | height: height}
  def set_height(%__MODULE__{}, _height), do: {:error, [height: :not_an_integer]}

  @spec set_weight(cut :: t(), weight :: weight()) :: result()
  def set_weight(%__MODULE__{} = cut, weight) when is_binary(weight), do: %{cut | weight: weight}
  def set_weight(%__MODULE__{}, _weight), do: {:error, [weight: :not_a_string]}

  @spec set_origin(cut :: t(), origin :: origin()) :: result()
  def set_origin(%__MODULE__{} = cut, %{id: id, address: address} = origin) do
    with {:ok, _id} <- validate_origin_id(id),
         {:ok, _address} <- validate_origin_address(address) do
      %{cut | origin: origin}
    end
  end

  def set_origin(%__MODULE__{}, _origin), do: {:error, [origin: :not_a_map]}

  @spec set_id(cut :: t(), id :: id()) :: result()
  def set_id(%__MODULE__{} = cut, id) when is_binary(id), do: %{cut | id: id}
  def set_id(%__MODULE__{}, _id), do: {:error, [id: :not_a_string]}

  @spec set_instance(cut :: t(), instance :: instance()) :: result()
  def set_instance(%__MODULE__{} = cut, instance) when is_binary(instance),
    do: %{cut | instance: instance}

  def set_instance(%__MODULE__{}, _instance), do: {:error, [instance: :not_a_string]}

  @spec validate_chain_id(chain_id :: chain_id()) :: validation()
  defp validate_chain_id(chain_id) when is_atom(chain_id) do
    chain_id
    |> Atom.to_string()
    |> validate_chain_id()
  end

  defp validate_chain_id(chain_id) when is_binary(chain_id) do
    case String.match?(chain_id, ~r/^1?[0-9]$/) do
      true -> {:ok, String.to_atom(chain_id)}
      false -> {:error, [chain_id: :invalid]}
    end
  end

  defp validate_chain_id(chain_id) when chain_id in 0..19,
    do: {:ok, String.to_atom("#{chain_id}")}

  defp validate_chain_id(_chain_id), do: {:error, [chain_id: :invalid]}

  @spec validate_origin_id(id :: String.t()) :: validation()
  defp validate_origin_id(id) when is_binary(id), do: {:ok, id}
  defp validate_origin_id(_id), do: {:error, [id: :not_a_string]}

  @spec validate_origin_address(address :: map()) :: validation()
  defp validate_origin_address(%{hostname: hostname, port: port} = address)
       when is_binary(hostname) and port >= 0,
       do: {:ok, address}

  defp validate_origin_address(_address), do: {:error, [address: :invalid]}

  @spec validate_hashes(cut :: t(), list(), hashes :: hashes()) :: t() | error()
  defp validate_hashes(%__MODULE__{} = cut, [], _hashes), do: cut

  defp validate_hashes(%__MODULE__{} = cut, [key | rest], hashes) do
    cut
    |> add_hash(key, hashes[key])
    |> validate_hashes(rest, hashes)
  end

  defp validate_hashes({:error, reason}, _keys, _hashes), do: {:error, [hashes: reason]}
end