FSL.Context (fsl v0.2.0)

Copy Markdown View Source

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

FieldWritten byRead by
lasterrthe application's verbsevery transition macro
errorreasonscenario_failure/1cleanup/1, the host
currentstatethe runner, on entering a statethe machine, the host
laststatethe runner, only on a real state changegoto back
parent_pidspawn_fsm, run_instance/2the parent notifications
appdataappdata_set, the sub-FSM and SBB bookkeepingeverything

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

t()

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

t()

@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

__using__(opts)

(macro)

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_ctx by default.

    A name worth choosing: a machine holding a chat session reads better with chat_ctx than with fsl_ctx, and the SIP embedding uses sip_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} that ctx_set and ctx_get go 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.

appdata_get(ctx, key)

@spec appdata_get(t(), term()) :: term()

Read an application-defined value from the appdata map.

appdata_set(ctx, key, value)

@spec appdata_set(t(), term(), term()) :: t()

Store an application-defined value in the appdata map.

check_struct!(module)

@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.

fields()

@spec fields() :: keyword()

The six fields with their defaults, as a keyword list to splice into a binding's defstruct.

get(ctx, key)

@spec get(t(), atom()) :: term()

Read one of the six fields.

keys()

@spec keys() :: [atom()]

The six field names.

put(ctx, list)

@spec put(t(), keyword()) :: t()

Write several of the six fields from a keyword list, in order.

put(ctx, key, value)

@spec put(t(), atom(), term()) :: t()

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.