State ids derived from block ids by a pure function (ADR-0004 decision 3).
state_id(block_id) = "s_" <> block_id
state_id(block_id, role) = "s_" <> block_id <> "__" <> roleA block id is already stable, document-unique, opaque and never reused (ADR-0001 decision 3), so a state id derived from it inherits all of that per block rather than per document: editing one block's config, or inserting a step at the top of a sequence, changes the ids of nothing else. That is what keeps a stored provenance map valid and a publish diff readable.
Roles
A role is a short, block-type-chosen local name for an auxiliary state
a block emits inside its own state. Roles are minted here rather than
by string concatenation inside a block type so this module owns the
namespacing, and it refuses any role it could not invert:
- it must match
~r/\A[a-z][a-z0-9_]*\z/, and - it must not contain
__, which is the separator itself.
The second rule is not implied by the first - a__b matches the pattern -
and without it unstate_id/1 would have two readings of the same string.
The three properties
- Uniqueness. Block ids are document-unique, a
blk_-prefixed UXID contains no__, and a role cannot either, so no two generated ids collide whatever a block type does. Thes_prefix additionally keeps generated state ids out of the namespace an author's own config writes into, which matters because statifier checks uniqueness over allID-typed attributes in one set. - Invertibility.
state_id/1andstate_id/2are injective andunstate_id/1inverts them without consulting the provenance map, so a human reading generated SCXML in a diff can still see which block a state came from. - Totality. Every generated state carries an id. Statifier permits
nameless states, but
Statifier.Position.export/1refuses an export containing one ({:error, {:unnameable_states, indexes}}) andStatifier.active_leaf_states/1drops it, so this package emits none.
Summary
Types
A block-type-chosen local name for an auxiliary state.
A generated SCXML state id: "s_" <> block_id, optionally "__" <> role.
Functions
The done.state event a compound state raises when it enters a <final>
child (ADR-0004 decision 2). This is the whole of what a parent needs to
know about a child it did not compile.
The completion event one declared outcome raises (ADR-0004's outcome
amendment, 2c): done.outcome.<state id>.<outcome>.
The reserved role prefix an outcome final's role carries (ADR-0004's outcome amendment, 2b).
Whether role is a role this module will mint an id for.
The state id of the state block_id's block compiles to.
The state id of an auxiliary state block_id's block mints under role.
Inverts done_event/1 and outcome_event/2 back to the block whose
completion the event names, and the outcome it names, or :error for a
string that is not unambiguously one of them.
Inverts an outcome final's id back into the block's own state id and
the outcome name, or :error for an id that is not one.
Inverts state_id/1 and state_id/2.
Types
Functions
The done.state event a compound state raises when it enters a <final>
child (ADR-0004 decision 2). This is the whole of what a parent needs to
know about a child it did not compile.
iex> StatifierBlocks.Compiler.StateId.done_event("s_blk_SEQ")
"done.state.s_blk_SEQ"
The completion event one declared outcome raises (ADR-0004's outcome
amendment, 2c): done.outcome.<state id>.<outcome>.
Spelled once, here, so the event a <final> raises and the event a
parent wires on cannot drift apart.
iex> StatifierBlocks.Compiler.StateId.outcome_event("s_blk_AUTH", "error")
"done.outcome.s_blk_AUTH.error"
@spec outcome_prefix() :: String.t()
The reserved role prefix an outcome final's role carries (ADR-0004's outcome amendment, 2b).
iex> StatifierBlocks.Compiler.StateId.outcome_prefix()
"o_"
Whether role is a role this module will mint an id for.
iex> StatifierBlocks.Compiler.StateId.role?("lane_capture")
true
iex> StatifierBlocks.Compiler.StateId.role?("Done")
false
@spec state_id(StatifierBlocks.Block.id()) :: t()
The state id of the state block_id's block compiles to.
iex> StatifierBlocks.Compiler.StateId.state_id("blk_ROOT")
"s_blk_ROOT"
@spec state_id(StatifierBlocks.Block.id(), role()) :: {:ok, t()} | {:error, {:invalid_role, StatifierBlocks.Block.id(), role()}}
The state id of an auxiliary state block_id's block mints under role.
Returns {:error, {:invalid_role, block_id, role}} rather than raising
for a role this module cannot invert - an ordinary Emit-stage finding,
since emit/2 is a total function like every other callback.
iex> StatifierBlocks.Compiler.StateId.state_id("blk_SEQ", "done")
{:ok, "s_blk_SEQ__done"}
iex> StatifierBlocks.Compiler.StateId.state_id("blk_SEQ", "a__b")
{:error, {:invalid_role, "blk_SEQ", "a__b"}}
@spec undone_event(term()) :: {:ok, {StatifierBlocks.Block.id(), role() | nil}} | :error
Inverts done_event/1 and outcome_event/2 back to the block whose
completion the event names, and the outcome it names, or :error for a
string that is not unambiguously one of them.
It lives here for the reason unoutcome_id/1's own @doc gives about
itself: the inversion belongs beside the derivation it inverts, not
inside a caller that would have to rediscover why it is exact. The
caller this exists for is ADR-0005 decision 10w - a summary chip whose
text has the shape of a generated done-event name is drawn as the
block's label rather than as the compiler's spelling of it.
nil comes back for the done.state form because that event carries no
outcome name: ADR-0004 decision 2 makes it the block's completion signal
and nothing more. Naming one here would invent a concept ADR-0004 does
not have, so what to draw in its place is the caller's word, not this
function's.
Why this is total rather than best-effort
StatifierBlocks.Validation admits any non-empty UTF-8 string as a block
id, so the opacity this module's moduledoc argues from is a property of
every id this package mints and not of every id it admits. A document
arriving through from_json/1 may carry a block id containing "__", or
a ".", and either gives a generated event name a second reading:
done.outcome.s_a__b.error reads as block a__b and as block a under
role b, and done.outcome.s_A.B.C reads as outcome B.C and as
outcome C.
Every such string answers :error, which is the fail-safe ADR-0005
decision 10y requires: a chip drawn as its raw event name is a
presentation defect, and a chip drawn as the wrong block's label is a
card that says a different block completed.
iex> StatifierBlocks.Compiler.StateId.undone_event("done.outcome.s_blk_AUTH.error")
{:ok, {"blk_AUTH", "error"}}
iex> StatifierBlocks.Compiler.StateId.undone_event("done.state.s_blk_SEQ")
{:ok, {"blk_SEQ", nil}}
iex> StatifierBlocks.Compiler.StateId.undone_event("done.outcome.s_a__b.error")
:error
iex> StatifierBlocks.Compiler.StateId.undone_event("done.outcome.s_A.B.C")
:error
iex> StatifierBlocks.Compiler.StateId.undone_event("order.paid")
:error
Inverts an outcome final's id back into the block's own state id and
the outcome name, or :error for an id that is not one.
This is unstate_id/1 with the outcome namespace read off the role, and
it lives here for that reason: the inversion belongs beside the
derivation it inverts, not inside a caller that would have to rediscover
why it is exact. It is exact because a generated id contains exactly one
"__" - a block id carries none (ADR-0001 decision 3) and a role is
refused if it does - so the role is unambiguous, and a role in the o_
namespace is minted only by
StatifierBlocks.Compiler.Context.outcome_id/2.
The state id that comes back is state_id(block_id), which is what the
outcome's completion event names.
iex> StatifierBlocks.Compiler.StateId.unoutcome_id("s_blk_AUTH__o_error")
{:ok, {"s_blk_AUTH", "error"}}
iex> StatifierBlocks.Compiler.StateId.unoutcome_id("s_blk_AUTH__done")
:error
iex> StatifierBlocks.Compiler.StateId.unoutcome_id("s_blk_AUTH")
:error
@spec unstate_id(t()) :: {:ok, {StatifierBlocks.Block.id(), role() | nil}} | :error
Inverts state_id/1 and state_id/2.
iex> StatifierBlocks.Compiler.StateId.unstate_id("s_blk_SEQ")
{:ok, {"blk_SEQ", nil}}
iex> StatifierBlocks.Compiler.StateId.unstate_id("s_blk_SEQ__done")
{:ok, {"blk_SEQ", "done"}}
iex> StatifierBlocks.Compiler.StateId.unstate_id("blk_SEQ")
:error