A DMN engine built on the FEEL library.

FEEL stays a separate package (:feel). This application reads a model (load/2), compiles it to prepared FEEL expressions (Dmn.Compiler) and evaluates the required units (Dmn.Run).

A model is untrusted input here, and so is every model it imports: see Dmn.XML and Dmn.Limits. Public operations use Feel.Worker by default. guard: configures the policy; sandbox: false skips the outer worker for trusted input, keeping model and FEEL limits, XML parsing guards and optional table-check guards.

Summary

Functions

Compiles a model (Dmn.Compiler): every text of it is parsed, every name resolved and every expression typed and prepared, once. Accepts XML bytes or a loaded Dmn.Model; bytes include loading in the same worker deadline. Containment is on by default. guard: configures it and sandbox: false opts out for trusted input. What is wrong with the model is in the diagnostics of the result, and the model is compiled all the same, unless validate: true is given, which refuses a model that has an error (validate/1). extensions: switches on additions to DMN 1.6 (Dmn.Extension), such as the names of DMN 1.1 for the date and time types; a name or module that is not an extension is :invalid_options.

Evaluates the decision called name with the inputs, in a compiled model: {:ok, value, diagnostics}.

Evaluates several decisions at once, with the inputs, in a compiled model: the Dmn.Result, which has the value of each and the diagnostics of everything that ran. A decision that more than one of them needs is run once.

Calls a business knowledge model or a decision service of a compiled model as a function (Dmn.Run.invoke/4): the arguments are a map of the names of the parameters to values, or a list in the order of the parameters. {:ok, value, diagnostics}.

Reads a DMN model from the bytes of an XML document.

Prepares repeated evaluation of one decision: resolves its name and dependency plan once, with fixed run options.

Evaluates a prepared decision with fresh inputs: {:ok, value, diagnostics} or {:error, Dmn.Error.t()}.

Whether a compiled model is valid: :ok if the compile found no error in it, and {:error, %Dmn.Error{kind: :model, code: :invalid_model}} with the errors (details.diagnostics) if it did.

Functions

compile(input, options \\ [])

@spec compile(binary() | Dmn.Model.t(), keyword()) ::
  {:ok, Dmn.Compiled.t()} | {:error, Dmn.Error.t()}

Compiles a model (Dmn.Compiler): every text of it is parsed, every name resolved and every expression typed and prepared, once. Accepts XML bytes or a loaded Dmn.Model; bytes include loading in the same worker deadline. Containment is on by default. guard: configures it and sandbox: false opts out for trusted input. What is wrong with the model is in the diagnostics of the result, and the model is compiled all the same, unless validate: true is given, which refuses a model that has an error (validate/1). extensions: switches on additions to DMN 1.6 (Dmn.Extension), such as the names of DMN 1.1 for the date and time types; a name or module that is not an extension is :invalid_options.

features: switches on what DMN 1.6 does not have (Dmn.Features, DD-29): :recursion, which lets a business knowledge model or a decision service invoke itself or one that invokes it, and enables guarded recursive item definitions with complete finite-value checks (DD-22). Other features are the additions of Dmn.Extension, such as the names of DMN 1.1 for the date and time types, by name or as modules. Only the caller can say so. A name that is no feature is :invalid_options. The spellings recursion: true and extensions: [...] are the same, and add to it.

The options are those of Dmn.Compiler.compile/2 (:resolver, :limits, :feel_limits, :check_tables, :backend, and the features above), and guard: (Feel.Worker: deadline, memory and transfer limits). Options that are not a keyword list, and a guard: that is not a valid worker policy, are {:error, %Dmn.Error{kind: :compile, code: :invalid_options}}, as for evaluate/4 with kind: :run: nothing raises for them (DD-36). So are limits: that are neither a Dmn.Limits nor a keyword list of its fields, and feel_limits: that is not a Feel.Limits (DD-39), with the option in details.option.

