The state a machine keeps about itself, and the macros a state body reads and writes it with.
Every machine has a context. %FSL.Context{} is the one a machine gets when
the application supplies none, and it holds six fields the engine needs.
Extending it
An application does not hand FSL a context of its own: it extends FSL's.
Build the struct from fields/0 and add whatever a session of the application
holds:
defmodule MyApp.Context do
@after_compile FSL.Context
defstruct FSL.Context.fields() ++ [endpoint: nil, connection: nil, user: nil]
end@after_compile FSL.Context turns a defstruct that forgot fields/0 into a
compile error rather than a crash on the first transition.
The six fields are the machine's own bookkeeping; the application's are the guests. That is the direction, and it is worth stating because the file layout suggests the opposite: a machine keeps state about itself whether or not there is an application around it, and nothing the application adds is anything FSL reads.
The six fields
| Field | Written by | Read by |
|---|---|---|
lasterr | the application's verbs | every transition macro |
errorreason | scenario_failure/1 | cleanup/1, the host |
currentstate | the runner, on entering a state | the machine, the host |
laststate | the runner, only on a real state change | goto back |
parent_pid | spawn_fsm, run_instance/2 | the parent notifications |
appdata | appdata_set, the sub-FSM and SBB bookkeeping | everything |
lasterr is the one field an application writes and FSL reads — the channel that
lets a verb report an error and a scenario stay readable without an if after
every call. The other five are FSL's alone.
Why the names are unspaced
lasterr, errorreason, currentstate and laststate are where Elixir style
would write last_error, error_reason, current_state, last_state.
Renaming them is refused, and not out of nostalgia: deployed scripts read
sip_ctx.lasterr off the struct, a struct field takes no deprecated alias, and
the failure would be a node that does not start rather than a warning. FSL
adopts the names it inherits. Changing them is a major version with a
migration, never a side effect of moving files.
Writing them
FSL writes these fields with put/3, and deliberately not through an
application's own setter, which would validate properties FSL has no business
knowing about. The guards in put/3 are the machine's own invariants and
nothing else: a state name is an atom, a failure reason is a string, a parent
is a pid or nil, and lasterr takes any term because it carries whatever a
verb failed with.
Summary
Types
Any struct carrying the six fields — %FSL.Context{} itself, or a binding's
context extending it. FSL never matches on %FSL.Context{}: it reads the six
fields by name, so a binding's own struct travels everywhere its own.
Functions
Inject the five generic context macros into a scenario: ctx_set, ctx_get,
ctx_set_multiple, appdata_set and appdata_get.
Read an application-defined value from the appdata map.
Store an application-defined value in the appdata map.
Assert at compile time that module's struct carries the six fields with the
right defaults. An application writes @after_compile FSL.Context in its context
module, so a defstruct that forgot FSL.Context.fields() is a compile error
rather than a crash on the first transition.
The six fields with their defaults, as a keyword list to splice into a
binding's defstruct.
Read one of the six fields.
The six field names.
Write several of the six fields from a keyword list, in order.
Write one of the six fields.
Types
@type t() :: struct()
Any struct carrying the six fields — %FSL.Context{} itself, or a binding's
context extending it. FSL never matches on %FSL.Context{}: it reads the six
fields by name, so a binding's own struct travels everywhere its own.
Functions
Inject the five generic context macros into a scenario: ctx_set, ctx_get,
ctx_set_multiple, appdata_set and appdata_get.
Options
:ctx_var— what a machine of this application calls the context variable.:fsl_ctxby default.A name worth choosing: a machine holding a chat session reads better with
chat_ctxthan withfsl_ctx, and the SIP embedding usessip_ctx. What FSL does not assume is that there is only one such name, which is what lets two applications' machines run in one VM.:setter/:getter—{module, function}thatctx_setandctx_getgo through.{FSL.Context, :put}and{FSL.Context, :get}by default.Name your own when your context has fields of its own to validate:
ctx_set(:endpoint, …)should go through whatever checks an endpoint, and FSL's own pair is restricted to the six fields on purpose, so that a machine with no application around it cannot write a field nobody defined.
Injected once per module: a machine reaching this through two use lines
would otherwise redefine the macros and warn on every clause.
The macros are one-liners over the *_ast/3 builders below rather than
quoted-inside-quoted code. Three levels of unquote is not a style
preference — it is the level at which nobody can read which stage a variable
belongs to.
Read an application-defined value from the appdata map.
Store an application-defined value in the appdata map.
@spec check_struct!(module()) :: :ok
Assert at compile time that module's struct carries the six fields with the
right defaults. An application writes @after_compile FSL.Context in its context
module, so a defstruct that forgot FSL.Context.fields() is a compile error
rather than a crash on the first transition.
@spec fields() :: keyword()
The six fields with their defaults, as a keyword list to splice into a
binding's defstruct.
Read one of the six fields.
@spec keys() :: [atom()]
The six field names.
Write several of the six fields from a keyword list, in order.
Write one of the six fields.
Raises for any other key: a context property that is not the FSM's is the binding's business, and writing it here would bypass whatever the binding validates about it.