The leaf step that names one host invoke type, as a base a host block type declares itself out of (ADR-0007 decision 2).
A host block type that calls the host and waits for it to answer is three
facts - the invoke type it names, what its answer produces, and how the
palette draws it - wrapped in the same hundred lines every time. This
module is those hundred lines, written once: use StatifierBlocks.InvokeStep, invoke_type: "myapp:authorize" declares the
behaviour and fills in every callback, and the type overrides only the
rows where it differs.
defmodule MyApp.Blocks.Authorize do
use StatifierBlocks.InvokeStep,
invoke_type: "myapp:authorize",
produces: "myapp.authorization",
fields: [
%{
key: "assign_to",
type: :string,
label: "Write the decision to",
required?: true,
default: "authorization",
datamodel_path?: true
}
],
palette: %{
label: "Authorize card",
group: "Card processing",
description: "Authorizes the transaction against the card network.",
icon: "credit-card",
order: 0
}
endThis module names invoke types; it never runs one
The two-registry seam ADR-0002 decision 2 draws is the thing easiest to
lose here, so it is worth saying in as many words: a block type names an
invoke type, and a handler the host registers separately - per session,
under st-ADR-0051 - is what runs it. Nothing in this module resolves a
name to a handler, and use-ing it registers nothing. A step whose
invoke type no handler answers is a deployment fact
StatifierBlocks.Compiler.InvokeTypes reports at the one moment a caller
holds both registries; it is not something this base can know.
What it is not
It is not a block type. It implements no StatifierBlocks.BlockType
callback of its own, appears in no palette, and has no type_name: what
it holds is the decisions a leaf step makes the same way every time,
factored out so a host's twelve steps do not spell them twelve times.
StatifierBlocks.Core.Invoke remains the shipped core.invoke type and
is unaffected - see "The relationship to core.invoke" below.
Every function here is pure
ADR-0002 decision 4's purity rule reaches anything a callback calls and
not only the callback itself, so it reaches this module in full: no
process dictionary, no application configuration, no IO, no clock, no
randomness. The injected callbacks are ordinary function definitions
over their arguments and the compile-time declaration, which is what
keeps a use-ing type as pure as a hand-written one.
The relationship to core.invoke
StatifierBlocks.Core.Invoke is core.invoke: a step with an on_error
slot, so a failing call runs a subtree the author put there. This is
that emission with the slot taken out. A leaf step has no children, so
its failure path is an error outcome a parent may wire rather than a
subtree the block runs, and both outcome finals are emitted
unconditionally rather than only when a slot is occupied.
The two share the invoke_type grammar (through
StatifierBlocks.Core.Config), the two event names, and the rule that
assign_to is written on the success transition rather than in a
<finalize> - the answer is only an answer when the call succeeded, and
<finalize> runs for every event the invocation delivers.
What use injects
| Callback | Injected default |
|---|---|
current_version/0 | 1 |
slots/1 | [] - a leaf step has no children |
config_schema/1 | label, then invoke_type, then the :fields given |
validate_config/1 | the invoke_type and assign_to checks |
io/1 | %{kinds: [:step]}, plus produces when :produces is given |
outcomes/1 | [{"done", "Done"}, {"error", "Error"}] |
palette_entry/0 | the :palette given, over this module's defaults |
emit/2 | emit/4 against the declared invoke type |
migrate_config/2 | StatifierBlocks.BlockType's refusing default |
Every one is defoverridable. A type with extra <param> children calls
emit/4 itself; one with a tighter rule adds to validate_config/1; one
that has changed its config shape overrides current_version/0 and
migrate_config/2 the way any block type does.
Summary
Functions
Declares the behaviour and injects the leaf-step defaults (ADR-0007 decision 2). Options
Checks an assign_to the step declared as optional: a blank one is a
step that throws its answer away, which is an answer rather than a gap.
Checks that key holds a bare lowercase identifier, anchoring message
on that key when it does not.
Checks a stored invoke_type, and passes an absent one - see
invoke_type/2 for why absence is an answer rather than a gap.
label first, then invoke_type, then whatever else the step declares.
A compound state that calls the host and finishes at the <final> of
whichever outcome the call reached.
The invoke type this config names: what it stored, or the step's own declared default when it stored nothing.
The invoke_type field, declared with the step's own invoke type as its
default.
A step constrains nothing beyond being a step unless it declares what a successful call produces (ADR-0003).
The optional label field: the name this particular step goes by on the
card, above the block type's own label.
A <param> carrying a literal, for a value the block type stores rather
than reads out of the datamodel.
The two ways a call can finish, in the order they compile in.
A palette entry with this base's shared presentation defaults filled in.
Turns an accumulated finding list into validate_config/1's return,
restoring the order the checks ran in.
Functions
Declares the behaviour and injects the leaf-step defaults (ADR-0007 decision 2). Options:
:invoke_type- required, the invoke type this step names when its config does not say otherwise. A literal in thenamespace:namegrammar is checked at compile time.:produces- what a successful call produces to the next sibling (ADR-0003). Absent leaves the step unconstrained beyond being one.:fields- extra field declarations, appended afterlabelandinvoke_typein the order given.:palette- palette-entry keys, merged over this module's defaults.
@spec check_assign_to( [StatifierBlocks.BlockType.finding()], StatifierBlocks.Block.config() ) :: [ StatifierBlocks.BlockType.finding() ]
Checks an assign_to the step declared as optional: a blank one is a
step that throws its answer away, which is an answer rather than a gap.
A step that requires the key instead - because a decision nobody keeps is
not a decision - declares the field required?: true and adds
check_identifier/4, which refuses the blank.
@spec check_identifier( [StatifierBlocks.BlockType.finding()], StatifierBlocks.Block.config(), String.t(), String.t() ) :: [StatifierBlocks.BlockType.finding()]
Checks that key holds a bare lowercase identifier, anchoring message
on that key when it does not.
@spec check_invoke_type( [StatifierBlocks.BlockType.finding()], StatifierBlocks.Block.config() ) :: [ StatifierBlocks.BlockType.finding() ]
Checks a stored invoke_type, and passes an absent one - see
invoke_type/2 for why absence is an answer rather than a gap.
@spec config_schema(String.t(), [StatifierBlocks.BlockType.field_decl()]) :: [ StatifierBlocks.BlockType.field_decl() ]
label first, then invoke_type, then whatever else the step declares.
label leads because it is the field an author reaches for first - it is
what the card says - and the inspector renders declaration order.
@spec emit( StatifierBlocks.Block.t(), StatifierBlocks.Compiler.Context.t(), String.t(), [ StatifierBlocks.Emission.t() ] ) :: {:ok, StatifierBlocks.Emission.t()} | {:error, StatifierBlocks.BlockType.emit_error()}
A compound state that calls the host and finishes at the <final> of
whichever outcome the call reached.
<state id="s_blk_RCP" initial="s_blk_RCP__running">
<state id="s_blk_RCP__running">
<invoke type="myapp:receipt"/>
<transition event="done.invoke" target="s_blk_RCP__o_done"/>
<transition event="error.communication.invoke" target="s_blk_RCP__o_error"/>
</state>
<final id="s_blk_RCP__o_done">
<onentry><raise event="done.outcome.s_blk_RCP.done"/></onentry>
</final>
<final id="s_blk_RCP__o_error">
<onentry><raise event="done.outcome.s_blk_RCP.error"/></onentry>
</final>
</state>Both transitions match by SCXML's descriptor prefix rule and neither
names an invocation id, which is safe for the reason core.invoke
gives: they sit on the inner state, active only while this block's own
call is outstanding, so no other invocation's completion can be selected
by them.
The type attribute is stamped as coming from invoke_type only when
the author actually wrote one. A step running on the declared default
has no config key for a finding to point at, and attributing the
attribute to a key the document does not carry would send an author
looking for text they never typed.
params are the <param> children the calling type wants on the
<invoke>, in the order it wants them - literal_param/3 builds one. A
step with nothing to send omits the argument.
A config carrying assign_to puts an <assign expr="_event.data"> on
the success transition, writing the handler's answer to that location. A
step that stores nothing there emits no <assign>, and an assign_to
that is not a bare identifier is a finding on the author's key rather
than an attribute nobody can read.
@spec invoke_type(StatifierBlocks.Block.config(), String.t()) :: String.t()
The invoke type this config names: what it stored, or the step's own declared default when it stored nothing.
A step whose config omits the key is naming its own invoke type, which is what the declared default says. The alternative is a document that cannot say "the usual handler" without repeating it, and the field's default is the one place the usual handler is written down.
@spec invoke_type_field(String.t()) :: StatifierBlocks.BlockType.field_decl()
The invoke_type field, declared with the step's own invoke type as its
default.
It is required?: true with a default for core.on_event's reason: the
field is one an author must answer, and the answer is prefilled with the
only one that is usually right.
@spec io(StatifierBlocks.Assignability.produces() | nil) :: StatifierBlocks.Assignability.io()
A step constrains nothing beyond being a step unless it declares what a successful call produces (ADR-0003).
@spec label_field() :: StatifierBlocks.BlockType.field_decl()
The optional label field: the name this particular step goes by on the
card, above the block type's own label.
Declaring it is the whole of what a host does to opt in - this package
titles a card from a declared :string field keyed "label" and demotes
the palette label to the subtitle (StatifierBlocks.ViewModel.title/1
and subtitle/1).
@spec literal_param(String.t(), String.t(), String.t()) :: StatifierBlocks.Emission.t()
A <param> carrying a literal, for a value the block type stores rather
than reads out of the datamodel.
config_key is stamped on the emission as the provenance of the value,
so a finding inside it points at the author's field and not at this
module.
@spec outcomes() :: [StatifierBlocks.BlockType.outcome_decl()]
The two ways a call can finish, in the order they compile in.
Declaration order is read by ADR-0004 decision 6's byte determinism, so this list is never sorted.
@spec palette_entry(StatifierBlocks.BlockType.palette_entry()) :: StatifierBlocks.BlockType.palette_entry()
A palette entry with this base's shared presentation defaults filled in.
attrs wins over every default, so a step that says something is
believed.
@spec verdict([StatifierBlocks.BlockType.finding()]) :: :ok | {:error, [StatifierBlocks.BlockType.finding()]}
Turns an accumulated finding list into validate_config/1's return,
restoring the order the checks ran in.