diff --git a/lib/flow_runner/spec/block.ex b/lib/flow_runner/spec/block.ex index c1ae4a5..565c6ce 100644 --- a/lib/flow_runner/spec/block.ex +++ b/lib/flow_runner/spec/block.ex @@ -54,11 +54,16 @@ defmodule FlowRunner.Spec.Block do * `{:invalid, reason}` — the flow runner will exit through the block's default response. + + * `{:error, reason}` — the block could not pick an exit at all (for + example a `Core.Case` whose tests all evaluated to false and which + has no default exit); the error is returned to the caller. """ @callback evaluate_outgoing(Container.t(), Flow.t(), Block.t(), Context.t(), user_input :: any) :: {:ok, user_input :: any} | {:ok, user_input :: any, opts :: Keyword.t()} | {:invalid, reason :: String.t()} + | {:error, reason :: String.t()} @derive Jason.Encoder defstruct uuid: nil, @@ -228,7 +233,9 @@ defmodule FlowRunner.Spec.Block do @decorate with_span("FlowRunner.Spec.Block.evaluate_outgoing") @spec evaluate_outgoing(Container.t(), Flow.t(), Block.t(), Context.t(), user_input :: any) :: - {:ok, Context.t(), Block.t()} | {:invalid, reason :: String.t()} + {:ok, Context.t(), Block.t() | nil} + | {:invalid, reason :: String.t()} + | {:error, reason :: String.t()} def evaluate_outgoing( container, flow, @@ -270,12 +277,16 @@ defmodule FlowRunner.Spec.Block do {:ok, context} = evaluate_user_input(block, context, user_input) fetch_default_block(block, flow, context) + + {:error, reason} -> + {:error, reason} end end defp normalize_outgoing({:ok, user_input}), do: {:ok, user_input, []} defp normalize_outgoing({:ok, user_input, opts}) when is_list(opts), do: {:ok, user_input, opts} defp normalize_outgoing({:invalid, _} = invalid), do: invalid + defp normalize_outgoing({:error, _} = error), do: error @spec fetch_default_block(Block.t(), Flow.t(), Context.t()) :: {:error, String.t()} | {:ok, Context.t(), Block.t() | nil} diff --git a/lib/flow_runner/spec/blocks/case.ex b/lib/flow_runner/spec/blocks/case.ex index b1ac3b4..49d2571 100644 --- a/lib/flow_runner/spec/blocks/case.ex +++ b/lib/flow_runner/spec/blocks/case.ex @@ -33,14 +33,14 @@ defmodule FlowRunner.Spec.Blocks.Case do @impl FlowRunner.Spec.Block def evaluate_outgoing(_container, _flow, block, context, nil) do - {:ok, block_exit} = Block.evaluate_exits(block, context) - - case FlowRunner.evaluate_expression_block(block_exit.name, context) do - # We didn't manage to parse it and it returned a parsing error - {:error, _error, _reason} -> {:ok, block_exit.name} - # We managed to evaluate it and it returned nil - nil -> {:ok, block_exit.name} - value -> {:ok, value} + with {:ok, block_exit} <- Block.evaluate_exits(block, context) do + case FlowRunner.evaluate_expression_block(block_exit.name, context) do + # We didn't manage to parse it and it returned a parsing error + {:error, _error, _reason} -> {:ok, block_exit.name} + # We managed to evaluate it and it returned nil + nil -> {:ok, block_exit.name} + value -> {:ok, value} + end end end end diff --git a/mix.exs b/mix.exs index 506893b..03fa3d5 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule FlowRunner.MixProject do use Mix.Project - @version "6.15.1" + @version "6.15.2" def project do [ diff --git a/test/block_test.exs b/test/block_test.exs index f889e56..ebeb6c1 100644 --- a/test/block_test.exs +++ b/test/block_test.exs @@ -4,6 +4,7 @@ defmodule BlockTest do alias FlowRunner.Context alias FlowRunner.Spec.Block + alias FlowRunner.Spec.Container alias FlowRunner.Spec.Exit alias FlowRunner.Spec.Flow @@ -259,6 +260,66 @@ defmodule BlockTest do assert {:ok, %Exit{uuid: "failure-exit"}} = Block.evaluate_exits(block, context_false) end + describe "evaluate_outgoing/5 on a Core.Case block" do + test "returns an error instead of raising when no exit matches and there is no default exit" do + routing_block = %Block{ + uuid: "5a0a4c1e-1d0e-4a4e-9a8e-3c1d7b2f6e01", + name: "Routing for check", + type: "Core.Case", + exits: [ + %Exit{ + uuid: "b586afa7-0097-4805-9951-f6d3156c08db", + name: "Exit for CheckYes", + test: "block.value = 5", + destination_block: "8e2f8c2d-4b0b-4a3f-8a6c-1f2e3d4c5b6a" + } + ] + } + + context = %Context{vars: %{"block" => %{"value" => 10}}} + + assert {:error, "No default exit available"} = + Block.evaluate_outgoing( + %Container{}, + %Flow{blocks: [routing_block]}, + routing_block, + context, + nil + ) + end + + test "stores the exit name when the name evaluates to nil" do + destination_uuid = "8e2f8c2d-4b0b-4a3f-8a6c-1f2e3d4c5b6a" + + routing_block = %Block{ + uuid: "5a0a4c1e-1d0e-4a4e-9a8e-3c1d7b2f6e02", + name: "Routing for check", + type: "Core.Case", + exits: [ + %Exit{ + uuid: "c2f1e9a0-6d3b-4f8e-9c1a-2b3d4e5f6a70", + name: "answer", + test: "true", + destination_block: destination_uuid + } + ] + } + + destination_block = %Block{uuid: destination_uuid, name: "destination", type: "Core.Log"} + context = %Context{vars: %{"answer" => nil}} + + assert {:ok, %Context{vars: %{"Routing for check" => "answer"}}, + %Block{uuid: ^destination_uuid}} = + Block.evaluate_outgoing( + %Container{}, + %Flow{blocks: [routing_block, destination_block]}, + routing_block, + context, + nil + ) + end + end + describe "load_config_for_set_contact_property!/1" do test "accepts the singular shape" do assert %{