How Gervaz works

v0.0.1

Mishka Gervaz turns a declaration on an Ash resource into a working admin surface. You write a mishka_gervaz block with a table section, a form section or both. At compile time Gervaz checks the block and folds it, together with the defaults of the domain, into one config map. At run time two Phoenix LiveComponents read that map, run the Ash queries and actions, and draw the result. This page explains the compile-time pipeline, the run-time layers and the rules that every option follows.

The idea

An admin screen around an Ash resource repeats the same work: a list with filters, sorting and pagination, a create and edit form with validation, multi-step forms, file uploads, and access rules for a platform operator and for a tenant. Each screen calls the same building blocks in slightly different ways. Gervaz moves that work into a declaration and keeps every piece replaceable, so nothing is hidden.

What happens at compile time

Two Spark extensions carry the DSL. MishkaGervaz.Domain goes on the Ash domain and holds defaults. MishkaGervaz.Resource goes on a resource and holds the table and form sections. When the resource compiles, these steps run in order:

  1. Merge. MergeDefaults fills a missing key from the domain, and derives the stream name when it is missing.
  2. Resolve. ResolveColumns and ResolveFields discover columns and fields from the resource's attributes, infer field types from Ash types, place items by position and detect which relationships to preload.
  3. Build. BuildRuntimeConfig persists one map for the table and one for the form.
  4. Verify. Verifiers check the result: identity, source actions, columns, filters, row and bulk actions, pagination and layout for tables, and identity, source, fields, groups, steps, uploads, preloads and chrome for forms. A failure is a Spark.Error.DslError, which the compiler prints as a warning. mix compile --warnings-as-errors turns it into a failed build.

The runtime does not walk the DSL again. It reads the persisted map through MishkaGervaz.Resource.Info.Table and MishkaGervaz.Resource.Info.Form. The state overrides and the url_sync and refresh sections come through Spark option accessors, and the preloads that Gervaz detects are a separate persisted key. See Introspection.

What happens at run time

MishkaGervaz.Table.Web.Live and MishkaGervaz.Form.Web.Live are thin orchestrators. Each delegates to five layers, and every layer is replaceable:

LayerDoes
StateHolds one struct per component. A static part carries the configuration and keeps the same reference on every update, so LiveView skips it. The rest is the dynamic state: filter values, page, selection, the form and its errors, and the current step.
DataLoaderRuns the Ash queries and builds the AshPhoenix.Form. It also loads the options of relation filters and fields, and runs the hooks that belong to loading.
EventsReceives every phx- event, split by concern: sanitization, records, selection, bulk actions and relation filters for a table, and validation, submit, steps, uploads and relation fields for a form.
RendererPasses the static and dynamic parts to the template.
Template and UI adapterThe template decides where things go. The adapter decides how each piece looks.

The template and the adapter are separate on purpose. Swapping the template to cards does not require a new adapter, and swapping the adapter does not require a new template. The one pairing is that MishkaGervaz.Table.Templates.MediaGallery draws with MishkaGervaz.UIAdapters.MediaGallery whenever the adapter in effect is the Tailwind one. See Change how the admin looks and Write a table or form template.

The three slots of a column or a field

Every column and field is described in three places, and putting a value in the wrong one is the most common mistake in the DSL:

SlotAnswersExamples
The entity bodyWhat the data is and how it behaves.source, sortable, required, mode, visible, restricted, load, apply, format
The ui blockHow it is presented.label, placeholder, icon, class, width, type, span, rows, extra
renderReplace the output entirely, with HEEx.A bespoke cell

A column's label is the one option that exists in both the body, as a shorthand, and the ui block. The built-in table header reads the body option. The ui.extra map is an escape hatch for options that a template or type invented. Gervaz never validates it.

Predicates and their arity

Many options accept a boolean or a function: visible, restricted, readonly, disabled. The number of arguments depends on the entity, and a function of the wrong arity fails when Gervaz calls it.

EntitySignature of the predicate
Table column, filter, filter groupfn state -> boolean end
Row action:active, :archived, true, false or fn record, state -> boolean end
Bulk action:active, :archived or fn state -> boolean end
Table or form header, footer, noticefn state -> boolean end
Form field, group, step, submit buttonfn state -> boolean end
Table realtimefn record, user -> boolean end
Form access gate for both modesfn mode, state -> boolean end
Domain menu groupfn user -> boolean end. No built-in code calls it.

The state is the live state struct. Read these fields:

Table stateForm state
current_user, master_user?, template, archive_status, filter_values, path_params, selected_ids, total_count, loading, records_result, loaded_records, staticcurrent_user, master_user?, mode (:create or :update), current_step, field_values, form, errors, form_errors, dirty?, relation_options, existing_files, defaults, static

state.template is the template module. Compare state.template.name() with an atom, or compare the module itself.

Layers of customization

Reach for the lowest layer that solves the problem. Each step down costs more code to maintain.

  1. A DSL option: a value, a label, a class, a predicate.
  2. render or format on one entity.
  3. A hook, for behavior that belongs to one resource. See React to table and form events with hooks.
  4. A custom type, when the same rendering repeats. See Write a custom type.
  5. A UI adapter, so every button, select and badge matches your design system.
  6. A template, when the arrangement differs.
  7. A builder, handler or loader, when the mechanism differs. See Override a builder, handler or loader.