Feel.Runtime.AllowedValues (feel v0.1.0)

Copy Markdown View Source

The verification of a value against allowed values (D-138).

A variable of a Feel.Scope can have allowed values: unary tests that a bound value has to satisfy, and so can the result of an expression. Feel.Analysis.Analyzer puts one :verify_bindings node around the expression, with the variables it reads that have them, and an :allowed node around the whole expression for its result. A backend calls verify_bindings/3 for the first when the run starts, and verify/5 for the second. Both are functions of the runtime, and not a part of a backend, because generated code may call only Feel.Runtime and both backends have to give the same result.

  • null is exempt, as it is a value of every type.
  • A value that the tests accept is the value.
  • A value that they do not is null, with an error (:value_not_allowed) that ends a run in strict mode. That is :enforce. In the mode {:report, severity} (D-138) the value stays as it is and only the diagnostic is given, with that severity: an :error ends a strict run, and a :warning does not.
  • A variable whose declared type is marked ! (D-48) cannot be null, so a value that the tests do not accept ends the run with an error of kind :input in the mode :enforce, instead of becoming null. The entry of the variable says so (the mark :non_null).
  • A variable declared as a list (list<number>) has the tests applied to each element, as the allowedValues of a DMN item definition with isCollection are (notes 03, section 4.3). The entry says so (the mark :each). A null element is exempt, as a null is. The first element that the tests do not accept makes the whole value not allowed, which is null with the error :value_not_allowed that says which element it is, and the report in the mode {:report, severity} is the same one error.
  • The constraint of a member of a variable (a component of a context, the elements of a list in it, D-161 step E) has the tests applied to what is at the end of its path (the mark {:path, steps}), :each in the steps being each element of a list. A member that is not there, or is null, is exempt; the first that the tests do not accept makes the whole variable not allowed, as for an element, and the error says where it is (details.path, [2, "total"] for the total of the second element).

Guarded recursive declarations use graph selectors in those paths (Feel.Scope.step/0). A selector visits every finite occurrence of its target declaration, including aliases and nested collections. Diagnostics contain the concrete member names and one-based list indexes, rather than the graph selector. Null occurrences are exempt. Missing members are rejected by structural conformance, not by allowed-value verification; membership alone does not enforce these constraints.

Traversal is incremental and stops at the first rejected occurrence. Reversed paths share their tails while checks succeed; only the failing diagnostic constructs a complete forward path.

The tests are run with a lenient environment of their own, and what they report is given the span of the variable (its first read in the expression), or of the expression for its result, since the spans of the tests are those of their own text. The tests are alternatives, and one that does not apply to a value of another kind ([0..150] for a string or a boolean, which the comparison reports as a type mismatch) is not an error of the run when another accepts the value. So the errors that the tests report are not given when the value is accepted, and when it is not, they are the causes of the one :value_not_allowed (in its details), and the first of them is its hint. Their warnings and infos are given as they are in the mode :enforce.

The variables are verified once, before anything is evaluated, against the values as they were bound. A test that reads another variable sees it as the expression does (D-170): converted to its declared type and verified, so a variable that is not allowed is null in the test (age with <= max age, for a max age that [18..65] turns down, is not allowed either). That is in the IR of the test, which has the :conform_bindings and :verify_bindings nodes of the variables it reads, so a backend runs it with nothing more; the constraints that are being verified, and the ones whose tests read them, are not applied to a variable that a test reads. The stances that leave a value as it is ({:report, _}) read every variable as it was bound. The expression then sees null for each value that is not allowed.

Summary

Types

Runs the tests of a variable with ? bound to a value, and gives whether they are satisfied.

What is known of a variable besides its tests: that it cannot be null (:non_null), that the tests are for the elements of a list (:each), and the path from the variable down to what the tests are for ({:path, steps}): the key of a member of a context, or :each for every element of a list.

What is done with a value that is not allowed: :enforce makes it null, with an error, and {:report, severity} leaves it and reports it with that severity (D-138).

What is verified: a variable, by its name, or the result of the expression.

Runs the tests with ? bound to the value, and gives whether they are satisfied.

What is verified of a variable: its canonical name, the span of its first read, and its tests. The fourth element is there when the variable has marks: :non_null if its declared type is marked !, and :each if it is declared as a list and the tests are for the elements.

Functions

Reports a value that the tests do not accept, and leaves it as it is (the mode {:report, severity}, D-138). null is exempt. Only the one diagnostic is given, with severity and at span; the errors the tests gave for a value of the wrong kind are its causes.

Verifies a value against the tests of its subject, found at span.

Verifies the bound values of the variables that have allowed values, once, at the start of a run.

