81 — Writing a lens
GuideRendered from 81-lens-authoring-guide.md in the public repository. Edits belong there.
This page lists the steps for adding a lens and states how the workbench chooses one.
A lens converts a datum's stored value into the value the workbench shows, and converts what an operator types back into the stored value. It does not change how the value is stored. Example: FiatCentsLens shows a stored 450 as $4.50, and stores $4.50 typed into a price cell as 450.
The contract
Lens in micyte/state_machine/lens/base.py is an abstract class:
| Member | Direction | Returns |
|---|---|---|
lens_id | — | Unique id, lower case. |
decode(canonical_value) | stored → shown | The display value. |
encode(display_value) | typed → stored | The canonical value. |
validate_display(display_value) | typed | A tuple of issue codes; () means valid. |
A lens holds no state and does no I/O. The registry keeps one shared instance per key.
Built-in lenses (all in base.py):
lens_id | Class | Behaviour |
|---|---|---|
identity | IdentityLens | Returns the value unchanged. The fallback. |
trimmed_string | TrimmedStringLens | Strips surrounding whitespace. Base class for the rest. |
samras_title | SamrasTitleLens | Decodes a SAMRAS-encoded title. |
email_address | EmailAddressLens | Lower-cases and normalizes an email address. |
secret_reference | SecretReferenceLens | Shows a secret reference, never the secret. |
numeric_hyphen | NumericHyphenLens | Shows SAMRAS/HOPS node addresses as a-b-c. |
fiat_cents | FiatCentsLens | Integer cents ↔ $d.cc, through parse_cents/format_cents. Symmetric. |
binary_text | BinaryTextLens | Bit string → ASCII text, 8 bits per character; non-printable bytes show as ·; an undecodable string shows as <N> bits. encode keeps the bit string. |
How a lens is chosen
DatumLensRegistry.resolve in micyte/state_machine/lens/registry.py checks, in order:
flag_lens_id— a lens named by a hyphae flag (matched_on="hyphae_flag"). No caller passes it; see Not built.recognized_familyin_family_lenses— for examplefiat_babelette→fiat_cents,samras→numeric_hyphen,title_babelette→binary_text.overlay_kindin_overlay_lenses.primary_value_kindin_value_kind_lenses— for examplefiat_cents,binary_string,literal_text.- Otherwise
identity(matched_on="fallback").
Keys are lower-cased before lookup.
If the instance has disabled the chosen lens, resolve returns identity with matched_on="lens_disabled". The disabled set is kept per instance by fnd_app/instances/_shared/runtime/portal_lens_runtime.py, read with GET /portal/api/lenses and changed with POST /portal/api/lenses/toggle.
The workbench (micyte/tools/workbench_ui/service.py) resolves one lens for each row and one for each reference binding in a record row. A binding's lens is chosen from its own expected_value_kind, so a price inside a record row is shown with fiat_cents even when the row's first value is a node address. Each row carries resolved_lens and resolved_lens_match.
Steps
- Write the class. Subclass
TrimmedStringLens(orLens) inbase.py, setlens_id, and overridedecode,encodeandvalidate_displayas needed. - Decide whether it is symmetric. A symmetric lens stores what
encodereturns (FiatCentsLens,EmailAddressLens). A display-only lens leaves the stored value as typed (BinaryTextLens). For a symmetric lens,decode(encode(d))must returndapart from deliberate normalization. - Bind it. Add an instance to
_family_lenses,_overlay_lensesor_value_kind_lensesinDatumLensRegistry.__init__. Bind by family when every datum of a recognized family needs it; by value kind when it follows the value's shape. - Add it to
_by_lens_idand_LENS_METADATA._by_lens_idis the catalog the toggle API accepts;_LENS_METADATAgives its label and description. A lens missing from_by_lens_idcannot be toggled and cannot be named by a flag. - Export it from
micyte/state_machine/lens/__init__.pyif callers import it by name. - Return issue codes, not prose.
StagingArea.stage_with_lens(micyte/state_machine/nimm/staging.py) callsvalidate_displaybeforeencodeand stores the codes on the staged value. Use short snake_case codes, for exampleprice_required,price_invalid,binary_text_invalid.
Tests
| Test | What it checks |
|---|---|
micyte/tests/unit/test_state_machine_lens_registry.py | Resolution order, fallback, the catalog. |
micyte/tests/unit/test_fiat_lens.py | FiatCentsLens round trip and refusals. |
fnd_app/tests/unit/test_lens_management.py | The per-instance disabled set and the toggle API. |
Add a case that resolves your binding to your lens_id and one that round-trips a value.
Not built
- Binding a lens to a hyphae value.
resolveacceptsflag_lens_id, but the hyphae flag registry is empty and no caller passes the argument. See 60. - A surface for the toggles. The toggle API works; the panel module for it is mounted by no surface.