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_variableValid 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_indexTwo 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.nameinto a context holding%{user: %{}}creates a new"user"map beside the atom key rather than descending into it. A caller reaching this throughPredicator.Context.assign/3never hits this case in practice:Context.new/2/bind/3already convert atom keys to strings deeply and eagerly, sodatahas no atom keys left by the timeassign/3calls in. It matters only for a caller invokingContextLocation.put/3directly on a hand-built, unnormalized map.