Verifies a result that is a list against the tests of its elements (the :allowed node with each: true, D-51).

Verifies a result against the tests of a member of it (the :allowed node with meta.path): the value at the end of steps, a key of a context, or :each for every element of a list, or each element of what is there when each? is true.

Types

binding_tests()

@type binding_tests() :: (Feel.Value.t(), Feel.Runtime.Env.t() ->
                      {boolean(), Feel.Runtime.Env.t()})

Runs the tests of a variable with ? bound to a value, and gives whether they are satisfied.

mark()

@type mark() :: :non_null | :each | {:path, [Feel.Scope.step()]}

What is known of a variable besides its tests: that it cannot be null (:non_null), that the tests are for the elements of a list (:each), and the path from the variable down to what the tests are for ({:path, steps}): the key of a member of a context, or :each for every element of a list.

mode()

@type mode() :: :enforce | {:report, :error | :warning}

What is done with a value that is not allowed: :enforce makes it null, with an error, and {:report, severity} leaves it and reports it with that severity (D-138).

subject()

@type subject() :: Feel.Name.t() | :result

What is verified: a variable, by its name, or the result of the expression.

tests()

@type tests() :: (Feel.Runtime.Env.t() -> {boolean(), Feel.Runtime.Env.t()})

Runs the tests with ? bound to the value, and gives whether they are satisfied.

verification()

What is verified of a variable: its canonical name, the span of its first read, and its tests. The fourth element is there when the variable has marks: :non_null if its declared type is marked !, and :each if it is declared as a list and the tests are for the elements.

Functions

report(env, arg2, subject, span, tests, severity)

@spec report(
  Feel.Runtime.Env.t(),
  Feel.Value.t(),
  subject(),
  Feel.Diagnostic.span(),
  tests(),
  :error | :warning
) :: Feel.Runtime.Env.t()

Reports a value that the tests do not accept, and leaves it as it is (the mode {:report, severity}, D-138). null is exempt. Only the one diagnostic is given, with severity and at span; the errors the tests gave for a value of the wrong kind are its causes.

iex> env = Feel.Runtime.Env.new()
iex> tests = fn env -> {false, env} end
iex> env = Feel.Runtime.AllowedValues.report(env, Decimal.new(200), "age", {0, 3}, tests, :warning)
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> {diagnostic.severity, diagnostic.code, diagnostic.span}
{:warning, :value_not_allowed, {0, 3}}
iex> env = Feel.Runtime.Env.new()
iex> env == Feel.Runtime.AllowedValues.report(env, nil, "age", {0, 3}, tests, :error)
true

verify(env, value, subject, span, tests)

Verifies a value against the tests of its subject, found at span.

iex> env = Feel.Runtime.Env.new()
iex> tests = fn env -> {false, env} end
iex> {nil, ^env} = Feel.Runtime.AllowedValues.verify(env, nil, "age", {0, 3}, tests)
iex> {nil, env} = Feel.Runtime.AllowedValues.verify(env, Decimal.new(200), "age", {0, 3}, tests)
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> {diagnostic.code, diagnostic.span}
{:value_not_allowed, {0, 3}}
iex> {"ok", _} = Feel.Runtime.AllowedValues.verify(env, "ok", "age", {0, 3}, fn env -> {true, env} end)
iex> {nil, env} = Feel.Runtime.AllowedValues.verify(env, "no", :result, {0, 9}, tests)
iex> hd(Enum.reverse(Feel.Runtime.Env.diagnostics(env))).message
"the result is not one of its allowed values"

verify_bindings(env, bindings, verifications, mode \\ :enforce)

@spec verify_bindings(Feel.Runtime.Env.t(), Feel.bindings(), [verification()], mode()) ::
  {Feel.bindings(), Feel.Runtime.Env.t()}

Verifies the bound values of the variables that have allowed values, once, at the start of a run.

verifications has an entry for each variable: its canonical name, the span of its first read in the expression, a function that runs its tests with ? bound to the value it is given, and a list of marks if it has any (:non_null for a type marked !, D-48, and :each for a list whose elements the tests are for), see the top of this module. Returns the bindings that the expression is to run with, which have null for a value that is not allowed, and the environment with what was reported. A variable that is not bound is left for the read to report, and a null is exempt (see verify/5). mode is :enforce, the default, or {:report, severity}, which leaves the bindings as they are and only reports (see report/6).

