Feel.ItemDefinitions (feel v0.1.0)

Copy Markdown View Source

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]"}
  ]
}
KeyIs
:namethe name of the item definition (required)
:type_refa 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
:componentsthe itemComponents: definitions of their own, with a :name; the type is a context of them
:allowed_valuesthe allowedValues, the source text of a list of unary tests ("[0..255]", "\"FOO\", \"BAR\""); for a collection they are for each element
:type_constraintthe typeConstraint, the same for the value as a whole: for a collection, the list ("count(?) > 0")
:functiona 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 what instance of and 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 of a and b), and a function type is one of the parameter types and the output type. Allowed values play no part in instance of: 256 instance of t255 is true for a number with the allowed values [0..255] (TCK, 0070 number_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 :each for 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's allowedValues are 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 with Any (D-189). For tNode with value: number and children: list<tNode>, the cut can produce context<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.

t()

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

constraint()

@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.

definition()

@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.

problem()

@type problem() ::
  {:undefined_type, String.t()}
  | :recursive
  | :more_than_one_kind
  | :duplicate_component
  | :unguarded_recursion
  | :recursive_function

Why a definition has no type.

t()

@type t() :: %Feel.ItemDefinitions{
  constraints: %{optional(String.t()) => [constraint()]},
  problems: %{optional(String.t()) => [problem()]},
  types: %{optional(String.t()) => Feel.Type.t()}
}

Functions

base_scope(item_definitions)

@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.

constraints(item_definitions)

@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"}
]

new(definitions, options \\ [])

@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}

problems(item_definitions)

@spec problems(t()) :: %{optional(String.t()) => [problem()]}

Why the item definitions that have no type have none, by name. They are not in types/1.

scope(definitions, variables, base \\ nil)

@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.

types(item_definitions)

@spec types(t()) :: %{optional(String.t()) => Feel.Type.t()}

The type of each item definition that could be read, by its name.

variable(definitions, type_ref)

@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>"}]}