The item definitions of a DMN model, read into the types and the allowed values of a Feel.Scope (DMN 1.6,
section 7.3.2, "Item definition"; D-99).
feel has no DMN reader, so a definition is given as data, the way a model layer reads it from the model:
%{
name: "tPerson",
components: [
%{name: "name", type_ref: "string"},
%{name: "age", type_ref: "number", allowed_values: "[0..150]"}
]
}| Key | Is |
|---|---|
:name | the name of the item definition (required) |
:type_ref | a built-in type ("string", "date and time", "Any"...), or the name of another item definition |
:collection? | true for isCollection="true": a list of the type |
:components | the itemComponents: definitions of their own, with a :name; the type is a context of them |
:allowed_values | the allowedValues, the source text of a list of unary tests ("[0..255]", "\"FOO\", \"BAR\""); for a collection they are for each element |
:type_constraint | the typeConstraint, the same for the value as a whole: for a collection, the list ("count(?) > 0") |
:function | a functionItem: %{output: type_ref, parameters: [%{name: name, type_ref: type_ref}]} |
A definition has one of :type_ref, :components and :function, or none of them, which is the type Any (it
constrains nothing). The definitions may refer to one another in any order, and a component may be of a type that
is defined later.
What is read
- The type of each definition (
types/1): the built-in type, a list of it for a collection, a context of the types of its components ({:context, %{"name" => :string, "age" => :number}}), or a function type ({:function, [:string, :number], :string}). A type that refers to another is the type of that. A context type is whatinstance ofand the declared types of a scope make of it: a value is an instance if it has the components ({a: 1, b: 2, c: 3}is an instance of the type ofaandb), and a function type is one of the parameter types and the output type. Allowed values play no part ininstance of:256 instance of t255istruefor a number with the allowed values[0..255](TCK, 0070number_013). - The constraints (
constraints/1) that a variable declared with the definition has to satisfy (D-138, D-51, D-161): its allowed values and its type constraint, those of the definition and those of the one it refers to, and those of the components, each for the member it is for (steps: the key of a component, and:eachfor the elements of a collection). They all have to hold, and each is a part of the type of what it is for, when it is read. A collection'sallowedValuesare for each element, as they are for a variable declared as a list, and so are those of a collection of collections for the elements of the elements. - What cannot be read (
problems/1). An undefined reference or incompatible definition kinds leave a definition without a type. Recursive references are rejected by default (:recursive). - Legacy approximate recursion, with
recursive: :any, replaces the reference closing a loop withAny(D-189). FortNodewithvalue: numberandchildren: list<tNode>, the cut can producecontext<children: list<Any>, value: number>. This deliberately accepts descendants that the full declaration would reject. It is distinct from guarded data validation. - Guarded recursive data, with
recursive: :guarded, retains a finite graph instead of discarding back references. Every cycle must consume a context member or list element and cannot cross a function signature. Runtime membership and declaration constraints check every finite descendant. Analysis uses only a finite upper approximation and retains uncertain recursive checks. Alias-only cycles report:unguarded_recursion; cycles through function signatures report:recursive_function. A finite function item may still accept or return recursive data. Type conformance between different recursive graphs is conservative.
A scope
scope/2 makes a Feel.Scope of the variables of a model, as name => type_ref: the types are all the item
definitions that could be read, so that x instance of tFooBar and function(a: tPerson) mean what they say,
and each variable is of its type, with the allowed values of it. A variable that cannot be given its type is left
out and reported, so that the caller can tell the expressions that read it.
iex> definitions = Feel.ItemDefinitions.new([%{name: "tAge", type_ref: "number", allowed_values: "[0..150]"}])
iex> {scope, unread} = Feel.ItemDefinitions.scope(definitions, %{"age" => "tAge", "name" => "string"})
iex> {scope.names, Feel.Scope.allowed_values(scope), unread}
{%{"age" => :number, "name" => :string}, %{"age" => "[0..150]"}, %{}}
Summary
Types
What a value has to satisfy: unary tests (source) of the kind :allowed_values or :type_constraint, for the
member of the value at the end of steps (see Feel.Scope.step/0), or for the value itself with none.
An item definition, or an item component, as data.
Why a definition has no type.
Functions
The scope that has the types of the definitions and no variable: what scope/3 adds the variables of an expression to.
What a value of each item definition that has any has to satisfy, as the source text of unary tests: its own, those of the definition it refers to, and those of its components, by the member they are for.
Reads the item definitions of a model. Raises ArgumentError for two with one name.
Why the item definitions that have no type have none, by name. They are not in types/1.
A scope of the variables of a model, given as name => typeRef, and the ones that could not be given a type, with why.
The type of each item definition that could be read, by its name.
The type and the constraints of a variable that is declared with this typeRef, or why it cannot be had.
Types
@type constraint() :: %{ steps: [Feel.Scope.step()], kind: Feel.Scope.kind(), source: String.t() }
What a value has to satisfy: unary tests (source) of the kind :allowed_values or :type_constraint, for the
member of the value at the end of steps (see Feel.Scope.step/0), or for the value itself with none.
@type definition() :: %{ :name => String.t(), optional(:type_ref) => String.t() | nil, optional(:collection?) => boolean(), optional(:components) => [definition()], optional(:allowed_values) => String.t() | nil, optional(:type_constraint) => String.t() | nil, optional(:function) => %{ optional(:output) => String.t() | nil, optional(:parameters) => [ %{:name => String.t(), optional(:type_ref) => String.t() | nil} ] } }
An item definition, or an item component, as data.
@type problem() :: {:undefined_type, String.t()} | :recursive | :more_than_one_kind | :duplicate_component | :unguarded_recursion | :recursive_function
Why a definition has no type.
@type t() :: %Feel.ItemDefinitions{ constraints: %{optional(String.t()) => [constraint()]}, problems: %{optional(String.t()) => [problem()]}, types: %{optional(String.t()) => Feel.Type.t()} }
Functions
@spec base_scope(t()) :: Feel.Scope.t()
The scope that has the types of the definitions and no variable: what scope/3 adds the variables of an expression to.
@spec constraints(t()) :: %{optional(String.t()) => [constraint()]}
What a value of each item definition that has any has to satisfy, as the source text of unary tests: its own, those of the definition it refers to, and those of its components, by the member they are for.
iex> definitions = Feel.ItemDefinitions.new([
...> %{name: "tPerson", components: [%{name: "age", type_ref: "number", allowed_values: "[0..150]"}]},
...> %{name: "tPeople", type_ref: "tPerson", collection?: true, type_constraint: "count(?) > 0"}
...> ])
iex> Feel.ItemDefinitions.constraints(definitions)["tPeople"]
[
%{steps: [:each, "age"], kind: :allowed_values, source: "[0..150]"},
%{steps: [], kind: :type_constraint, source: "count(?) > 0"}
]
@spec new([definition()], [{:recursive, :error | :any | :guarded}]) :: t()
Reads the item definitions of a model. Raises ArgumentError for two with one name.
The option :recursive is :error by default, and a definition that refers to itself has no type (problems/1). With
:any it has the type that the loop is cut at, the reference that closes it being Any (D-189): the definitions are
read in the order of their names, so that what the cut is does not depend on the order the model gave them in.
iex> definitions = Feel.ItemDefinitions.new([
...> %{name: "tNames", type_ref: "string", collection?: true},
...> %{name: "tPerson", components: [%{name: "name", type_ref: "string"}, %{name: "friends", type_ref: "tNames"}]}
...> ])
iex> Feel.ItemDefinitions.types(definitions)
%{"tNames" => {:list, :string}, "tPerson" => {:context, %{"name" => :string, "friends" => {:list, :string}}}}The option :guarded retains guarded data recursion as a finite graph, checks full finite values and recursive
component constraints, and rejects unguarded or function-type cycles. Type relations remain conservative.
iex> definitions = Feel.ItemDefinitions.new([
...> %{name: "Node", components: [%{name: "next", type_ref: "Node"}]}
...> ], recursive: :guarded)
iex> type = Feel.ItemDefinitions.types(definitions)["Node"]
iex> {Feel.Type.value_conforms?(%{"next" => %{"next" => nil}}, type),
...> Feel.Type.value_conforms?(%{"next" => %{}}, type)}
{true, false}
Why the item definitions that have no type have none, by name. They are not in types/1.
@spec scope(t(), %{optional(String.t()) => String.t() | nil}, Feel.Scope.t() | nil) :: {Feel.Scope.t(), %{optional(String.t()) => [problem()]}}
A scope of the variables of a model, given as name => typeRef, and the ones that could not be given a type, with why.
The types of the scope are all the item definitions that could be read, so that their names mean something in an
expression; the names are the variables, each of its declared type, and their constraints are those of the
definition it is declared with, for the variable and for its members.
Making the types of a scope costs what the definitions are (every name is canonicalized), so a caller that makes a scope for
each of many expressions over one set of definitions makes base_scope/1 once and gives it as the third argument: the scope
is the same, and the types are not made again.
iex> definitions = Feel.ItemDefinitions.new([%{name: "tAge", type_ref: "number", allowed_values: "[0..150]"}])
iex> variables = %{"age" => "tAge", "nobody" => "tNowhere"}
iex> {scope, unread} = Feel.ItemDefinitions.scope(definitions, variables)
iex> {Feel.Scope.fetch_name(scope, "age"), Feel.Scope.allowed_values(scope), Map.keys(unread)}
{{:ok, :number}, %{"age" => "[0..150]"}, ["nobody"]}
iex> base = Feel.ItemDefinitions.base_scope(definitions)
iex> {^scope, ^unread} = Feel.ItemDefinitions.scope(definitions, variables, base)Use scope/3 to carry guarded recursive constraints into an expression. Passing only the type
returned by types/1 checks structure, but does not attach the declaration's allowed values or
type constraints. Under the default declarations: :enforce, an invalid descendant makes the
bound value null; its diagnostic identifies the concrete path. Other declaration stances
retain their ordinary meaning (Feel.Scope).
iex> definitions = Feel.ItemDefinitions.new([
...> %{name: "Node", components: [
...> %{name: "value", type_ref: "number", allowed_values: "[0..9]"},
...> %{name: "next", type_ref: "Node"}
...> ]}
...> ], recursive: :guarded)
iex> {scope, unread} = Feel.ItemDefinitions.scope(definitions, %{"node" => "Node"})
iex> unread
%{}
iex> {:ok, expression} = Feel.prepare("node", scope: scope)
iex> bad = %{"value" => Decimal.new(1), "next" => %{"value" => Decimal.new(20), "next" => nil}}
iex> {:ok, nil, [diagnostic]} = Feel.run(expression, %{"node" => bad})
iex> {diagnostic.code, diagnostic.details.path}
{:value_not_allowed, ["next", "value"]}For an expression's result boundary, use variable/2 to obtain both the type and its constraints,
and pass them as :result_type and :result_allowed_values to Feel.prepare/2.
@spec types(t()) :: %{optional(String.t()) => Feel.Type.t()}
The type of each item definition that could be read, by its name.
@spec variable(t(), String.t() | nil) :: {:ok, Feel.Type.t(), [constraint()]} | {:error, [problem()]}
The type and the constraints of a variable that is declared with this typeRef, or why it cannot be had.
nil is no declaration, which is the type Any. A built-in type has no constraints.
iex> definitions = Feel.ItemDefinitions.new([%{name: "tFooBar", type_ref: "string", allowed_values: ~s("FOO", "BAR")}])
iex> Feel.ItemDefinitions.variable(definitions, "tFooBar")
{:ok, :string, [%{steps: [], kind: :allowed_values, source: ~s("FOO", "BAR")}]}
iex> Feel.ItemDefinitions.variable(definitions, "number")
{:ok, :number, []}
iex> Feel.ItemDefinitions.variable(definitions, "tNowhere")
{:error, [{:undefined_type, "tNowhere"}]}
iex> Feel.ItemDefinitions.variable(definitions, "list<number!>")
{:ok, {:list, {:non_null, :number}}, []}
iex> Feel.ItemDefinitions.variable(definitions, "tFooBar!")
{:ok, {:non_null, :string}, [%{steps: [], kind: :allowed_values, source: ~s("FOO", "BAR")}]}
iex> Feel.ItemDefinitions.variable(definitions, "list<tFooBar>")
{:error, [{:undefined_type, "list<tFooBar>"}]}