iex> xml = ~s(<definitions xmlns="https://www.omg.org/spec/DMN/20240513/MODEL/" name="m" namespace="urn:m"><decision id="d" name="Double"><variable name="Double" typeRef="number"/><informationRequirement><requiredInput href="#i"/></informationRequirement><literalExpression><text>Amount * 2</text></literalExpression></decision><inputData id="i" name="Amount"><variable name="Amount" typeRef="number"/></inputData></definitions>)
iex> {:ok, model} = Dmn.load(xml)
iex> {:ok, compiled} = Dmn.compile(model)
iex> {:ok, value, []} = Dmn.evaluate(compiled, "Double", %{"Amount" => Decimal.new(21)})
iex> Decimal.equal?(value, 42)
true

evaluate(compiled, name, inputs \\ %{}, options \\ [])

@spec evaluate(Dmn.Compiled.t(), String.t(), map(), keyword()) ::
  {:ok, Feel.Value.t(), [Dmn.Diagnostic.t()]} | {:error, Dmn.Error.t()}

Evaluates the decision called name with the inputs, in a compiled model: {:ok, value, diagnostics}.

Dmn.Run says what the inputs are and what an error is. Options: :mode (:lenient or :strict), the options of a run of FEEL (:clock, :entropy, :limits), and guard: (Feel.Worker: deadline, memory and transfer limits). What a caller got from outside can be given as it is, and nothing raises (DD-36): a name that is not a string is :unknown_decision, inputs that are not a map of names are :invalid_inputs, and options that are not a keyword list, a mode that is none, and a guard: that is not a valid worker policy are :invalid_options, all with kind: :run. Options also accept sandbox: false for trusted input. Unknown keys, repeated keys and invalid modes are rejected before selection or worker startup. Guard policy keys must be unique too.

evaluate_all(compiled, names, inputs \\ %{}, options \\ [])

@spec evaluate_all(Dmn.Compiled.t(), [String.t()], map(), keyword()) ::
  {:ok, Dmn.Result.t()} | {:error, Dmn.Error.t()}

Evaluates several decisions at once, with the inputs, in a compiled model: the Dmn.Result, which has the value of each and the diagnostics of everything that ran. A decision that more than one of them needs is run once.

Takes the options of evaluate/4, and nothing raises for them or for what a caller got from outside: names that are not a list, and inputs that are not a map, are :invalid_inputs, and a name in the list that is not a string is :unknown_decision (DD-36).

invoke(compiled, name, arguments, options \\ [])

@spec invoke(Dmn.Compiled.t(), String.t(), map() | list(), keyword()) ::
  {:ok, Feel.Value.t(), [Dmn.Diagnostic.t()]} | {:error, Dmn.Error.t()}

Calls a business knowledge model or a decision service of a compiled model as a function (Dmn.Run.invoke/4): the arguments are a map of the names of the parameters to values, or a list in the order of the parameters. {:ok, value, diagnostics}.

A service returns its single output value, a context keyed by canonical decision names for several outputs, or an empty context for no outputs. A missing or Any service result declaration keeps the result type derived from its output decisions. A readable non-Any declaration controls the final conversion; a function item on the service variable contributes its output type, while a function-valued output decision keeps its function layer. Output and service constraints are enforced at their respective boundaries, including recursive descendant constraints when recursion is enabled.

A service inherits faults from its input data, input decisions, encapsulated decisions and output decisions. These are reported even when the caller supplies the inputs: strict mode stops on a fault, and lenient mode reports it and continues. The same contracts apply when a decision calls the service.

Takes guard: as evaluate/4 does. What is not a function that can be called (a decision, a knowledge model with no logic, an external one), a name that is not a string (:unknown_decision), arguments of another shape and options that are not what they should be are an error value (:not_a_function, :invalid_arguments, :invalid_options) and not an exception.

A call that ends on a limit of FEEL is that error (:limit_exceeded, with details.kind: :limit), as a run of a decision is; :strict is the code of an error in the :strict mode, and of that only (DD-36).

load(xml, options \\ [])

@spec load(binary(), keyword()) :: {:ok, Dmn.Model.t()} | {:error, Dmn.Error.t()}

Reads a DMN model from the bytes of an XML document.

The model is data, as the document says it: nothing is resolved or compiled yet. Options: :limits, a Dmn.Limits.t/0 or the fields of one to replace; and :base_uri, the URI the document was read from (Dmn.Model has it as base_uri), which a resolver that resolves a relative locationURI of its imports against the document that has them needs (Dmn.Resolver). It is not read from the document and is not opened. Unknown and repeated option keys are rejected. Options that are not a keyword list, a :base_uri that is not a string, and :limits that are neither a Dmn.Limits nor a keyword list of its fields (positive integers) are {:error, %Dmn.Error{kind: :model, code: :invalid_options}}: nothing raises for them (DD-39).

