Feel.Value.Context (feel v0.1.0)

Copy Markdown View Source

The FEEL type context (DMN 1.6, section 10.3.2.6): a collection of key/value entries, like a record or a JSON object, in the order they were written (D-135).

The specification calls a context "partially ordered", and says no more of the order of the entries than that the entries of a context literal are evaluated in the order of what they depend on. The functions that show the order are get entries, context put, context merge and context(entries), and so does the text of a value (string(...)), where the specification's examples have the entries in the order they are written (context put({x:1}, "y", 2) is {x:1, y:2}). Equality ignores it ({a: 1, b: 2} = {b: 2, a: 1} is true, Table 54). So a context keeps its entries in order, and nothing else does anything with it.

The representation

A context the engine makes is a %Feel.Value.Context{}: the entries by key in a map, and the keys in the order of the entries. The keys are strings (see Feel.Name) and the values are any FEEL value, null included.

The engine also reads a plain map with string keys as a context, in the order of its keys, so that a host can bind %{"age" => Decimal.new(21)} as it is. A host that has an order to give builds the context with new/1.

Outside the engine a context is a plain map, as the specification and the TCK have it (the entries by name, no order): the value of a run is converted with to_host/1, unless the run is asked to keep the order (contexts: :ordered). Native functions get and give plain maps as well (Feel.Value.Function).

The order

  • A context literal has the order it is written in, and not the order its entries are evaluated in.

  • put/3 replaces the value of an entry where it is, and adds a new entry last.

  • merge/2 keeps the entries of the first context where they are and adds the new ones of the second after them; an entry that both have takes the value of the second, in the place of the first.

  • A plain map is in the order of its keys.

    iex> context = Feel.Value.Context.new([{"b", 1}, {"a", 2}]) iex> Feel.Value.Context.keys(context) ["b", "a"] iex> context |> Feel.Value.Context.put("c", 3) |> Feel.Value.Context.put("b", 0) |> Feel.Value.Context.to_list() [{"b", 0}, {"a", 2}, {"c", 3}] iex> Feel.Value.Context.to_host(context) %{"a" => 2, "b" => 1}

Summary

Types

A context in either of the forms the engine reads.

A plain map with string keys, which the engine reads as a context in the order of its keys.

t()

A context: the entries by key, and the keys in the order of the entries. Treat it as opaque and read it through the functions of this module.

Functions

The value of an entry: {:ok, value}, or :error for a key it does not have.

A value as the engine holds it: a plain map is a context in the order of its keys, in a list and in a context as well. A value that has none is given back as it is.

The context of a map, in the order of its keys. A context is given back as it is.

The value of an entry, or nil for a key it does not have.

Whether the context has an entry with the key.

Whether a term is a context in either form: a %Feel.Value.Context{} or a plain map. For a guard; it does not look at the keys.

The keys, in the order of the entries.

The entries of the second context added to the first: those the first has take the value of the second where they are, and the new ones come last, in the order of the second.

The context with no entry.

A context of entries, in their order. An entry with a key that an earlier one has takes its value, in the place of the earlier (as put/3 does).

The context made of a map, with the keys in the given order: the order a context literal is written in, for the entries were evaluated in another. keys has the keys of the map and no other.

The same, for the keys given last first, which is how the context keeps them. For the code that the BEAM backend generates, which has the keys of the literal in that order already.

The context with the entry set to the value: where it is if it has the key, and last if it has not.

The number of entries.

A value as a host has it: a context is a plain map, in a list and in a context as well. The specification's contexts and those of the TCK are that: entries by name, equal whatever their order.

The entries as {key, value}, in order.

The entries as a plain map, which has no order.

Returns the descriptor of a context type with the given member types.

Returns true for a context in either form: a %Feel.Value.Context{}, or a map that is not a struct and has only string keys.

The values, in the order of the entries.

Types

any_context()

@type any_context() :: t() | host()

A context in either of the forms the engine reads.

host()

@type host() :: %{optional(String.t()) => Feel.Value.t()}

A plain map with string keys, which the engine reads as a context in the order of its keys.

t()

@type t() :: %Feel.Value.Context{
  map: %{optional(String.t()) => Feel.Value.t()},
  order: [String.t()] | {:sorted, [String.t()]}
}

A context: the entries by key, and the keys in the order of the entries. Treat it as opaque and read it through the functions of this module.

order is the keys last first (so that an entry is added in constant time), or {:sorted, extras}: the keys that the map has but the extras in the order of the keys, and then the extras (last first). The second is what put/3 and merge/2 make of a plain map, which has the order of its keys but is not sorted until someone asks for it: a map of a hundred thousand entries is put into in constant time, and a loop of puts is not a loop of sorts.

Functions

fetch(map, key)

@spec fetch(any_context(), String.t()) :: {:ok, Feel.Value.t()} | :error

The value of an entry: {:ok, value}, or :error for a key it does not have.

iex> Feel.Value.Context.fetch(Feel.Value.Context.new([{"a", nil}]), "a")
{:ok, nil}
iex> Feel.Value.Context.fetch(%{"a" => 1}, "b")
:error

from_host(context)

@spec from_host(Feel.Value.t()) :: Feel.Value.t()

