StatifierBlocks.InvokeStep (StatifierBlocks v0.26.0)

Copy Markdown View Source

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: {:path, %{}},
        label: "Write the decision to",
        required?: true,
        default: "cards.authorization"
      }
    ],
    palette: %{
      label: "Authorize card",
      group: "Card processing",
      description: "Authorizes the transaction against the card network.",
      icon: "credit-card",
      order: 0
    }
end

This 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

CallbackInjected default
current_version/01
slots/1[] - a leaf step has no children
config_schema/1label, then invoke_type, then the :fields given
validate_config/1the invoke_type and assign_to checks
io/1%{kinds: [:step]}, plus produces when :produces is given
outcomes/1[{"done", "Done"}, {"error", "Error"}]
palette_entry/0the :palette given, over this module's defaults
emit/2emit/4 against the declared invoke type
migrate_config/2StatifierBlocks.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.

error, the outcome a step reaches when the call did not succeed (ADR-0002's amendment of 2026-09-06, section 3).

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

__using__(opts)

(macro)

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 the namespace:name grammar 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 after label and invoke_type in the order given.
  • :palette - palette-entry keys, merged over this module's defaults.
  • :failure_outcomes - the outcome names this step's family counts as having failed, declared once here rather than as an @impl on every member. Absent, the injection is failure_outcomes/0's ["error"], exactly as it was before the option existed. A host wrapper macro over this one may set it for its whole family (ADR-0002's Note of 2026-09-07, item 6).

check_assign_to(findings, config)

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.

What it accepts is core.assign's and core.invoke's datamodel path - any non-empty value with no whitespace, dotted or not (ADR-0011 decision 13, widened here on sb-r313). A step that requires the key instead - because a decision nobody keeps is not a decision - declares the field required?: true and adds a refusal of the blank of its own; this check stays blank-permissive either way, so the two compose rather than disagreeing about the same value. check_identifier/4 is the shipped shape for a step field whose rule really is a bare identifier, which assign_to no longer is.

check_identifier(findings, config, key, message)

Checks that key holds a bare lowercase identifier, anchoring message on that key when it does not.

check_invoke_type(findings, config)

Checks a stored invoke_type, and passes an absent one - see invoke_type/2 for why absence is an answer rather than a gap.

config_schema(default, extra \\ [])

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.

emit(block, context, default, params \\ [])

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.

failure_outcomes()

@spec failure_outcomes() :: [String.t()]

error, the outcome a step reaches when the call did not succeed (ADR-0002's amendment of 2026-09-06, section 3).

The use macro defines failure_outcomes/1 from this for every host type built on it that does not declare failure_outcomes: itself, so a step that calls out to the world and comes back on error is classed as having failed without its author writing anything. It is in defoverridable: a host whose error is routine - a probe that reports "not found" through it, say - declares failure_outcomes: [] at the use site or defines its own failure_outcomes/1, in the same place it would override outcomes/1.

This narrows one sentence of StatifierBlocks.BlockType.failure_outcomes/1's own Note: a type built on this module now exports the callback by inheritance, so it is classed even though it was written before the callback existed. The amendment counts that as a cost and takes it.

invoke_type(config, default)

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

invoke_type_field(default)

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

io(produces)

A step constrains nothing beyond being a step unless it declares what a successful call produces (ADR-0003).

label_field()

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

literal_param(name, value, config_key)

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

outcomes()

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

palette_entry(attrs)

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.

verdict(findings)

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