# `StatifierBlocks.Recipe`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/recipe.ex#L1)

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.

# `insert`

```elixir
@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`
*optional* 

```elixir
@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`

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

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

# `within_reach?`

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

A command that names neither - `{:set_datamodel, entries}` or
`{:set_accepts, names}`, a declaration on the document rather than a
position in it - has no reach to exceed, and is within the bound wherever
it sits in the list.

    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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
