MishkaCmsCore.Runtime.PageBuilder.Interface (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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.

Summary

Types

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

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

Functions

The attributes a record declares, always a list.

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

The slots a record declares, always a list.

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

What the template reads and the declaration never mentions.

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

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

Types

attr()

@type attr() :: %{required(String.t()) => term()}

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

slot()

@type slot() :: %{required(String.t()) => term()}

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

Functions

attrs(arg1)

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

The attributes a record declares, always a list.

required?(row)

@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(arg1)

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

The slots a record declares, always a list.

types()

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

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

undeclared(template, attrs, slots)

@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(attrs, slots)

@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(row, key)

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