Location Expressions

View Source

Predicator provides specialized support for SCXML datamodel location expressions, which determine valid assignment targets (l-values) for <assign> operations.

Resolving locations

iex> Predicator.context_location("user.profile.name", %{})
{:ok, ["user", "profile", "name"]}

iex> Predicator.context_location("items[0]", %{})
{:ok, ["items", 0]}

iex> Predicator.context_location("data['users'][index]['profile']", %{"index" => 2})
{:ok, ["data", "users", 2, "profile"]}

Invalid targets and undefined bracket variables come back as Predicator.Errors.LocationError. Bind-and-project the type field rather than inlining a full struct literal, since the struct carries additional details that can grow without changing what these examples assert:

iex> {:error, err} = Predicator.context_location("len(name)", %{})
iex> err.type
:not_assignable

iex> {:error, err} = Predicator.context_location("42", %{})
iex> err.type
:not_assignable

iex> {:error, err} = Predicator.context_location("items[missing_var]", %{})
iex> err.type
:undefined_variable

Valid assignment targets

  • Simple identifiers: user, score, config
  • Property access: user.name, config.database.host
  • Bracket access: items[0], user['profile'], data["key"]
  • Mixed notation: user.settings['theme'], data['users'][0].profile

Invalid assignment targets

  • Literals: 42, "hello", true, #2024-01-15#
  • Function calls: len(name), upper(role), Math.max(a, b)
  • Arithmetic expressions: score + 1, items[i + 1]
  • Comparison results: score > 85, name == "John"
  • Any computed expression that cannot serve as a memory location

Location path format

Location paths are returned as lists representing the navigation path to a specific location:

["user"]                                  # user
["user", "name"]                          # user.name
["items", 0]                              # items[0]
["user", "profile", "settings", "theme"]  # user.profile.settings['theme']
["data", "users", 2, "name"]              # data['users'][2]['name']

This enables safe assignment operations in SCXML processors while preventing assignment to computed values or literals.

Assignment

Resolving a location tells you where to write. Predicator.context_assign/4 performs the write, and Predicator.ContextLocation.put/3 does the same given an already-resolved path. Note that context_assign/4 takes the context first, unlike context_location/3: it transforms a context and returns a new one, so it composes in a pipeline.

iex> Predicator.context_assign(%{}, "user.profile.name", "Ada")
{:ok, %{"user" => %{"profile" => %{"name" => "Ada"}}}}

Missing intermediate containers are created automatically - a string segment vivifies a map, an integer segment vivifies a list:

iex> Predicator.context_assign(%{}, "data['users'][0].name", "Ada")
{:ok, %{"data" => %{"users" => [%{"name" => "Ada"}]}}}

Existing siblings are preserved:

iex> Predicator.context_assign(%{"user" => %{"id" => 1}}, "user.name", "Ada")
{:ok, %{"user" => %{"id" => 1, "name" => "Ada"}}}

Indices past the end of a list pad the gap with :undefined:

iex> Predicator.context_assign(%{"items" => [1]}, "items[2]", "x")
{:ok, %{"items" => [1, :undefined, "x"]}}

ContextLocation.put/3 does the same given an already-resolved path:

iex> Predicator.ContextLocation.put(%{}, ["user", "profile", "name"], "Ada")
{:ok, %{"user" => %{"profile" => %{"name" => "Ada"}}}}

Auto-vivification never destroys existing data. Assigning through a value that is neither a map nor a list is an error, as is a negative list index:

iex> {:error, err} = Predicator.context_assign(%{"user" => 5}, "user.profile.name", "Ada")
iex> err.type
:not_a_container

iex> {:error, err} = Predicator.context_assign(%{"items" => [1, 2]}, "items[-1]", "x")
iex> err.type
:invalid_index

Two rules worth knowing:

  • The leaf is always overwritten, whatever it currently holds - including a map or a list.
  • Only string and integer keys are consulted, never atom keys. Assigning user.name into a context holding %{user: %{}} creates a new "user" map beside the atom key rather than descending into it.