Current section
Files
Jump to
Current section
Files
lib/boxic/dmn/evaluator.ex
defmodule Boxic.DMN.Evaluator do
@moduledoc false
alias Boxic.DMN.Model
alias Boxic.DMN.Model.BusinessKnowledgeModel
alias Boxic.DMN.Model.ContextExpression
alias Boxic.DMN.Model.ConditionalExpression
alias Boxic.DMN.Model.DecisionService
alias Boxic.DMN.Model.DecisionTable
alias Boxic.DMN.Model.FunctionDefinition
alias Boxic.DMN.Model.FilterExpression
alias Boxic.DMN.Model.InformationRequirement
alias Boxic.DMN.Model.Invocation
alias Boxic.DMN.Model.IteratorExpression
alias Boxic.DMN.Model.LiteralExpression
alias Boxic.DMN.Model.ListExpression
alias Boxic.DMN.Model.Relation
alias Boxic.DMN.Model.Variable
alias Boxic.DMN.{FEEL, Validator}
alias Boxic.DMN.DecisionTable, as: DecisionTableEvaluator
alias Boxic.FEEL.ExternalFunctions
@type evaluation_error ::
Boxic.FEEL.Error.t()
| {atom(), term()}
| {atom(), term(), term()}
| {atom(), term(), term(), term()}
@spec evaluate(Model.t(), String.t(), map()) :: {:ok, term()} | {:error, evaluation_error()}
def evaluate(%Model{} = model, decision_name, context \\ %{})
when is_binary(decision_name) and is_map(context) do
with {:ok, context} <- coerce_input_context(model, context),
{:ok, decision} <- find_decision(model, decision_name),
{:ok, value, _memo} <- evaluate_decision(model, decision, context, %{}, %{}) do
{:ok, value}
end
end
@spec evaluate(Model.t(), String.t(), map(), keyword()) ::
{:ok, term()} | {:error, evaluation_error()}
def evaluate(%Model{} = model, decision_name, context, opts)
when is_binary(decision_name) and is_map(context) and is_list(opts) do
external_context =
case Keyword.get(opts, :external_functions) do
nil -> %{}
registry -> ExternalFunctions.to_context(registry)
end
evaluate(model, decision_name, Map.merge(context, external_context))
end
@spec evaluate_service(Model.t(), String.t(), map() | list()) ::
{:ok, term()} | {:error, evaluation_error()}
def evaluate_service(%Model{} = model, service_name, arguments)
when is_binary(service_name) and (is_map(arguments) or is_list(arguments)) do
with {:ok, service} <- find_service(model, service_name) do
args =
if is_map(arguments),
do: Enum.map(arguments, fn {name, value} -> {:named_arg, name, value} end),
else: arguments
invoke_decision_service(model, service, %{}, args)
end
end
defp evaluate_decision(model, decision, context, memo, visiting) do
cond do
Map.has_key?(memo, decision.id) ->
{:ok, Map.fetch!(memo, decision.id), memo}
Map.has_key?(visiting, decision.id) ->
{:error, {:cyclic_decision_dependency, decision.id}}
true ->
visiting = Map.put(visiting, decision.id, true)
with :ok <- Validator.validate_for_evaluation(model, decision),
{:ok, dependency_context, memo} <-
resolve_requirements(model, decision, context, memo, visiting),
{:ok, value} <- evaluate_expression(decision.expression, dependency_context, model),
{:ok, value} <-
coerce_type(value, decision.variable && decision.variable.type_ref, model) do
{:ok, value, Map.put(memo, decision.id, value)}
else
nil -> {:error, {:missing_expression, decision.id}}
{:unsupported, kind} -> {:error, {:unsupported_expression, kind}}
error -> error
end
end
end
defp evaluate_expression(%LiteralExpression{text: text, type_ref: type_ref}, context, model)
when is_binary(text) do
{text, context} = FEEL.normalize_names(text, context)
context = Map.put(context, "__feel_types__", model.item_definitions)
with {:ok, value} <- Boxic.FEEL.evaluate(text, context) do
coerce_type(value, type_ref, model)
end
end
defp evaluate_expression(%DecisionTable{} = table, context, _model),
do: DecisionTableEvaluator.evaluate(table, context)
defp evaluate_expression(%ContextExpression{} = expression, context, model),
do: evaluate_context(expression, context, model)
defp evaluate_expression(%Invocation{} = invocation, context, model),
do: evaluate_invocation(invocation, context, model)
defp evaluate_expression(%FunctionDefinition{} = function, context, model) do
callable =
{:external_function,
fn args ->
names = Enum.map(function.parameters, & &1.name)
with {:ok, values} <- normalize_service_args(args, names) do
evaluate_expression(
function.body,
Map.merge(context, Map.new(Enum.zip(names, values))),
model
)
end
end}
{:ok, callable}
end
defp evaluate_expression(%Relation{} = relation, context, model) do
names = Enum.map(relation.columns, & &1.name)
relation.rows
|> Enum.reduce_while({:ok, []}, fn row, {:ok, results} ->
case Enum.reduce_while(row, {:ok, []}, fn expression, {:ok, values} ->
case evaluate_expression(expression, context, model) do
{:ok, value} -> {:cont, {:ok, values ++ [value]}}
{:error, _reason} = error -> {:halt, error}
end
end) do
{:ok, values} -> {:cont, {:ok, results ++ [Map.new(Enum.zip(names, values))]}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp evaluate_expression(%ListExpression{items: items}, context, model) do
Enum.reduce_while(items, {:ok, []}, fn item, {:ok, values} ->
case evaluate_expression(item, context, model) do
{:ok, value} -> {:cont, {:ok, values ++ [value]}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp evaluate_expression(%ConditionalExpression{} = expression, context, model) do
with {:ok, condition} <- evaluate_expression(expression.condition, context, model) do
case condition do
true -> evaluate_expression(expression.then_branch, context, model)
false -> evaluate_expression(expression.else_branch, context, model)
nil -> evaluate_expression(expression.else_branch, context, model)
_ -> {:error, {:type_error, :boolean}}
end
end
end
defp evaluate_expression(%FilterExpression{} = expression, context, model) do
with {:ok, source} <- evaluate_expression(expression.source, context, model),
true <- is_list(source) do
Enum.reduce_while(source, {:ok, []}, fn item, {:ok, result} ->
case evaluate_expression(expression.match, bind_boxed_item(context, item), model) do
{:ok, true} -> {:cont, {:ok, result ++ [item]}}
{:ok, false} -> {:cont, {:ok, result}}
{:ok, _value} -> {:halt, {:error, {:type_error, :boolean}}}
{:error, _reason} = error -> {:halt, error}
end
end)
else
false -> {:error, {:type_error, :list}}
{:error, _reason} = error -> error
end
end
defp evaluate_expression(%IteratorExpression{kind: :for} = expression, context, model) do
with {:ok, source} <- evaluate_expression(expression.source, context, model),
true <- is_list(source) do
Enum.reduce_while(source, {:ok, []}, fn item, {:ok, result} ->
case evaluate_expression(
expression.body,
Map.put(context, expression.variable, item),
model
) do
{:ok, value} -> {:cont, {:ok, result ++ [value]}}
{:error, _reason} = error -> {:halt, error}
end
end)
else
false -> {:error, {:type_error, :list}}
{:error, _reason} = error -> error
end
end
defp evaluate_expression(%IteratorExpression{} = expression, context, model) do
with {:ok, source} <- evaluate_expression(expression.source, context, model),
true <- is_list(source) do
evaluate_boxed_quantifier(expression.kind, source, expression, context, model)
else
false -> {:error, {:type_error, :list}}
{:error, _reason} = error -> error
end
end
defp evaluate_expression(nil, _context, _model), do: {:error, :missing_expression}
defp evaluate_expression({:unsupported, kind}, _context, _model),
do: {:error, {:unsupported_expression, kind}}
defp evaluate_boxed_quantifier(kind, source, expression, context, model) do
initial = if kind == :every, do: true, else: false
Enum.reduce_while(source, {:ok, initial}, fn item, {:ok, _result} ->
case evaluate_expression(
expression.body,
Map.put(context, expression.variable, item),
model
) do
{:ok, value} when is_boolean(value) ->
if (kind == :some and value) or (kind == :every and not value),
do: {:halt, {:ok, value}},
else: {:cont, {:ok, value}}
{:ok, _value} ->
{:halt, {:error, {:type_error, :boolean}}}
{:error, _reason} = error ->
{:halt, error}
end
end)
end
defp bind_boxed_item(context, item) when is_map(item) and not is_struct(item),
do: Map.merge(context, Map.put(item, "item", item))
defp bind_boxed_item(context, item), do: Map.put(context, "item", item)
defp evaluate_context(%ContextExpression{entries: entries}, outer_context, model) do
entries
|> Enum.with_index()
|> Enum.reduce_while({:ok, %{}, outer_context}, fn {entry, index}, {:ok, result, scope} ->
case evaluate_expression(entry.expression, scope, model) do
{:ok, value} ->
case entry.variable do
%Variable{name: name} when is_binary(name) ->
{:cont, {:ok, Map.put(result, name, value), Map.put(scope, name, value)}}
nil when index == length(entries) - 1 ->
{:halt, {:result, value}}
_ ->
{:halt, {:error, {:invalid_context_entry, index}}}
end
{:error, _reason} = error ->
{:halt, error}
end
end)
|> case do
{:ok, result, _scope} -> {:ok, result}
{:result, value} -> {:ok, value}
error -> error
end
end
defp resolve_requirements(model, decision, context, memo, visiting) do
Enum.reduce_while(decision.requirements, {:ok, context, memo}, fn requirement,
{:ok, acc, memo} ->
case resolve_requirement(model, requirement, acc, memo, visiting) do
{:ok, next_context, next_memo} -> {:cont, {:ok, next_context, next_memo}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp resolve_requirement(
model,
%InformationRequirement{kind: :decision, href: id},
context,
memo,
visiting
) do
with {:ok, dependency} <- fetch_reference(model.decisions, :decision, id) do
case existing_node_value(context, dependency) do
{:ok, value} ->
{:ok, bind_reference_value(context, dependency, value, id, model), memo}
:error ->
with {:ok, value, memo} <- evaluate_decision(model, dependency, context, memo, visiting) do
{:ok, bind_reference_value(context, dependency, value, id, model), memo}
end
end
end
end
defp resolve_requirement(
model,
%InformationRequirement{kind: :knowledge, href: id},
context,
memo,
_visiting
) do
case fetch_reference(Map.merge(model.bkms, model.decision_services), :knowledge, id) do
{:ok, %BusinessKnowledgeModel{} = bkm} ->
callable =
{:external_function, fn args -> invoke_bkm_from_feel(model, bkm, context, args) end}
{:ok, bind_reference_value(context, bkm, callable, id, model), memo}
{:ok, %DecisionService{} = service} ->
callable =
{:external_function,
fn args -> invoke_decision_service(model, service, context, args) end}
{:ok, bind_reference_value(context, service, callable, id, model), memo}
{:error, _reason} = error ->
error
end
end
defp resolve_requirement(
model,
%InformationRequirement{kind: :input_data, href: id},
context,
memo,
_visiting
) do
with {:ok, input} <- fetch_reference(model.input_data, :input_data, id),
{:ok, value} <- input_value(context, input),
{:ok, value} <- coerce_type(value, input.variable && input.variable.type_ref, model) do
{:ok, bind_reference_value(context, input, value, id, model), memo}
end
end
defp resolve_requirement(_model, requirement, _context, _memo, _visiting),
do: {:error, {:invalid_information_requirement, requirement}}
defp fetch_reference(index, kind, id) do
case Map.fetch(index, id) do
{:ok, value} ->
{:ok, value}
:error ->
fragment = id |> String.split("#") |> List.last()
case Map.fetch(index, fragment) do
{:ok, value} -> {:ok, value}
:error -> {:error, {:unresolved_reference, kind, id}}
end
end
end
defp input_value(context, input) do
[input.name, input.variable && input.variable.name, input.id]
|> Enum.reject(&is_nil/1)
|> Enum.find_value(fn name ->
case Map.fetch(context, name) do
{:ok, value} -> {:found, value}
:error -> nil
end
end)
|> case do
{:found, value} -> {:ok, value}
nil -> {:error, {:missing_input, input.name || input.id}}
end
end
defp coerce_input_context(model, context) do
Enum.reduce_while(model.input_data, {:ok, context}, fn {_id, input}, {:ok, result} ->
keys =
[input.name, input.variable && input.variable.name, input.id] |> Enum.reject(&is_nil/1)
case Enum.find(keys, &Map.has_key?(result, &1)) do
nil ->
{:cont, {:ok, result}}
key ->
case coerce_type(
Map.fetch!(result, key),
input.variable && input.variable.type_ref,
model
) do
{:ok, value} -> {:cont, {:ok, Enum.reduce(keys, result, &Map.put(&2, &1, value))}}
{:error, _reason} = error -> {:halt, error}
end
end
end)
end
defp existing_node_value(context, node) do
[node.name, node.variable && node.variable.name, node.id]
|> Enum.reject(&is_nil/1)
|> Enum.find_value(fn key ->
if Map.has_key?(context, key), do: {:found, Map.get(context, key)}
end)
|> case do
{:found, value} -> {:ok, value}
nil -> :error
end
end
defp bind_value(context, node, value) do
[node.name, node.variable && node.variable.name, node.id]
|> Enum.reject(&is_nil/1)
|> Enum.reduce(context, &Map.put(&2, &1, value))
end
defp bind_reference_value(context, node, value, href, model) do
context = bind_value(context, node, value)
case String.split(href, "#", parts: 2) do
[namespace, _id] ->
case Map.get(model.imports, namespace) do
%{name: alias_name} when is_binary(alias_name) ->
Map.update(context, alias_name, %{node.name => value}, &Map.put(&1, node.name, value))
_ ->
context
end
_ ->
context
end
end
defp evaluate_invocation(%Invocation{} = invocation, context, model) do
with %LiteralExpression{text: function_name} when is_binary(function_name) <-
invocation.function,
{:ok, bkm} <- find_bkm(model, String.trim(function_name)),
:ok <- validate_invocation_result_shape(invocation, bkm),
{:ok, bindings} <- evaluate_bindings(invocation.bindings, context, model),
:ok <- validate_bkm_arguments(bkm, bindings),
values = Enum.map(bkm.parameters, &Map.fetch!(bindings, &1.name)),
{:ok, values} <- coerce_parameter_values(values, bkm.parameters, model),
bindings = Map.new(Enum.zip(Enum.map(bkm.parameters, & &1.name), values)),
{:ok, bkm_context, _memo} <-
resolve_requirements(model, bkm, context, %{}, MapSet.new()),
{:ok, result} <-
evaluate_expression(bkm.expression, Map.merge(bkm_context, bindings), model) do
coerce_type(result, bkm.variable && bkm.variable.type_ref, model)
else
nil -> {:error, :missing_invocation_function}
error -> error
end
end
defp validate_invocation_result_shape(
%Invocation{type_ref: type},
%BusinessKnowledgeModel{expression: %LiteralExpression{text: text}}
)
when type in ["string", "number", "boolean"] and is_binary(text) do
if String.starts_with?(String.trim(text), "["),
do: {:error, {:invocation_type_error, type}},
else: :ok
end
defp validate_invocation_result_shape(_invocation, _bkm), do: :ok
defp invoke_bkm_from_feel(model, bkm, outer_context, args) do
names = Enum.map(bkm.parameters, & &1.name)
self_callable =
{:external_function,
fn recursive_args -> invoke_bkm_from_feel(model, bkm, outer_context, recursive_args) end}
with {:ok, values} <- normalize_service_args(args, names),
{:ok, values} <- coerce_parameter_values(values, bkm.parameters, model),
{:ok, bkm_context, _memo} <-
resolve_requirements(model, bkm, outer_context, %{}, MapSet.new()),
{:ok, result} <-
evaluate_expression(
bkm.expression,
bkm_context
|> bind_value(bkm, self_callable)
|> Map.merge(Map.new(Enum.zip(names, values))),
model
) do
coerce_type(result, bkm.variable && bkm.variable.type_ref, model)
end
end
defp evaluate_bindings(bindings, context, model) do
Enum.reduce_while(bindings, {:ok, %{}}, fn binding, {:ok, result} ->
case evaluate_expression(binding.expression, context, model) do
{:ok, value} -> {:cont, {:ok, Map.put(result, binding.parameter, value)}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp validate_bkm_arguments(bkm, bindings) do
expected = Enum.map(bkm.parameters, & &1.name) |> MapSet.new()
actual = Map.keys(bindings) |> MapSet.new()
if expected == actual,
do: :ok,
else:
{:error,
{:bkm_argument_mismatch, bkm.name, MapSet.to_list(expected), MapSet.to_list(actual)}}
end
defp find_bkm(model, name_or_id) do
case Map.fetch(model.bkms, name_or_id) do
{:ok, bkm} ->
{:ok, bkm}
:error ->
case Enum.find_value(model.bkms, fn {_id, bkm} -> bkm.name == name_or_id && bkm end) do
nil -> {:error, {:bkm_not_found, name_or_id}}
bkm -> {:ok, bkm}
end
end
end
defp invoke_decision_service(model, service, outer_context, args) do
with {:ok, service_context} <- service_arguments(model, service, args, outer_context),
{:ok, outputs, _memo} <-
evaluate_service_outputs(model, service.output_decisions, service_context, %{}) do
result =
case outputs do
[{_name, value}] -> value
values -> Map.new(values)
end
coerce_type(result, service.variable && service.variable.type_ref, model)
end
end
defp find_service(model, name_or_id) do
case Map.fetch(model.decision_services, name_or_id) do
{:ok, service} ->
{:ok, service}
:error ->
case Enum.find_value(model.decision_services, fn {_id, service} ->
service.name == name_or_id && service
end) do
nil -> {:error, {:decision_service_not_found, name_or_id}}
service -> {:ok, service}
end
end
end
defp service_arguments(model, service, args, outer_context) do
references = service.input_data ++ service.input_decisions
with {:ok, inputs} <- service_inputs(model, references),
names = Enum.map(inputs, & &1.name),
{:ok, values} <- normalize_service_args(args, names),
{:ok, values} <- coerce_input_values(values, inputs, model) do
{:ok, Map.merge(outer_context, Map.new(Enum.zip(names, values)))}
end
end
defp coerce_input_values(values, inputs, model) do
types = Enum.map(inputs, fn input -> input.variable && input.variable.type_ref end)
coerce_values(values, types, model)
end
defp coerce_parameter_values(values, parameters, model) do
coerce_values(values, Enum.map(parameters, & &1.type_ref), model)
end
defp coerce_values(values, types, model) do
Enum.zip(values, types)
|> Enum.reduce_while({:ok, []}, fn {value, type}, {:ok, result} ->
case coerce_type(value, type, model) do
{:ok, coerced} -> {:cont, {:ok, result ++ [coerced]}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp coerce_type(value, nil, _model), do: {:ok, value}
defp coerce_type(nil, _type, _model), do: {:ok, nil}
defp coerce_type([value], type, model) when type in ["string", "number", "boolean"],
do: coerce_type(value, type, model)
defp coerce_type(value, "string", _model) when is_binary(value), do: {:ok, value}
defp coerce_type(%Decimal{} = value, "number", _model), do: {:ok, value}
defp coerce_type(value, "number", _model) when is_binary(value) do
case Decimal.parse(value) do
{number, ""} -> {:ok, number}
_ -> {:error, {:type_error, "number"}}
end
end
defp coerce_type(value, "boolean", _model) when is_boolean(value), do: {:ok, value}
defp coerce_type("true", "boolean", _model), do: {:ok, true}
defp coerce_type("false", "boolean", _model), do: {:ok, false}
defp coerce_type(%Date{} = value, "date", _model), do: {:ok, value}
defp coerce_type(value, "date", _model) when is_binary(value), do: Date.from_iso8601(value)
defp coerce_type(%Boxic.FEEL.Time{} = value, "time", _model), do: {:ok, value}
defp coerce_type(%Time{} = value, "time", _model), do: {:ok, value}
defp coerce_type(value, "time", _model) when is_binary(value) do
case Boxic.FEEL.Time.parse(value) do
{:ok, time} -> {:ok, time}
:error -> {:error, {:type_error, "time"}}
end
end
defp coerce_type(%Boxic.FEEL.DateTime{} = value, "date and time", _model), do: {:ok, value}
defp coerce_type(%Boxic.FEEL.DateTime{} = value, "dateTime", _model), do: {:ok, value}
defp coerce_type(%DateTime{} = value, "date and time", _model), do: {:ok, value}
defp coerce_type(%DateTime{} = value, "dateTime", _model), do: {:ok, value}
defp coerce_type(%NaiveDateTime{} = value, "date and time", _model), do: {:ok, value}
defp coerce_type(%NaiveDateTime{} = value, "dateTime", _model), do: {:ok, value}
defp coerce_type(value, type, _model)
when is_binary(value) and type in ["date and time", "dateTime"] do
case Boxic.FEEL.DateTime.parse(value) do
{:ok, datetime} -> {:ok, datetime}
:error -> {:error, {:type_error, type}}
end
end
defp coerce_type(%Boxic.FEEL.Duration{} = value, "duration", _model), do: {:ok, value}
defp coerce_type(value, type, _model)
when is_binary(value) and
type in [
"duration",
"dayTimeDuration",
"yearMonthDuration",
"days and time duration",
"years and months duration"
] do
case Boxic.FEEL.Duration.parse_iso8601(value) do
{:ok, duration} -> coerce_duration_kind(duration, type)
{:error, _reason} -> {:error, {:type_error, type}}
end
end
defp coerce_type(%Boxic.FEEL.Duration{kind: :day_time} = value, "dayTimeDuration", _model),
do: {:ok, value}
defp coerce_type(
%Boxic.FEEL.Duration{kind: :year_month} = value,
"yearMonthDuration",
_model
),
do: {:ok, value}
defp coerce_type(
%Boxic.FEEL.Duration{kind: :day_time} = value,
"days and time duration",
_model
),
do: {:ok, value}
defp coerce_type(
%Boxic.FEEL.Duration{kind: :year_month} = value,
"years and months duration",
_model
),
do: {:ok, value}
defp coerce_type(value, "Any", _model), do: {:ok, value}
defp coerce_type(value, type, model) do
case find_item_definition(model, type) do
nil -> {:error, {:type_error, type}}
definition -> coerce_item_definition(value, definition, model)
end
end
defp find_item_definition(model, type) do
qualified =
case String.split(type, ".", parts: 2) do
[alias_name, local_name] ->
Enum.find_value(model.imports, fn
{namespace, %{name: ^alias_name}} ->
Map.get(model.item_definitions, namespace <> "#" <> local_name)
_entry ->
nil
end)
_ ->
nil
end
qualified ||
Enum.find_value(model.item_definitions, fn {_id, definition} ->
definition.name == type && definition
end)
end
defp coerce_duration_kind(duration, "duration"), do: {:ok, duration}
defp coerce_duration_kind(%Boxic.FEEL.Duration{kind: :day_time} = duration, type)
when type in ["dayTimeDuration", "days and time duration"],
do: {:ok, duration}
defp coerce_duration_kind(%Boxic.FEEL.Duration{kind: :year_month} = duration, type)
when type in ["yearMonthDuration", "years and months duration"],
do: {:ok, duration}
defp coerce_duration_kind(_duration, type), do: {:error, {:type_error, type}}
defp coerce_item_definition(value, %{is_collection: true} = definition, model) do
values = if is_list(value), do: value, else: [value]
values
|> Enum.reduce_while({:ok, []}, fn item, {:ok, result} ->
case coerce_type(item, definition.type_ref || "Any", model) do
{:ok, coerced} -> {:cont, {:ok, result ++ [coerced]}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp coerce_item_definition(value, %{components: components} = definition, model)
when components != [] do
if is_map(value) and not is_struct(value) do
Enum.reduce_while(components, {:ok, value}, fn component, {:ok, result} ->
case Map.fetch(value, component.name) do
{:ok, item} ->
case coerce_item_component(item, component, model) do
{:ok, coerced} -> {:cont, {:ok, Map.put(result, component.name, coerced)}}
{:error, _reason} = error -> {:halt, error}
end
:error ->
if definition.id,
do: {:halt, {:ok, nil}},
else: {:halt, {:error, {:missing_context_component, component.name}}}
end
end)
else
{:error, {:type_error, :context}}
end
end
defp coerce_item_definition(value, definition, model),
do: coerce_item_base(value, definition.type_ref, model)
defp coerce_item_component(value, %{is_collection: true} = component, model) do
values = if is_list(value), do: value, else: [value]
Enum.reduce_while(values, {:ok, []}, fn item, {:ok, result} ->
case coerce_type(item, component.type_ref || "Any", model) do
{:ok, coerced} -> {:cont, {:ok, result ++ [coerced]}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp coerce_item_component(value, component, model),
do: coerce_type(value, component.type_ref || "Any", model)
defp coerce_item_base(%Boxic.FEEL.Function{} = value, "function:" <> _output_type, _model),
do: {:ok, value}
defp coerce_item_base({:external_function, _} = value, "function:" <> _output_type, _model),
do: {:ok, value}
defp coerce_item_base({:builtin, _} = value, "function:" <> _output_type, _model),
do: {:ok, value}
defp coerce_item_base(value, "function:" <> output_type, model),
do: coerce_type(value, output_type, model)
defp coerce_item_base(value, type, model), do: coerce_type(value, type, model)
defp service_inputs(model, references) do
Enum.reduce_while(references, {:ok, []}, fn id, {:ok, inputs} ->
node = Map.get(model.input_data, id) || Map.get(model.decisions, id)
if node,
do: {:cont, {:ok, inputs ++ [node]}},
else: {:halt, {:error, {:unresolved_service_input, id}}}
end)
end
defp normalize_service_args(args, names) do
if Enum.all?(args, &match?({:named_arg, _, _}, &1)) do
values = Map.new(args, fn {:named_arg, name, value} -> {name, value} end)
if Map.keys(values) |> MapSet.new() == MapSet.new(names),
do: {:ok, Enum.map(names, &Map.fetch!(values, &1))},
else: {:error, {:decision_service_argument_mismatch, names, Map.keys(values)}}
else
if length(args) == length(names),
do: {:ok, args},
else: {:error, {:decision_service_arity, length(names), length(args)}}
end
end
defp evaluate_service_outputs(model, ids, context, memo) do
Enum.reduce_while(ids, {:ok, [], memo}, fn id, {:ok, outputs, memo} ->
case Map.fetch(model.decisions, id) do
{:ok, decision} ->
case evaluate_decision(model, decision, context, memo, %{}) do
{:ok, value, memo} -> {:cont, {:ok, outputs ++ [{decision.name, value}], memo}}
{:error, _reason} = error -> {:halt, error}
end
:error ->
{:halt, {:error, {:unresolved_service_output, id}}}
end
end)
end
defp find_decision(model, name_or_id) do
case Map.fetch(model.decisions, name_or_id) do
{:ok, decision} ->
{:ok, decision}
:error ->
case Enum.find_value(model.decisions, fn {_id, decision} ->
decision.name == name_or_id && decision
end) do
nil -> {:error, {:decision_not_found, name_or_id}}
decision -> {:ok, decision}
end
end
end
end