A palette entry that puts down an arrangement rather than a block (ADR-0005 clauses 1C and 2C).
A block type answers "what is this block". A recipe answers "how do these
blocks go together": it is handed the position the author armed and the
document, and it returns the commands that build the arrangement. The
caller wraps them in one {:compound, commands} and commits, so the
arrangement is one gesture in and one gesture out.
A recipe is not a block type. It has no type_name, it appears in no
document, and nothing resolves a block against it - StatifierBlocks.Palette
keeps recipes in a second map for exactly that reason, and the two maps are
two namespaces rather than one. A recipe named "deadline" and a block type
named "deadline" do not collide.
The callbacks
palette_entry/0 is StatifierBlocks.BlockType's callback, in every
particular: the same optional keys, the same total normalizers, the same
fallback to the entry's name when a key is absent. A recipe draws in the
palette browser the way a type draws, which is deliberate - an author
picking a deadline is not doing a different kind of thing from an author
picking a send.
insert/2 is where a recipe differs. It is pure: it reads the document
rather than writing it, mints no ids beyond the ones ADR-0005 decision 2
already requires an :insert to carry, and may refuse. A refusal is the
ordinary case where the arrangement does not fit the position the author
armed, and it is an error term, never an exception.
members/2 is optional, and is the only callback here that is. It is
the delete-time counterpart of insert/2: handed a block id and the
document, it answers the ids of the arrangement that block belongs to, or
[] when the block is none of the recipe's business. A recipe that
implements only insert/2 and palette_entry/0 is a valid recipe exactly
as it was before the callback existed, and says "not mine" by omission.
A recipe recognises its arrangement structurally, in the document it is
shown, rather than by a mark: nothing a document holds says "this send is a
deadline's send" (ADR-0005 clause 11u), and members/2 does not change
that. The consequence is deliberate rather than incidental - a hand-built
pair is claimed, because the recipe cannot tell it from a picked one and
should not.
What a recipe may reach
Clause 3C bounds the commands insert/2 returns to the armed position
itself and any slot of the block that encloses it - the block the
armed target names as its parent - and nothing above that. A recipe that
could write two levels up would move blocks into a region the author is
not looking at.
The bound is the caller's to enforce: insert/2 is a pure function
answering with commands, and it is StatifierBlocks.Recipe.within_reach?/2
that says whether a returned list stays inside it.
The same bound holds a members/2 answer, for the same reason and with the
same division of labour: an author deleting a block must not have a block
removed from a region they are not looking at, so a claim naming a block
that does not sit in the same enclosing block as the asked-about one is
refused by the caller before a compound is built.
Summary
Callbacks
The commands that build the arrangement at target, or a refusal.
The ids of the arrangement block_id belongs to, or [].
How the recipe draws in the palette browser. ADR-0005 decision 10's map, unchanged.
Functions
Whether every command in commands stays inside clause 3C's bound for an
insertion armed at target.
Callbacks
@callback insert( target :: StatifierBlocks.Edit.target(), document :: StatifierBlocks.Document.t() ) :: {:ok, [StatifierBlocks.Edit.t()]} | {:error, term()}
The commands that build the arrangement at target, or a refusal.
Called with the armed position and the document as it stands. Pure.
@callback members( block_id :: StatifierBlocks.Block.id(), document :: StatifierBlocks.Document.t() ) :: [ StatifierBlocks.Block.id() ]
The ids of the arrangement block_id belongs to, or [].
Called with a block id and the document as it stands. Pure, on insert/2's
terms: it reads and does not write, mints nothing, and raises nothing.
The answer includes the block it was asked about when the recipe claims
it. [] is how a recipe says "not mine", and an answer of one id is the
same gesture as declining - a compound of one remove and a plain remove are
the same thing.
Optional. A recipe that never wants a compound delete writes nothing.
@callback palette_entry() :: StatifierBlocks.BlockType.palette_entry()
How the recipe draws in the palette browser. ADR-0005 decision 10's map, unchanged.
Functions
@spec within_reach?(StatifierBlocks.Edit.target(), [StatifierBlocks.Edit.t()]) :: boolean()
Whether every command in commands stays inside clause 3C's bound for an
insertion armed at target.
The bound is two positions wide: the armed position itself, and any slot
of the block that encloses it. Both are named by the same block id - the
parent in target - so the check is that every command naming a position
names that block, and that every command naming a block names one this
compound itself inserted.
iex> StatifierBlocks.Recipe.within_reach?({"blk_g", "body", 0}, [])
true
iex> block = StatifierBlocks.Block.new("core.send", id: "blk_s")
iex> StatifierBlocks.Recipe.within_reach?(
...> {"blk_g", "body", 0},
...> [{:insert, {"blk_g", "interrupts", 0}, block}]
...> )
true
iex> block = StatifierBlocks.Block.new("core.send", id: "blk_s")
iex> StatifierBlocks.Recipe.within_reach?(
...> {"blk_g", "body", 0},
...> [{:insert, {"blk_elsewhere", "body", 0}, block}]
...> )
false