lib/tx_build/validations.ex

defmodule Stellar.TxBuild.Validations do
  @moduledoc """
  Ensures that child components/structures used by operations are properly initialized otherwise, returns a formatted error.
  """

  alias Stellar.TxBuild.{
    Account,
    AccountID,
    Amount,
    Asset,
    AssetsPath,
    Flags,
    ClaimableBalanceID,
    OptionalAccount,
    OptionalAccountID,
    OptionalFlags,
    OptionalWeight,
    OptionalSigner,
    OptionalString32,
    PoolID,
    Price,
    SequenceNumber,
    Signer,
    String32,
    Weight,
    SCAddress,
    SCVal,
    SCVec
  }

  @type account_id :: String.t()
  @type asset_code :: String.t()
  @type args :: list(SCVal.t())
  @type asset :: {asset_code(), account_id()} | Keyword.t() | atom()
  @type value :: account_id() | asset() | number() | args()
  @type component :: {atom(), value()}
  @type error :: Keyword.t() | atom()
  @type validation :: {:ok, any()} | {:error, error()}

  @spec is_struct?(term :: any(), name :: module()) :: boolean()
  def is_struct?(term, name) when is_struct(term) and is_atom(name), do: term.__struct__ == name
  def is_struct?(_term, _name), do: false

  @spec validate_pos_integer(component :: component()) :: validation()
  def validate_pos_integer({_field, number}) when is_integer(number) and number >= 0,
    do: {:ok, number}

  def validate_pos_integer({field, _number}), do: {:error, [{field, :integer_expected}]}

  @spec validate_account_id(component :: component()) :: validation()
  def validate_account_id({field, account_id}) do
    case AccountID.new(account_id) do
      %AccountID{} = account_id -> {:ok, account_id}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_account_id(component :: component()) :: validation()
  def validate_optional_account_id({_field, nil}), do: {:ok, OptionalAccountID.new()}

  def validate_optional_account_id({field, account_id}) do
    with {:ok, _account} <- validate_account_id({field, account_id}) do
      {:ok, OptionalAccountID.new(account_id)}
    end
  end

  @spec validate_account(component :: component()) :: validation()
  def validate_account({field, account}) do
    case Account.new(account) do
      %Account{} = account -> {:ok, account}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_account(component :: component()) :: validation()
  def validate_optional_account({_field, nil}), do: {:ok, OptionalAccount.new()}

  def validate_optional_account({field, account_id}) do
    with {:ok, _account} <- validate_account({field, account_id}) do
      {:ok, OptionalAccount.new(account_id)}
    end
  end

  @spec validate_asset(component :: component()) :: validation()
  def validate_asset({field, asset}) do
    case Asset.new(asset) do
      %Asset{} = asset -> {:ok, asset}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_assets_path(component :: component()) :: validation()
  def validate_optional_assets_path({_field, nil}), do: {:ok, AssetsPath.new()}

  def validate_optional_assets_path({field, assets}) do
    case AssetsPath.new(assets) do
      %AssetsPath{} = assets -> {:ok, assets}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_amount(component :: component()) :: validation()
  def validate_amount({field, amount}) do
    case Amount.new(amount) do
      %Amount{} = amount -> {:ok, amount}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_amount(component :: component()) :: validation()
  def validate_optional_amount({_field, nil}), do: {:ok, Amount.new(:max)}

  def validate_optional_amount({field, value}) do
    validate_amount({field, value})
  end

  @spec validate_price(component :: component()) :: validation()
  def validate_price({field, price}) do
    case Price.new(price) do
      %Price{} = price -> {:ok, price}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_claimable_balance_id(component :: component()) :: validation()
  def validate_claimable_balance_id({field, balance_id}) do
    case ClaimableBalanceID.new(balance_id) do
      %ClaimableBalanceID{} = claimable_balance_id -> {:ok, claimable_balance_id}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_pool_id(component :: component()) :: validation()
  def validate_pool_id({field, pool_id}) do
    case PoolID.new(pool_id) do
      %PoolID{} = pool_id -> {:ok, pool_id}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_flags(component :: component()) :: validation()
  def validate_optional_flags({_field, nil}), do: {:ok, OptionalFlags.new()}

  def validate_optional_flags({field, value}) do
    case Flags.new(value) do
      %Flags{} = flags -> {:ok, OptionalFlags.new(flags)}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_weight(component :: component()) :: validation()
  def validate_optional_weight({_field, nil}), do: {:ok, OptionalWeight.new()}

  def validate_optional_weight({field, value}) do
    case Weight.new(value) do
      %Weight{} = weight -> {:ok, OptionalWeight.new(weight)}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_string32(component :: component()) :: validation()
  def validate_optional_string32({_field, nil}), do: {:ok, OptionalString32.new()}

  def validate_optional_string32({field, value}) do
    case String32.new(value) do
      %String32{} = str -> {:ok, OptionalString32.new(str)}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_optional_signer(component :: component()) :: validation()
  def validate_optional_signer({_field, nil}), do: {:ok, OptionalSigner.new()}

  def validate_optional_signer({field, value}) do
    case Signer.new(value) do
      %Signer{} = signer -> {:ok, OptionalSigner.new(signer)}
      {:error, reason} -> {:error, [{field, reason}]}
    end
  end

  @spec validate_sc_vals(component :: component()) :: validation()
  def validate_sc_vals({field, args}) when is_list(args) do
    if Enum.all?(args, &is_struct?(&1, SCVal)),
      do: {:ok, args},
      else: {:error, :"invalid_#{field}"}
  end

  def validate_sc_vals({field, _args}), do: {:error, :"invalid_#{field}"}

  @spec validate_contract_id(component :: component()) :: validation()
  def validate_contract_id({field, contract_id}) when is_binary(contract_id) do
    case Base.decode16(contract_id, case: :lower) do
      {:ok, _} -> {:ok, contract_id}
      :error -> {:error, :"invalid_#{field}"}
    end
  end

  def validate_contract_id({field, _contract_id}), do: {:error, :"invalid_#{field}"}

  @spec validate_string(tuple()) :: validation()
  def validate_string({_type, string}) when is_binary(string), do: {:ok, string}
  def validate_string({type, _string}), do: {:error, :"invalid_#{type}"}

  @spec validate_sequence_number(tuple()) :: validation()
  def validate_sequence_number({_field, %SequenceNumber{} = value}),
    do: {:ok, value}

  def validate_sequence_number({field, _}), do: {:error, :"invalid_#{field}"}

  @spec validate_address(address :: SCAddress.t()) :: validation()
  def validate_address(%SCAddress{} = address), do: {:ok, address}

  def validate_address(_address), do: {:error, :invalid_address}

  @spec validate_vec(args :: SCVec.t()) :: validation()
  def validate_vec(%SCVec{} = args), do: {:ok, args}
  def validate_vec(_args), do: {:error, :invalid_args}
end