Skip to content

Commit

Permalink
Soroban Compound Types (#54)
Browse files Browse the repository at this point in the history
* Soroban Compound Types

* Fix minor details

---------

Co-authored-by: Felipe Guzmán Sierra <[email protected]>
  • Loading branch information
Odraxs and FelipeGuzmanSierra authored May 11, 2023
1 parent 5cb5742 commit 3b007ed
Show file tree
Hide file tree
Showing 17 changed files with 713 additions and 1 deletion.
49 changes: 49 additions & 0 deletions lib/types/enum.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Soroban.Types.Enum do
@moduledoc """
`Enum` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Soroban.Types.Symbol
alias Stellar.TxBuild.SCVal

defstruct [:key, :value]

@type sc_val :: SCVal.t()
@type t :: %__MODULE__{key: String.t(), value: struct()}

@impl true
def new(key) when is_binary(key),
do: %__MODULE__{key: key}

def new({key, value}) when is_binary(key) and is_struct(value),
do: %__MODULE__{key: key, value: value}

def new(_args), do: {:error, :invalid}

@impl true
def to_sc_val(%__MODULE__{key: key, value: nil}) do
key
|> Symbol.new()
|> Symbol.to_sc_val()
|> (&SCVal.new(vec: [&1])).()
end

def to_sc_val(%__MODULE__{key: key, value: value}) do
value = param_to_sc_val(value)

key
|> Symbol.new()
|> Symbol.to_sc_val()
|> (&SCVal.new(vec: [&1, value])).()
end

def to_sc_val(_error), do: {:error, :invalid_struct_enum}

@spec param_to_sc_val(param :: struct()) :: sc_val()
defp param_to_sc_val(param) do
struct = param.__struct__
struct.to_sc_val(param)
end
end
42 changes: 42 additions & 0 deletions lib/types/map.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Soroban.Types.Map do
@moduledoc """
`Map` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Soroban.Types.MapEntry
alias Stellar.TxBuild.SCVal

defstruct [:values]

@type errors :: atom()
@type values :: list(MapEntry.t())
@type validation :: {:ok, values()} | {:error, errors()}
@type t :: %__MODULE__{values: values}

@impl true
def new(values) when is_list(values) do
with {:ok, values} <- validate_map_entry_values(values) do
%__MODULE__{values: values}
end
end

def new(_values), do: {:error, :invalid}

@impl true
def to_sc_val(%__MODULE__{values: values}) do
values
|> Enum.map(&MapEntry.to_sc_map_entry/1)
|> (&SCVal.new(map: &1)).()
end

def to_sc_val(_error), do: {:error, :invalid_struct_map}

@spec validate_map_entry_values(values :: values()) :: validation()
def validate_map_entry_values(values) do
if Enum.all?(values, &is_struct(&1, MapEntry)),
do: {:ok, values},
else: {:error, :invalid_values}
end
end
36 changes: 36 additions & 0 deletions lib/types/map_entry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Soroban.Types.MapEntry do
@moduledoc """
`MapEntry` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Stellar.TxBuild.{SCMapEntry, SCVal}

defstruct [:key, :value]

@type sc_val :: SCVal.t()
@type t :: %__MODULE__{key: struct(), value: struct()}

@impl true
def new({key, value}) when is_struct(key) and is_struct(value),
do: %__MODULE__{key: key, value: value}

def new(_args), do: {:error, :invalid}

@impl true
def to_sc_map_entry(%__MODULE__{key: key, value: value}) do
key = param_to_sc_val(key)
value = param_to_sc_val(value)

SCMapEntry.new(key, value)
end

def to_sc_map_entry(_error), do: {:error, :invalid_struct_map_entry}

@spec param_to_sc_val(param :: struct()) :: sc_val()
defp param_to_sc_val(param) do
struct = param.__struct__
struct.to_sc_val(param)
end
end
32 changes: 32 additions & 0 deletions lib/types/option.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Soroban.Types.Option do
@moduledoc """
`Option` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Stellar.TxBuild.SCVal

defstruct [:value]

@type sc_val :: SCVal.t()
@type value :: struct() | nil
@type t :: %__MODULE__{value: value()}

@impl true
def new(value \\ nil)
def new(nil), do: %__MODULE__{}
def new(value) when is_struct(value), do: %__MODULE__{value: value}
def new(_value), do: {:error, :invalid_option}

@impl true
def to_sc_val(%__MODULE__{value: nil}), do: SCVal.new(void: nil)
def to_sc_val(%__MODULE__{value: value}), do: param_to_sc_val(value)
def to_sc_val(_error), do: {:error, :invalid_struct_bool}

@spec param_to_sc_val(param :: struct()) :: sc_val()
defp param_to_sc_val(param) do
struct = param.__struct__
struct.to_sc_val(param)
end
end
6 changes: 5 additions & 1 deletion lib/types/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ defmodule Soroban.Types.Spec do
@moduledoc """
Defines base types constructions.
"""
alias Stellar.TxBuild.SCVal
alias Stellar.TxBuild.{SCMapEntry, SCVal}

@type error :: {:error, atom()}
@type sc_val :: SCVal.t()
@type map_entry :: SCMapEntry.t()

@callback new(any()) :: struct() | error()
@callback to_sc_val(struct()) :: sc_val()
@callback to_sc_map_entry(struct()) :: map_entry()

@optional_callbacks to_sc_val: 1, to_sc_map_entry: 1
end
42 changes: 42 additions & 0 deletions lib/types/struct.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Soroban.Types.Struct do
@moduledoc """
`Struct` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Soroban.Types.StructField
alias Stellar.TxBuild.SCVal

defstruct [:values]

@type errors :: atom()
@type values :: list(StructField.t())
@type validation :: {:ok, values()} | {:error, errors()}
@type t :: %__MODULE__{values: values}

@impl true
def new(values) when is_list(values) do
with {:ok, values} <- validate_struct_field_values(values) do
%__MODULE__{values: values}
end
end

def new(_values), do: {:error, :invalid}

@impl true
def to_sc_val(%__MODULE__{values: values}) do
values
|> Enum.map(&StructField.to_sc_map_entry/1)
|> (&SCVal.new(map: &1)).()
end

def to_sc_val(_error), do: {:error, :invalid_struct}

@spec validate_struct_field_values(values :: values()) :: validation()
def validate_struct_field_values(values) do
if Enum.all?(values, &is_struct(&1, StructField)),
do: {:ok, values},
else: {:error, :invalid_values}
end
end
40 changes: 40 additions & 0 deletions lib/types/struct_field.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Soroban.Types.StructField do
@moduledoc """
`StructField` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Soroban.Types.Symbol
alias Stellar.TxBuild.{SCMapEntry, SCVal}

defstruct [:key, :value]

@type key :: String.t()
@type sc_val :: SCVal.t()
@type t :: %__MODULE__{key: key(), value: struct()}

@impl true
def new({key, value}) when is_binary(key) and is_struct(value),
do: %__MODULE__{key: key, value: value}

def new(_args), do: {:error, :invalid}

@impl true
def to_sc_map_entry(%__MODULE__{key: key, value: value}) do
value = param_to_sc_val(value)

key
|> Symbol.new()
|> Symbol.to_sc_val()
|> SCMapEntry.new(value)
end

def to_sc_map_entry(_error), do: {:error, :invalid_struct_field}

@spec param_to_sc_val(param :: struct()) :: sc_val()
defp param_to_sc_val(param) do
struct = param.__struct__
struct.to_sc_val(param)
end
end
29 changes: 29 additions & 0 deletions lib/types/tuple.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule Soroban.Types.Tuple do
@moduledoc """
`Tuple` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Stellar.TxBuild.SCVal

defstruct [:values]

@type values :: list(struct())
@type errors :: atom()
@type t :: %__MODULE__{values: values()}

@impl true
def new(values) when is_list(values), do: %__MODULE__{values: values}

def new(_values), do: {:error, :invalid}

@impl true
def to_sc_val(%__MODULE__{values: values}) do
values
|> Enum.map(fn %{__struct__: struct} = arg -> struct.to_sc_val(arg) end)
|> (&SCVal.new(vec: &1)).()
end

def to_sc_val(_error), do: {:error, :invalid_struct_tuple}
end
41 changes: 41 additions & 0 deletions lib/types/vec.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule Soroban.Types.Vec do
@moduledoc """
`Vec` struct definition.
"""

@behaviour Soroban.Types.Spec

alias Stellar.TxBuild.SCVal

defstruct [:values]

@type values :: list(struct())
@type errors :: atom()
@type validation :: {:ok, values()} | {:error, errors()}
@type t :: %__MODULE__{values: values()}

@impl true
def new(values) when is_list(values) do
with {:ok, values} <- validate_vec_values(values) do
%__MODULE__{values: values}
end
end

def new(_values), do: {:error, :invalid}

@impl true
def to_sc_val(%__MODULE__{values: values}) do
values
|> Enum.map(fn %{__struct__: struct} = arg -> struct.to_sc_val(arg) end)
|> (&SCVal.new(vec: &1)).()
end

def to_sc_val(_error), do: {:error, :invalid_struct_vec}

@spec validate_vec_values(values :: values()) :: validation()
defp validate_vec_values([value | _] = values) do
if Enum.any?(values, fn val -> val.__struct__ != value.__struct__ end),
do: {:error, :invalid_args},
else: {:ok, values}
end
end
63 changes: 63 additions & 0 deletions test/types/enum_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
defmodule Soroban.Types.EnumTest do
use ExUnit.Case

alias Soroban.Types.{Enum, UInt32}
alias Stellar.TxBuild.SCVal

setup do
key = "key"
value = UInt32.new(100)
enum = Enum.new({key, value})
enum2 = Enum.new(key)

%{
key: key,
value: value,
enum: enum,
enum2: enum2
}
end

describe "new/1" do
test "with only key", %{key: key} do
%Enum{key: ^key, value: nil} = Enum.new(key)
end

test "with key and value", %{key: key, value: value} do
%Enum{key: ^key, value: ^value} = Enum.new({key, value})
end

test "with a nil value" do
{:error, :invalid} = Enum.new(nil)
end

test "with an atom value" do
{:error, :invalid} = Enum.new(:atom)
end
end

describe "to_sc_val/1" do
test "with a valid enum type without value struct", %{enum2: enum2} do
%SCVal{
type: :vec,
value: [
%SCVal{type: :symbol, value: "key"}
]
} = Enum.to_sc_val(enum2)
end

test "with a valid enum type struct", %{enum: enum} do
%SCVal{
type: :vec,
value: [
%SCVal{type: :symbol, value: "key"},
%SCVal{type: :u32, value: 100}
]
} = Enum.to_sc_val(enum)
end

test "with an invalid value" do
{:error, :invalid_struct_enum} = Enum.to_sc_val(nil)
end
end
end
Loading

0 comments on commit 3b007ed

Please sign in to comment.