# `MishkaCmsCore.Runtime.PageBuilder.Interface`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/interface.ex#L5)

A component's INTERFACE — the attributes and slots it declares — as data the editor can edit and
check.

## Why this exists

A runtime component is markup plus a declaration, and until now the builder could only edit the
markup. The declaration lives in the `attrs` and `slots` columns of
`MishkaCmsCore.Runtime.Resources.Component`, and the only way to touch it was the Components table
form, where a slot's options are a raw JSON textarea. Meanwhile that same declaration drives almost
everything the page builder does with the component: which controls the Inspector offers, what
`.properties` and `.slots` list, which properties are marked required, whether a drop into a slot
is allowed at all.

So an author could build a component visually and had no way to say what it takes — and a template
that reads `@size` without `attr :size` does not fail at save time. It compiles, and then raises
`KeyError` the first time the component renders on a page, which is the same shape as the
`key :src not found` an empty required slot attribute used to produce.

## What it checks

`validate/2` refuses the declaration itself: a name that is not a valid Elixir identifier, a
duplicate, an unknown type, a struct attribute with no module, and the one combination Phoenix
rejects outright — `required: true` together with a default.

`undeclared/3` is the other direction, and the one that catches the KeyError before it happens: it
reads the template for assigns and slots the declaration does not mention. `site` and
`component_name` are never reported, because
`MishkaCmsCore.Runtime.Compilers.ComponentCompiler` declares both on every runtime component.

Reading a template with a regex has the limits `MishkaCmsCore.Runtime.PageBuilder.Inspector`'s
`renders_slot?/2` documents, and the same rule applies here: this is ADVICE, shown beside a button
that writes the declaration for you. It never refuses a save, so a false positive costs a line of
text and never a blocked author.

# `attr`

```elixir
@type attr() :: %{required(String.t()) =&gt; term()}
```

One declared attribute, as it is stored on the resource.

# `slot`

```elixir
@type slot() :: %{required(String.t()) =&gt; term()}
```

One declared slot, as it is stored on the resource.

# `attrs`

```elixir
@spec attrs(map() | nil) :: [attr()]
```

The attributes a record declares, always a list.

# `required?`

```elixir
@spec required?(attr() | slot()) :: boolean()
```

Whether this row says it is required, whichever of the two shapes it is in.

A row on its way out of the form carries `required` at the top level; a row read back off the
record carries it under `opts`, which is where the compiler looks. Public because reading a
declaration is not only this module's business — the layers rail draws the same rows.

# `slots`

```elixir
@spec slots(map() | nil) :: [slot()]
```

The slots a record declares, always a list.

# `types`

```elixir
@spec types() :: [String.t()]
```

Every attribute type this editor will write, in the order it offers them.

# `undeclared`

```elixir
@spec undeclared(String.t() | nil, [attr()], [slot()]) :: %{
  attrs: [String.t()],
  slots: [String.t()]
}
```

What the template reads and the declaration never mentions.

`%{attrs: [names], slots: [names]}`, in the order they first appear, so the list reads the way the
markup does. A name that is declared as a slot is never reported as a missing attribute, and the
two the compiler always provides are never reported at all.

# `validate`

```elixir
@spec validate([attr()], [slot()]) :: [{:attr | :slot, non_neg_integer(), String.t()}]
```

What is wrong with this declaration, as `{kind, index, sentence}` for the row it belongs to.

Empty means it can be written. The sentences are meant to be read by whoever is filling the form,
so they say what to do rather than what rule was broken.

# `value`

```elixir
@spec value(attr() | slot(), String.t()) :: String.t()
```

One field of a row as trimmed text, atom keys and string keys alike, `""` when it is not there.

---

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