A value as the engine holds it: a plain map is a context in the order of its keys, in a list and in a context as well. A value that has none is given back as it is.

iex> Feel.Value.Context.from_host([%{"b" => 1}]) == [Feel.Value.Context.new([{"b", 1}])]
true

from_map(context)

@spec from_map(any_context()) :: t()

The context of a map, in the order of its keys. A context is given back as it is.

iex> Feel.Value.Context.from_map(%{"b" => 1, "a" => 2}) |> Feel.Value.Context.keys()
["a", "b"]

get(context, key)

@spec get(any_context(), String.t()) :: Feel.Value.t()

The value of an entry, or nil for a key it does not have.

has_key?(map, key)

@spec has_key?(any_context(), String.t()) :: boolean()

Whether the context has an entry with the key.

is_context(term)

(macro)

Whether a term is a context in either form: a %Feel.Value.Context{} or a plain map. For a guard; it does not look at the keys.

keys(map)

@spec keys(any_context()) :: [String.t()]

The keys, in the order of the entries.

merge(first, second)

@spec merge(any_context(), any_context()) :: t()

The entries of the second context added to the first: those the first has take the value of the second where they are, and the new ones come last, in the order of the second.

iex> a = Feel.Value.Context.new([{"x", 1}, {"y", 0}])
iex> b = Feel.Value.Context.new([{"z", 3}, {"y", 2}])
iex> a |> Feel.Value.Context.merge(b) |> Feel.Value.Context.to_list()
[{"x", 1}, {"y", 2}, {"z", 3}]

new()

@spec new() :: t()

The context with no entry.

new(entries)

@spec new([{String.t(), Feel.Value.t()}]) :: t()

A context of entries, in their order. An entry with a key that an earlier one has takes its value, in the place of the earlier (as put/3 does).

iex> Feel.Value.Context.new([{"b", 1}, {"a", 2}]) |> Feel.Value.Context.to_list()
[{"b", 1}, {"a", 2}]
iex> Feel.Value.Context.new([{"a", 1}, {"b", 2}, {"a", 3}]) |> Feel.Value.Context.to_list()
[{"a", 3}, {"b", 2}]

ordered(map, keys)

@spec ordered(%{optional(String.t()) => Feel.Value.t()}, [String.t()]) :: t()

The context made of a map, with the keys in the given order: the order a context literal is written in, for the entries were evaluated in another. keys has the keys of the map and no other.

iex> Feel.Value.Context.ordered(%{"a" => 1, "b" => 2}, ["b", "a"]) |> Feel.Value.Context.to_list()
[{"b", 2}, {"a", 1}]

ordered_reversed(map, reversed_keys)

@spec ordered_reversed(%{optional(String.t()) => Feel.Value.t()}, [String.t()]) :: t()

The same, for the keys given last first, which is how the context keeps them. For the code that the BEAM backend generates, which has the keys of the literal in that order already.

put(context, key, value)

@spec put(any_context(), String.t(), Feel.Value.t()) :: t()

The context with the entry set to the value: where it is if it has the key, and last if it has not.

iex> context = Feel.Value.Context.new([{"a", 1}, {"b", 2}])
iex> context |> Feel.Value.Context.put("a", 0) |> Feel.Value.Context.to_list()
[{"a", 0}, {"b", 2}]
iex> context |> Feel.Value.Context.put("c", 3) |> Feel.Value.Context.to_list()
[{"a", 1}, {"b", 2}, {"c", 3}]

size(map)

@spec size(any_context()) :: non_neg_integer()

The number of entries.

to_host(map)

@spec to_host(Feel.Value.t()) :: Feel.Value.t()

A value as a host has it: a context is a plain map, in a list and in a context as well. The specification's contexts and those of the TCK are that: entries by name, equal whatever their order.

iex> Feel.Value.Context.to_host([Feel.Value.Context.new([{"b", Feel.Value.Context.new([{"a", 1}])}])])
[%{"b" => %{"a" => 1}}]

to_list(context)

@spec to_list(any_context()) :: [{String.t(), Feel.Value.t()}]

The entries as {key, value}, in order.

iex> Feel.Value.Context.to_list(%{"b" => 1, "a" => 2})
[{"a", 2}, {"b", 1}]

to_map(map)

@spec to_map(any_context()) :: host()

The entries as a plain map, which has no order.

iex> Feel.Value.Context.new([{"b", 1}, {"a", 2}]) |> Feel.Value.Context.to_map()
%{"a" => 2, "b" => 1}

type(members)

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

Returns the descriptor of a context type with the given member types.

value?(term)

@spec value?(term()) :: boolean()

Returns true for a context in either form: a %Feel.Value.Context{}, or a map that is not a struct and has only string keys.

The values are not examined; use Feel.Type.instance?/2 for that.

iex> Feel.Value.Context.value?(%{"a" => 1})
true
iex> Feel.Value.Context.value?(Feel.Value.Context.new([{"a", 1}]))
true
iex> Feel.Value.Context.value?(%{a: 1})
false
iex> Feel.Value.Context.value?(~D[2026-10-12])
false

values(context)

@spec values(any_context()) :: [Feel.Value.t()]

The values, in the order of the entries.