Skip to content

The rule cascade

Screen(rules=...) is declui's app sheet — the MetaUI idea of setting a field's presentation by selector. A rule matches a field's context and applies Field options to it, so one line can shape many fields at once, or vary one model by screen. Like everything in declui, it resolves once, at mount time — there is no runtime rule engine.

from situ.declui import Rule, Screen

ISSUE = Screen(
    model=Issue,
    rules=(
        Rule({"type": "Priority"}, {"widget": "Popup"}),                     # every Priority field
        Rule({"field": "subject", "operation": "list"}, {"link": "detail_url"}),  # subject links, list only
        Rule({"field": "notes", "operation": "create"}, {"hidden": True}),   # hide notes on the create form
        Rule({}, {"editable": "not locked"}),                                # a screen-wide predicate
    ),
)

A rule doesn't add a second vocabulary — it sets the same Field options you'd write inline, but by selector and per operation. Rule({"type": "Status"}, {"widget": "Popup"}) is "set Field(widget="Popup") on every Status-typed field."

Ranks

Each field resolves from three ranked sources, folded into one effective Field:

Rank Source Example
0 introspection — the field's type → widget default str → text input
1 field metadata — the field's own Field(...) Field(label="Subject")
2 the app sheet — matching Screen.rules Rule({"type": "Status"}, {"widget": "Popup"})

Higher rank wins, so a rule (rank 2) overrides the field's own Field (rank 1). Within rank 2, a more specific rule (more selector keys) wins, and ties break by declaration order (later wins).

The context and the selectors

A rule matches against a field's context: {model, field, type, operation}, where operation is the screen kind being generated — form / create / edit / list / detail. A selector matches when all its entries match, in one of three arities:

Rule({"field": "title"}, {...})                       # equality — the field named "title"
Rule({"type": "Status"}, {...})                       # equality — every Status-typed field
Rule({"operation": ["list", "detail"]}, {...})        # set-membership — either operation
Rule({"field": ...}, {...})                           # presence — any field (Ellipsis)
Rule({}, {...})                                        # empty — every field (a screen-wide rule)

The operation axis is what one model can't express through Field() alone: a Field(hidden=True) hides a field on every screen, but Rule({"field": "notes", "operation": "create"}, {"hidden": True}) hides it on the create form only.

Override properties vs predicates

Properties merge by a fixed policy:

  • Overridewidget / label / hidden / link / format / source / search / badge / required / secret / label_field: the highest-ranked, most-specific rule replaces the value.
  • Predicatevisible / editable / valid: every matching rule's predicate AND-chains with the field's own, so a field is visible only when all of them hold. Screen.object_editable is the same idea (a screen-wide editable AND-ed into every field).
# body is editable only when its own rule AND the screen-wide rule both hold
Rule({}, {"editable": "not locked"}),                 # screen-wide
Rule({"field": "body"}, {"editable": "len(title) > 3"}),
# -> body editable = "(len(title) > 3) and (not locked)"

Force — escaping a chain

Force("...") sets a predicate unconditionally, discarding the lower / less-specific predicates it would otherwise AND with (situ's stand-in for MetaUI's !). Use it to re-enable one field a screen-wide rule disabled:

Rule({}, {"editable": "not locked"}),                        # locks every field
Rule({"field": "notes"}, {"editable": Force("True")}),       # …except notes

explain — see how a field resolves

Because the whole cascade is a pure function over the Screen, you can print a field's derivation from the command line — no runtime inspector:

$ python -m situ.declui explain myapp.screens:ISSUE status detail
Issue.status @ detail
  context:  {'model': 'Issue', 'field': 'status', 'type': 'Status', 'operation': 'detail'}
  rank 0 (type):   Status
  rank 1 (Field):  {'label': 'Status'}
  rank 2 (rules), least specific first:
    [0] specificity 1  {'type': 'Status'} -> {'widget': 'Popup'}
    [1] specificity 2  {'field': 'status', 'operation': 'detail'} -> {'label': 'État', 'widget': 'Combobox'}
  effective:
    label = 'État'   <- changed by a rule
    widget = 'Combobox'   <- changed by a rule

The situ.declui.explain(screen, field, operation) function backs the CLI if you want the trace in a test.

Fail-closed

Rules are checked once per screen at mount:

  • an unknown selector key (not model / field / type / operation) → CompileError;
  • a field selector naming a field the model lacks (a typo that would silently match nothing) → CompileError;
  • a predicate value that isn't a string or Force, or an unknown propertyCompileError.

The line, restated

A rule sheet fatter than the markup it would replace is the signal to eject that screen. The cascade owns the boring 80% — one model → a working screen with per-selector, per-operation defaults — and bespoke markup stays yours in plain situ. It deletes boilerplate, not design.