iex> env = Feel.Runtime.Env.new()
iex> tests = fn value, env -> {Decimal.compare(value, 150) != :gt, env} end
iex> verifications = [{"age", {0, 3}, tests}]
iex> {bindings, env} = Feel.Runtime.AllowedValues.verify_bindings(env, %{"age" => Decimal.new(200), "id" => 7}, verifications)
iex> bindings
%{"age" => nil, "id" => 7}
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> {diagnostic.code, diagnostic.span}
{:value_not_allowed, {0, 3}}
iex> {same, ^env} = Feel.Runtime.AllowedValues.verify_bindings(env, %{"age" => Decimal.new(20)}, verifications)
iex> Decimal.equal?(same["age"], 20)
true
iex> required = [{"age", {0, 3}, tests, [:non_null]}]
iex> {:error, error} = Feel.Runtime.Env.run(env, &Feel.Runtime.AllowedValues.verify_bindings(&1, %{"age" => Decimal.new(200)}, required))
iex> {error.kind, error.code, error.span}
{:input, :value_not_allowed, {0, 3}}
iex> each = [{"ages", {0, 4}, tests, [:each]}]
iex> {bindings, env} = Feel.Runtime.AllowedValues.verify_bindings(Feel.Runtime.Env.new(), %{"ages" => [Decimal.new(20), nil, Decimal.new(200)]}, each)
iex> bindings
%{"ages" => nil}
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> {diagnostic.code, diagnostic.details}
{:value_not_allowed, %{name: "ages", index: 3}}
iex> total = [{"orders", {0, 6}, tests, [{:path, [:each, "total"]}]}]
iex> orders = [%{"total" => Decimal.new(20)}, %{"total" => nil}, %{"total" => Decimal.new(200)}]
iex> {bindings, env} = Feel.Runtime.AllowedValues.verify_bindings(Feel.Runtime.Env.new(), %{"orders" => orders}, total)
iex> bindings
%{"orders" => nil}
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> {diagnostic.message, diagnostic.details}
{~s(the value of "orders[3].total" is not one of its allowed values), %{name: "orders", path: [3, "total"]}}

verify_each(env, value, subject, span, tests)

Verifies a result that is a list against the tests of its elements (the :allowed node with each: true, D-51).

tests runs the tests with ? bound to the element it is given. A null element is exempt, and the first element that the tests do not accept makes the whole result null, with the one error :value_not_allowed that says which element it is (details.index).

iex> env = Feel.Runtime.Env.new()
iex> tests = fn value, env -> {Decimal.compare(value, 150) != :gt, env} end
iex> {same, ^env} = Feel.Runtime.AllowedValues.verify_each(env, [Decimal.new(20), nil], :result, {0, 9}, tests)
iex> same == [Decimal.new(20), nil]
true
iex> {nil, env} = Feel.Runtime.AllowedValues.verify_each(env, [Decimal.new(20), Decimal.new(200)], :result, {0, 9}, tests)
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> {diagnostic.code, diagnostic.message, diagnostic.details}
{:value_not_allowed, "element 2 of the result is not one of its allowed values", %{result: true, index: 2}}

verify_member(env, value, subject, span, steps, each?, tests)

Verifies a result against the tests of a member of it (the :allowed node with meta.path): the value at the end of steps, a key of a context, or :each for every element of a list, or each element of what is there when each? is true.

tests runs the tests with ? bound to the member it is given. What is null, or is not there, is exempt, so a result that has no such member is not rejected; the first member that the tests do not accept makes the whole result null, with the one error :value_not_allowed that says where it is (details.path, [2, "total"] for the total of the second element).

iex> env = Feel.Runtime.Env.new()
iex> tests = fn value, env -> {Decimal.compare(value, 150) != :gt, env} end
iex> person = %{"name" => "Ann", "age" => Decimal.new(20)}
iex> {^person, ^env} = Feel.Runtime.AllowedValues.verify_member(env, person, :result, {0, 9}, ["age"], false, tests)
iex> {%{"name" => "Ann"}, ^env} = Feel.Runtime.AllowedValues.verify_member(env, %{"name" => "Ann"}, :result, {0, 9}, ["age"], false, tests)
iex> {nil, env} = Feel.Runtime.AllowedValues.verify_member(env, %{"age" => Decimal.new(200)}, :result, {0, 9}, ["age"], false, tests)
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> {diagnostic.code, diagnostic.message, diagnostic.details}
{:value_not_allowed, ~s(the value of "result.age" is not one of its allowed values), %{name: :result, path: ["age"]}}
iex> people = [%{"age" => Decimal.new(20)}, %{"age" => Decimal.new(200)}]
iex> {nil, env} = Feel.Runtime.AllowedValues.verify_member(Feel.Runtime.Env.new(), people, :result, {0, 9}, [:each, "age"], false, tests)
iex> [diagnostic] = Feel.Runtime.Env.diagnostics(env)
iex> diagnostic.details.path
[2, "age"]