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:
- Merge.
MergeDefaultsfills a missing key from the domain, and derives the stream name when it is missing. - Resolve.
ResolveColumnsandResolveFieldsdiscover columns and fields from the resource's attributes, infer field types from Ash types, place items bypositionand detect which relationships to preload. - Build.
BuildRuntimeConfigpersists one map for the table and one for the form. - 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-errorsturns 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:
| Layer | Does |
|---|---|
| State | Holds 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. |
| DataLoader | Runs 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. |
| Events | Receives 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. |
| Renderer | Passes the static and dynamic parts to the template. |
| Template and UI adapter | The 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:
| Slot | Answers | Examples |
|---|---|---|
| The entity body | What the data is and how it behaves. | source, sortable, required, mode, visible, restricted, load, apply, format |
The ui block | How it is presented. | label, placeholder, icon, class, width, type, span, rows, extra |
render | Replace 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.
| Entity | Signature of the predicate |
|---|---|
| Table column, filter, filter group | fn 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, notice | fn state -> boolean end |
| Form field, group, step, submit button | fn state -> boolean end |
Table realtime | fn record, user -> boolean end |
Form access gate for both modes | fn mode, state -> boolean end |
| Domain menu group | fn user -> boolean end. No built-in code calls it. |
The state is the live state struct. Read these fields:
| Table state | Form state |
|---|---|
current_user, master_user?, template, archive_status, filter_values, path_params, selected_ids, total_count, loading, records_result, loaded_records, static | current_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.
- A DSL option: a value, a label, a class, a predicate.
renderorformaton one entity.- A hook, for behavior that belongs to one resource. See React to table and form events with hooks.
- A custom type, when the same rendering repeats. See Write a custom type.
- A UI adapter, so every button, select and badge matches your design system.
- A template, when the arrangement differs.
- A builder, handler or loader, when the mechanism differs. See Override a builder, handler or loader.