iex> xml = ~s(<definitions xmlns="https://www.omg.org/spec/DMN/20240513/MODEL/" name="m" namespace="urn:m"><inputData id="i" name="x"/></definitions>)
iex> {:ok, model} = Dmn.load(xml)
iex> Enum.map(model.input_data, & &1.name)
["x"]
iex> {:error, %Dmn.Error{code: :doctype_not_allowed}} = Dmn.load(~s(<!DOCTYPE a [<!ENTITY x "y">]><a/>))

load!(xml, options \\ [])

@spec load!(binary(), keyword()) :: Dmn.Model.t()

Like load/2, and raises the Dmn.Error.

prepare_call(compiled, name, options \\ [])

@spec prepare_call(Dmn.Compiled.t(), String.t(), keyword()) ::
  {:ok, Dmn.PreparedCall.t()} | {:error, Dmn.Error.t()}

Prepares repeated evaluation of one decision: resolves its name and dependency plan once, with fixed run options.

Takes the options of evaluate/4, including :sandbox and :guard, which every run then applies. Unknown or ambiguous names and invalid mode, sandbox or guard options are errors at preparation. Inputs are supplied to run/2, which returns the same value and diagnostics as evaluate/4. Input validation, declared types, limits, diagnostics and the clock of each run still apply. A supplied clock function is called at execution, not preparation. This selects existing compiled expressions; it does not generate more code. Prepare a new call to change the decision, model or options. This evaluates an element; use invoke/4 to invoke a BKM or decision service as a function.

{:ok, call} = Dmn.prepare_call(compiled, "Double", mode: :strict)
{:ok, value, diagnostics} = Dmn.run(call, %{"Amount" => Decimal.new(21)})

run(call, inputs \\ %{})

@spec run(Dmn.PreparedCall.t(), map()) ::
  {:ok, Feel.Value.t(), [Dmn.Diagnostic.t()]} | {:error, Dmn.Error.t()}

Evaluates a prepared decision with fresh inputs: {:ok, value, diagnostics} or {:error, Dmn.Error.t()}.

Input names are validated and normalized on every call, and intermediate values and diagnostics are never shared between runs. Inside a worker (the default, Feel.Worker) the request is selected and admitted as evaluate/4 does it, so the worker is given the part of the model the decision needs and not the prepared plan; with sandbox: false (or inside a worker that is running already) the plan from prepare_call/3 is used and no decision lookup or planning is repeated.

validate(compiled)

@spec validate(Dmn.Compiled.t()) :: :ok | {:error, Dmn.Error.t()}

Whether a compiled model is valid: :ok if the compile found no error in it, and {:error, %Dmn.Error{kind: :model, code: :invalid_model}} with the errors (details.diagnostics) if it did.

compile/2 normally retains model defects as diagnostics, as DMN has an element that cannot be evaluated be null (7.3.8), and a run of such a model goes on. Invalid caller options, resource limits, and validate: true can instead refuse compilation. So a compile that succeeded, and a run that did not end in :strict mode, do not say that the model is valid: an element that no decision asked for is not run, and a run in lenient mode goes on. A caller that wants a model that is valid asks this once, or gives validate: true to compile/2. Confirmed unresolved names also refuse a model, including names in unreachable branches. Other warnings and infos do not.

iex> xml = ~s(<definitions xmlns="https://www.omg.org/spec/DMN/20240513/MODEL/" name="m" namespace="urn:m"><decision id="d" name="D"><variable name="D"/><informationRequirement><requiredInput href="#nope"/></informationRequirement><literalExpression><text>1</text></literalExpression></decision></definitions>)
iex> {:ok, model} = Dmn.load(xml)
iex> {:ok, compiled} = Dmn.compile(model)
iex> {:error, %Dmn.Error{code: :invalid_model, details: %{count: 1}}} = Dmn.validate(compiled)
iex> {:error, %Dmn.Error{code: :invalid_model}} = Dmn.compile(model, validate: true)