StatifierBlocks.Recipe behaviour (StatifierBlocks v0.28.0)

Copy Markdown View Source

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

insert(target, document)

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

members(block_id, document)

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

palette_entry()

@callback palette_entry() :: StatifierBlocks.BlockType.palette_entry()

How the recipe draws in the palette browser. ADR-0005 decision 10's map, unchanged.

Functions

within_reach?(arg, commands)

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