diff --git a/README.md b/README.md index 4af9127..4fa6394 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Hex Version](https://img.shields.io/hexpm/v/numscriptex.svg)](https://hex.pm/packages/numscriptex) [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/numscriptex/) -NumscriptEx is a library that allows its users to check and run Numscripts in Elixir. If this is your first time hearing about +NumscriptEx is a library that allows its users to run Numscripts in Elixir. If this is your first time hearing about Numscripts, here is a quick explanation: [Numscript](https://docs.formance.com/numscript/) is a DSL made by [Formance](https://www.formance.com/) @@ -34,14 +34,13 @@ Ex: ```elixir config :numscriptex, binary_path: :numscriptex |> :code.priv_dir() |> Path.join("numscript.wasm") - version: "0.0.2", + version: "0.1.0", retries: 3 ``` These above are the default values. ## Usage -You can build Numscripts dynamically with `Numscriptex.Builder.build/1`, check if your script is valid with `Numscriptex.check/1`, -and last but not least, you can run your script with `Numscriptex.run/2`. +You can build Numscripts dynamically with `Numscriptex.Builder.build/1` and run your script with `Numscriptex.run/2`. You can read more about the `Numscriptex.Builder` module and how to use it on its [guide](https://hexdocs.pm/numscriptex/builder-introduction.html) @@ -59,18 +58,30 @@ The abstraction is made by creating a struct: iex> %Numscriptex.Run{ ...> balances: %{}, ...> metadata: %{}, -...> variables: %{} +...> variables: %{}, +...> feature_flags: [] ...> } ``` Where: - `:balances` a map with the account's assets balances. -- `:metadata` [metada variables](https://docs.formance.com/numscript/reference/metadata); -- `:variables` [variables](https://docs.formance.com/numscript/reference/variables) used inside the script. +- `:metadata` [metada variables](https://docs.formance.com/modules/numscript/reference/metadata); +- `:variables` [variables](https://docs.formance.com/modules/numscript/reference/variables). +- `:feature_flags` feature flags that enables numscript experimental features. + +The avaialable feature flags are: +- experimental_overdraft_function +- experimental_get_asset_function +- experimental_get_amount_function +- experimental_oneof +- experimental_account_interpolation +- experimental_mid_script_function_call +- experimental_asset_colors And to create a new struct, you can use the `Numscriptex.Run.put/3` or `Numscriptex.Run.put!/3` functions. Ex: ```elixir iex> variables = %{"order" => "orders:2345"} ...> balances = %{"orders:2345" => %{"USD/2" => 1000}} +...> feature_flags = [:experimental_oneof] ...> metadata = %{ ...> "merchants:1234" => %{"commission" => "15%"}, ...> "orders:2345" => %{"merchant" => "merchants:1234"} @@ -80,6 +91,7 @@ iex> variables = %{"order" => "orders:2345"} ...> |> Numscriptex.Run.put!(:balances, balances) ...> |> Numscriptex.Run.put!(:metadata, metadata) ...> |> Numscriptex.Run.put!(:variables, variables) +...> |> Numscriptex.Run.put!(:feature_flags, feature_flags) ``` Will return: @@ -88,6 +100,7 @@ Will return: iex> %Numscriptex.Run{ ...> variables: %{"orders:2345" => %{"USD/2" => 1000}}, ...> balances: %{"order" => "orders:2345"}, +...> feature_flags: [:experimental_oneof] ...> metadata: %{ ...> "merchants:1234" => %{"commission" => "15%"}, ...> "orders:2345" => %{"merchant" => "merchants:1234"} @@ -97,53 +110,8 @@ iex> %Numscriptex.Run{ Kindly reminder: you will always need a valid `Numscriptex.Run` struct to successfully execute your scripts. -### Check -To use `Numscriptex.check/1` you just have to pass your numscript as it's argument. Ex: - -```elixir -iex> "tmp/script.num" -...> |> File.read!() -...> |> Numscriptex.check() -{:ok, %{script: script} -``` - -You don“t need to necessarily read from a file, as long as it is a string it's fine. - -Sometimes, even if your script is valid, it could also return some warnings, infos or hints inside the map. -Ex: -```elixir -iex> {:ok, %{ -...> script: "your numscript here", -...> warnings: [ -...> %CheckLog{ -...> character: 10, -...> level: :warning, -...> line: 1, -...> message: "warning message" -...> } -...> ], -...> hints: [ -...> %CheckLog{ -...> character: 2, -...> level: :hint, -...> line: 7, -...> message: "hint message" -...> } -...> ], -...> infos: [ -...> %CheckLog{ -...> character: 9, -...> level: :info, -...> line: 14, -...> message: "info message" -...> } -...> ] -...> } -...> } -``` -The `:script` is the only field that will always return if your script is valid, the other three are optional. ### Run -To use `Numscriptex.run/2` your first argument must be your script (the same you used in `Numscriptex.check/1`), and the second must be the `%Numscriptex.Run{}` struct. Ex: +To use `Numscriptex.run/2` you must pass your script as the first argument, and the `%Numscriptex.Run{}` struct as the second. Ex: ```elixir iex> Numscriptex.run(script, struct) diff --git a/lib/numscriptex.ex b/lib/numscriptex.ex index 09cd8d8..566e03d 100644 --- a/lib/numscriptex.ex +++ b/lib/numscriptex.ex @@ -1,29 +1,19 @@ defmodule Numscriptex do @moduledoc """ - NumscriptEx is a library that allows its users to check and run [Numscripts](https://docs.formance.com/numscript/) + NumscriptEx is a library that allows its users to run [Numscripts](https://docs.formance.com/numscript/) via Elixir. - - Want to check if your script is valid and ready to go? Use the `check/1` function. - Already checked the script and want to execute him? Use the `run/2` function. """ alias Numscriptex.AssetsManager alias Numscriptex.Balance - alias Numscriptex.CheckLog alias Numscriptex.Posting + alias Numscriptex.Run alias Numscriptex.Utilities require AssetsManager AssetsManager.ensure_wasm_binary_is_valid() - @type check_result() :: %{ - required(:script) => binary(), - optional(:hints) => list(CheckLog.t()), - optional(:infos) => list(CheckLog.t()), - optional(:warnings) => list(CheckLog.t()) - } - @type run_result() :: %{ required(:balances) => list(Balance.t()), required(:postings) => list(Posting.t()), @@ -32,7 +22,7 @@ defmodule Numscriptex do } @type errors() :: %{ - required(:reason) => list(CheckLog.t()) | any(), + required(:reason) => any(), optional(:details) => any() } @@ -41,7 +31,7 @@ defmodule Numscriptex do ```elixir iex> Numscriptex.version() - %{numscriptex: "v0.2.5", numscript_wasm: "v0.0.2"} + %{numscriptex: "v0.2.5", numscript_wasm: "v0.1.0"} ``` """ @spec version() :: %{numscriptex: binary(), numscript_wasm: binary()} @@ -61,32 +51,6 @@ defmodule Numscriptex do end end - @doc """ - To use `check/1` you just need to pass your numscript as its argument. - Ex: - - ```elixir - iex> script = "send [USD/2 100] (source = @foo destination = @bar)" - iex> Numscriptex.check(script) - {:ok, %{script: script}} - ``` - - It could also return some warnings, infos or hints inside the map - """ - @spec check(binary()) :: {:ok, check_result()} | {:error, errors()} - def check(input) do - case execute_command(input, :check) do - :ok -> - {:ok, %{script: input}} - - {:ok, details} -> - {:ok, %{script: input, details: normalize_check_logs(details)}} - - {:error, %{reason: errors}} -> - {:error, %{reason: normalize_check_logs(errors)}} - end - end - @doc """ To use `run/2` your first argument must be your script, and the second must be a `%Numscriptex.Run{}` (go to `Numscriptex.Run` module to see more) struct. @@ -110,7 +74,7 @@ defmodule Numscriptex do initial_balance = Map.get(run_struct, :balances) run_struct - |> Map.from_struct() + |> Run.normalize_to_map() |> Map.merge(%{script: numscript}) |> JSON.encode!() |> execute_command(:run) @@ -230,20 +194,6 @@ defmodule Numscriptex do {:ok, %{numscript_wasm: String.trim(result)}} end - defp handle_operation_result({:ok, %{"valid" => valid?} = result}, :check) - when is_boolean(valid?) and valid? do - normalized_result = Map.delete(result, "valid") - - has_details? = not Enum.empty?(normalized_result) - - if has_details?, do: {:ok, normalized_result}, else: :ok - end - - defp handle_operation_result({:ok, %{"valid" => valid?, "errors" => _err} = result}, :check) - when is_boolean(valid?) and not valid? do - {:error, %{reason: Map.delete(result, "valid")}} - end - defp handle_operation_result({:ok, %{"postings" => postings} = result}, :run) do if Enum.empty?(postings), do: {:error, %{reason: :invalid_input}}, @@ -256,8 +206,6 @@ defmodule Numscriptex do defp handle_errors({:ok, _} = result), do: result - defp handle_errors(:ok), do: :ok - defp handle_errors({:error, %{reason: reason, details: details}}) do {:error, %{reason: normalize_error(reason), details: normalize_error(details)}} end @@ -273,29 +221,4 @@ defmodule Numscriptex do end defp normalize_error(error), do: error - - defp normalize_check_logs(logs) do - logs - |> Utilities.normalize_keys(:atom) - |> check_log_level_to_atom() - |> check_logs_to_struct() - |> Enum.into(%{}) - end - - defp check_logs_to_struct(logs) do - Enum.map(logs, fn {key, value} -> - {key, Enum.map(value, &CheckLog.from_map/1)} - end) - end - - defp check_log_level_to_atom(check_logs) do - Enum.flat_map(check_logs, fn {key, logs} -> - normalized_level_field = - Enum.map(logs, fn log -> - Map.update(log, :level, nil, &String.to_existing_atom/1) - end) - - Map.replace(check_logs, key, normalized_level_field) - end) - end end diff --git a/lib/numscriptex/assets_manager.ex b/lib/numscriptex/assets_manager.ex index 99fb043..456e098 100755 --- a/lib/numscriptex/assets_manager.ex +++ b/lib/numscriptex/assets_manager.ex @@ -6,7 +6,7 @@ defmodule Numscriptex.AssetsManager do """ Module.register_attribute(__MODULE__, :numscript_wasm_version, persist: true) - @numscript_wasm_version Application.compile_env(:numscriptex, :version, "0.0.2") + @numscript_wasm_version Application.compile_env(:numscriptex, :version, "0.1.0") @retries Application.compile_env(:numscriptex, :retries, 3) @numscript_checksums_url "https://github.com/PagoPlus/numscript-wasm/releases/download/v#{@numscript_wasm_version}/numscript_checksums.txt" @numscript_wasm_url "https://github.com/PagoPlus/numscript-wasm/releases/download/v#{@numscript_wasm_version}/numscript.wasm" diff --git a/lib/numscriptex/check_log.ex b/lib/numscriptex/check_log.ex deleted file mode 100644 index b19121f..0000000 --- a/lib/numscriptex/check_log.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule Numscriptex.CheckLog do - @moduledoc """ - After you check your numscript you might get a variety of logs even if it is valid, - `Numscriptex.CheckLog` is responsible for standardize these logs. - """ - - @derive JSON.Encoder - defstruct character: nil, - level: nil, - line: nil, - message: nil - - @typedoc """ - Type that represents `Numscriptex.CheckLog` struct. - - ## Fields - * `:character` the character position where the log occurred - * `:level` the log level - * `:line` the line where the log occur - * `:message` the log message - """ - @type t() :: %__MODULE__{ - character: pos_integer(), - level: log_levels(), - line: pos_integer(), - message: binary() - } - - @type log_levels() :: :error | :warning | :hint | :info - - @doc """ - Get a map with log data about a checked numscript. - - ```elixir - iex> map = %{ - ...> character: 10, - ...> level: :warning, - ...> line: 1, - ...> message: "warning message" - ...> } - ...> - ...> Numscriptex.CheckLog.from_map(map) - %Numscriptex.CheckLog{ - character: 10, - level: :warning, - line: 1, - message: "warning message" - } - ``` - """ - @spec from_map(map()) :: __MODULE__.t() - def from_map(map) do - struct(__MODULE__, normalize(map)) - end - - defp normalize(map) do - Map.new(map, fn - {:error, value} -> - {:message, value} - - {:warning, value} -> - {:message, value} - - {:info, value} -> - {:message, value} - - {:hint, value} -> - {:message, value} - - pair -> - pair - end) - end -end diff --git a/lib/numscriptex/run.ex b/lib/numscriptex/run.ex index 2f06229..2f8fbc9 100644 --- a/lib/numscriptex/run.ex +++ b/lib/numscriptex/run.ex @@ -8,7 +8,8 @@ defmodule Numscriptex.Run do @derive JSON.Encoder defstruct variables: %{}, balances: %{}, - metadata: %{} + metadata: %{}, + feature_flags: [] alias Numscriptex.Utilities @@ -16,6 +17,17 @@ defmodule Numscriptex.Run do variables balances metadata + feature_flags + )a + + @valid_feature_flags ~w( + experimental_overdraft_function + experimental_get_asset_function + experimental_get_amount_function + experimental_oneof + experimental_account_interpolation + experimental_mid_script_function_call + experimental_asset_colors )a @typedoc """ @@ -23,13 +35,15 @@ defmodule Numscriptex.Run do ## Fields * `:balances` a map with account's assets balances - * `:metadata` [metada variables](https://docs.formance.com/numscript/reference/metadata) - * `:variables` [variables](https://docs.formance.com/numscript/reference/variables) used inside the script + * `:metadata` [metadata variables](https://docs.formance.com/modules/numscript/reference/metadata) + * `:variables` [variables](https://docs.formance.com/modules/numscript/reference/variables) + * `:feature_flags` a list of feature flags used to enable experimental features """ @type t() :: %__MODULE__{ variables: map(), balances: map(), - metadata: map() + metadata: map(), + feature_flags: list() } @doc """ @@ -40,7 +54,8 @@ defmodule Numscriptex.Run do %Numscriptex.Run{ variables: %{}, balances: %{}, - metadata: %{} + metadata: %{}, + feature_flags: [] } ``` """ @@ -60,7 +75,8 @@ defmodule Numscriptex.Run do %Numscriptex.Run{ balances: %{"foo" => %{"USD/2" => 500, "EUR/2" => 300}}, variables: %{}, - metadata: %{} + metadata: %{}, + feature_flags: [] } } ``` @@ -75,13 +91,32 @@ defmodule Numscriptex.Run do ``` """ @spec put(t(), atom(), map()) :: {:ok, t()} | {:error, atom(), map()} - def put(run_struct, field, value \\ %{}) + def put(run_struct, field, value) def put(_run_struct, field, _value) when field not in @valid_fields, do: {:error, :invalid_field, %{details: "The field '#{field}' does not exists."}} - def put(_run_struct, _field, value) when not is_map(value), - do: {:error, :invalid_value, %{details: "Argument `value` must be a map."}} + def put(_run_struct, :feature_flags, value) when not is_list(value), + do: + {:error, :invalid_value, + %{details: "Argument `value` for field `feature_flags` must be a list."}} + + def put(_run_struct, field, value) when field != :feature_flags and not is_map(value), + do: + {:error, :invalid_value, %{details: "Argument `value` for field `#{field}` must be a map."}} + + def put(%__MODULE__{} = run_struct, :feature_flags, flags) do + invalid_flags = Enum.filter(flags, &(&1 not in @valid_feature_flags)) + + if invalid_flags == [] do + {:ok, Map.replace(run_struct, :feature_flags, flags)} + else + err_msg = + "Invalid feature flag(s). See `Numscriptex.Run.list_available_feature_flags/0` for a list of valid feature flags." + + {:error, :invalid_value, %{details: err_msg}} + end + end def put(%__MODULE__{} = run_struct, :balances, value) do {:ok, Map.replace(run_struct, :balances, Utilities.normalize_keys(value, :string))} @@ -99,7 +134,7 @@ defmodule Numscriptex.Run do ...> balances = [%{"foo" => %{"USD/2" => 500, "EUR/2" => 300}}] ...> ...> Numscriptex.Run.put!(struct, :balances, balances) - ** (ArgumentError) Argument `value` must be a map. + ** (ArgumentError) Argument `value` for field `balances` must be a map. ``` """ @spec put!(t(), atom(), map()) :: t() | no_return() @@ -112,4 +147,63 @@ defmodule Numscriptex.Run do raise ArgumentError, message: message end end + + @doc """ + Normalizes the `Numscriptex.Run` struct to a map. + + ```elixir + iex> struct = + ...> Numscriptex.Run.put!(Numscriptex.Run.new(), :feature_flags, [:experimental_overdraft_function]) + ...> + ...> Numscriptex.Run.normalize_to_map(struct) + %{ + balances: %{}, + metadata: %{}, + variables: %{}, + featureFlags: %{"experimental-overdraft-function" => %{}} + } + ``` + """ + @spec normalize_to_map(__MODULE__.t()) :: map() + def normalize_to_map(%__MODULE__{} = run_input) do + feature_flags = normalize_feature_flags(run_input) + + run_input + |> Map.from_struct() + |> Map.delete(:feature_flags) + |> Map.put(:featureFlags, feature_flags) + end + + defp normalize_feature_flags(run_input) do + stringified_flags = + run_input + |> Map.get(:feature_flags, []) + |> Enum.map(fn flag -> + flag + |> to_string() + |> String.replace("_", "-") + end) + + stringified_flags + |> Enum.map(fn flag -> {flag, %{}} end) + |> Enum.into(%{}) + end + + @doc """ + Lists all available feature flags. + ```elixir + iex> Numscriptex.Run.list_available_feature_flags() + [ + :experimental_overdraft_function, + :experimental_get_asset_function, + :experimental_get_amount_function, + :experimental_oneof, + :experimental_account_interpolation, + :experimental_mid_script_function_call, + :experimental_asset_colors + ] + ``` + """ + @spec list_available_feature_flags() :: list(atom()) + def list_available_feature_flags, do: @valid_feature_flags end diff --git a/test/numscriptex/builder_test.exs b/test/numscriptex/builder_test.exs index 20f0605..58cda10 100644 --- a/test/numscriptex/builder_test.exs +++ b/test/numscriptex/builder_test.exs @@ -19,7 +19,6 @@ defmodule Numscriptex.BuilderTest do } assert {:ok, %{script: script}} = Builder.build(metadata) - assert {:ok, _script} = Numscriptex.check(script) assert script == """ send [USD 500] ( @@ -43,7 +42,6 @@ defmodule Numscriptex.BuilderTest do } assert {:ok, %{script: script}} = Builder.build(metadata) - assert {:ok, _script} = Numscriptex.check(script) assert script == """ send [USD *] ( @@ -76,7 +74,6 @@ defmodule Numscriptex.BuilderTest do } assert {:ok, %{script: script}} = Builder.build(metadata) - assert {:ok, _script} = Numscriptex.check(script) assert script == """ send [USD *] ( @@ -159,7 +156,6 @@ defmodule Numscriptex.BuilderTest do } assert {:ok, %{script: script}} = Builder.build(metadata) - assert {:ok, _script} = Numscriptex.check(script) assert script == """ send [EUR *] ( @@ -216,7 +212,6 @@ defmodule Numscriptex.BuilderTest do } assert {:ok, %{script: script}} = Builder.build(metadata) - assert {:ok, _script} = Numscriptex.check(script) assert script == """ send [EUR *] ( @@ -255,7 +250,6 @@ defmodule Numscriptex.BuilderTest do } assert {:ok, %{script: script}} = Builder.build(metadata) - assert {:ok, _script} = Numscriptex.check(script) assert script == """ send [EUR *] ( @@ -292,7 +286,6 @@ defmodule Numscriptex.BuilderTest do } assert {:ok, %{script: script}} = Builder.build(metadata) - assert {:ok, _script} = Numscriptex.check(script) assert script == """ send [EUR *] ( diff --git a/test/numscriptex/check_log_test.exs b/test/numscriptex/check_log_test.exs deleted file mode 100644 index 218eead..0000000 --- a/test/numscriptex/check_log_test.exs +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Numscriptex.CheckLogTest do - use ExUnit.Case - - alias Numscriptex.CheckLog - - doctest Numscriptex.CheckLog - - describe "from_map/1" do - test "transforms a map into %CheckLog{}" do - map = %{ - character: 10, - level: :warning, - line: 1, - message: "warning message" - } - - assert struct = CheckLog.from_map(map) - - assert struct == %CheckLog{ - character: 10, - level: :warning, - line: 1, - message: "warning message" - } - end - end -end diff --git a/test/numscriptex/run_test.exs b/test/numscriptex/run_test.exs index bf4b30a..385f690 100644 --- a/test/numscriptex/run_test.exs +++ b/test/numscriptex/run_test.exs @@ -64,7 +64,7 @@ defmodule Numscriptex.RunTest do ] assert {:error, :invalid_value, error} = Run.put(%Run{}, :balances, balances) - assert error.details == "Argument `value` must be a map." + assert error.details == "Argument `value` for field `balances` must be a map." end test "put metadata field with valid params" do @@ -92,7 +92,7 @@ defmodule Numscriptex.RunTest do ] assert {:error, :invalid_value, error} = Run.put(%Run{}, :metadata, metadata) - assert error.details == "Argument `value` must be a map." + assert error.details == "Argument `value` for field `metadata` must be a map." end test "put variables field with valid params" do @@ -122,7 +122,7 @@ defmodule Numscriptex.RunTest do ] assert {:error, :invalid_value, error} = Run.put(%Run{}, :variables, variables) - assert error.details == "Argument `value` must be a map." + assert error.details == "Argument `value` for field `variables` must be a map." end test "fails to put an invalid field" do @@ -156,7 +156,7 @@ defmodule Numscriptex.RunTest do end test "raise an error with invalid value" do - error_message = "Argument `value` must be a map." + error_message = "Argument `value` for field `metadata` must be a map." assert_raise ArgumentError, error_message, fn -> Run.put!(%Run{}, :metadata, "") end end diff --git a/test/numscriptex_test.exs b/test/numscriptex_test.exs index b8f8c78..392be4b 100644 --- a/test/numscriptex_test.exs +++ b/test/numscriptex_test.exs @@ -2,88 +2,9 @@ defmodule NumscriptexTest do use ExUnit.Case, async: false alias Numscriptex.AssetsManager - alias Numscriptex.CheckLog doctest Numscriptex - describe "check/1" do - setup do - script = """ - vars { - account $order - account $merchant = meta($order, "merchant") - portion $commission = meta($merchant, "commission") - } - - send [USD/2 *] ( - source = $order - destination = { - $commission to @platform:fees - remaining to $merchant - } - ) - """ - - warning_script = """ - vars { - account $unused - account $order - account $merchant = meta($order, "merchant") - portion $commission = meta($merchant, "commission") - } - - send [USD/2 *] ( - source = $order - destination = { - $commission to @platform:fees - remaining to $merchant - } - ) - """ - - {:ok, %{script: script, warning_script: warning_script}} - end - - test "with valid script", %{script: script} do - assert {:ok, result} = Numscriptex.check(script) - assert result.script == script - end - - test "with valid script, but unused var", %{warning_script: warning_script} do - assert {:ok, result} = Numscriptex.check(warning_script) - assert result.script == warning_script - - assert result.details == %{ - warnings: [ - %CheckLog{ - character: 10, - level: :warning, - line: 1, - message: "The variable '$unused' is never used" - } - ] - } - - has_errors? = Map.has_key?(result.details, :errors) - - refute has_errors? - end - - test "with invalid script", %{script: script} do - error_script = String.replace(script, "a", "e") - - assert {:error, %{reason: reason}} = Numscriptex.check(error_script) - assert [error | _errors] = reason.errors - - assert error == %CheckLog{ - character: 0, - level: :error, - line: 0, - message: "The function 'vers' does not exist" - } - end - end - describe "run/2" do test "simple send" do script = """ @@ -515,8 +436,15 @@ defmodule NumscriptexTest do source: "orders:1234" }, %Numscriptex.Posting{ - amount: 8000, - decimal_amount: 80.0, + amount: 500, + decimal_amount: 5.0, + asset: "USD/2", + destination: "merchants:6789", + source: "orders:1234" + }, + %Numscriptex.Posting{ + amount: 7500, + decimal_amount: 75.0, asset: "USD/2", destination: "merchants:6789", source: "orders:1234" @@ -1139,6 +1067,77 @@ defmodule NumscriptexTest do assert error.details == "Not enough funds. Needed [USD/2 501] (only [USD/2 500] available)" end + test "with experimental feature flags" do + script = """ + vars { + monetary $mon = [USD/2 100] + number $n = get_amount($mon) + } + + send [USD/2 $n] ( + source = oneof { + @foo + @bar + } + + destination = @baz + ) + """ + + balances = %{"bar" => %{"USD/2" => 500, "EUR/2" => 300}} + + feature_flags = [ + :experimental_oneof, + :experimental_get_amount_function, + :experimental_mid_script_function_call + ] + + metadata = %{} + variables = %{} + + struct = build_run_struct(balances, metadata, variables, feature_flags) + + assert {:ok, result} = Numscriptex.run(script, struct) + + assert result.postings == [ + %Numscriptex.Posting{ + source: "bar", + destination: "baz", + asset: "USD/2", + amount: 100, + decimal_amount: 1.0 + } + ] + + assert result.balances == [ + %Numscriptex.Balance{ + account: "bar", + asset: "EUR/2", + final_balance: 300, + initial_balance: 300, + decimal_final_balance: 3.0, + decimal_initial_balance: 3.0 + }, + %Numscriptex.Balance{ + account: "bar", + asset: "USD/2", + final_balance: 400, + initial_balance: 500, + decimal_final_balance: 4.0, + decimal_initial_balance: 5.0 + }, + %Numscriptex.Balance{ + account: "baz", + asset: "USD/2", + final_balance: 100, + initial_balance: 0, + decimal_final_balance: 1.0, + decimal_initial_balance: 0.0 + } + # foo is not present because he does not have any balance + ] + end + test "with insufficient amount" do script = """ send [USD/2 100] ( @@ -1192,7 +1191,7 @@ defmodule NumscriptexTest do assert {:error, error} = Numscriptex.run(script, %Numscriptex.Run{}) assert error.details == - "Got errors while parsing:\nmismatched input 'source' expecting {')', '[', RATIO_PORTION_LITERAL, PERCENTAGE_PORTION_LITERAL, STRING, NUMBER, VARIABLE_NAME, ACCOUNT, ASSET}\n 0 | sd ( source = @foo destination = @bar)\n | ~~~~~" + "Got errors while parsing:\nmismatched input 'source' expecting {'overdraft', '(', ')', '[', PERCENTAGE_PORTION_LITERAL, STRING, IDENTIFIER, NUMBER, ASSET, '@', VARIABLE_NAME}\n 0 | sd ( source = @foo destination = @bar)\n | ~~~~~" end test "fails with invalid script but valid sctruct" do @@ -1210,13 +1209,6 @@ defmodule NumscriptexTest do end end - defp build_run_struct(balances, metadata, variables) do - %Numscriptex.Run{} - |> Numscriptex.Run.put!(:balances, balances) - |> Numscriptex.Run.put!(:metadata, metadata) - |> Numscriptex.Run.put!(:variables, variables) - end - describe "version/0" do setup do numscriptex_version = @@ -1256,4 +1248,12 @@ defmodule NumscriptexTest do File.copy!(dest_path, wasm_binary_path) end end + + defp build_run_struct(balances, metadata, variables, feature_flags \\ []) do + %Numscriptex.Run{} + |> Numscriptex.Run.put!(:balances, balances) + |> Numscriptex.Run.put!(:metadata, metadata) + |> Numscriptex.Run.put!(:variables, variables) + |> Numscriptex.Run.put!(:feature_flags, feature_flags) + end end