MiCyte

← The wiki

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:

MemberDirectionReturns
lens_id—Unique id, lower case.
decode(canonical_value)stored → shownThe display value.
encode(display_value)typed → storedThe canonical value.
validate_display(display_value)typedA 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_idClassBehaviour
identityIdentityLensReturns the value unchanged. The fallback.
trimmed_stringTrimmedStringLensStrips surrounding whitespace. Base class for the rest.
samras_titleSamrasTitleLensDecodes a SAMRAS-encoded title.
email_addressEmailAddressLensLower-cases and normalizes an email address.
secret_referenceSecretReferenceLensShows a secret reference, never the secret.
numeric_hyphenNumericHyphenLensShows SAMRAS/HOPS node addresses as a-b-c.
fiat_centsFiatCentsLensInteger cents ↔ $d.cc, through parse_cents/format_cents. Symmetric.
binary_textBinaryTextLensBit 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:

  1. flag_lens_id — a lens named by a hyphae flag (matched_on="hyphae_flag"). No caller passes it; see Not built.
  2. recognized_family in _family_lenses — for example fiat_babelette → fiat_cents, samras → numeric_hyphen, title_babelette → binary_text.
  3. overlay_kind in _overlay_lenses.
  4. primary_value_kind in _value_kind_lenses — for example fiat_cents, binary_string, literal_text.
  5. 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

  1. Write the class. Subclass TrimmedStringLens (or Lens) in base.py, set lens_id, and override decode, encode and validate_display as needed.
  2. Decide whether it is symmetric. A symmetric lens stores what encode returns (FiatCentsLens, EmailAddressLens). A display-only lens leaves the stored value as typed (BinaryTextLens). For a symmetric lens, decode(encode(d)) must return d apart from deliberate normalization.
  3. Bind it. Add an instance to _family_lenses, _overlay_lenses or _value_kind_lenses in DatumLensRegistry.__init__. Bind by family when every datum of a recognized family needs it; by value kind when it follows the value's shape.
  4. Add it to _by_lens_id and _LENS_METADATA. _by_lens_id is the catalog the toggle API accepts; _LENS_METADATA gives its label and description. A lens missing from _by_lens_id cannot be toggled and cannot be named by a flag.
  5. Export it from micyte/state_machine/lens/__init__.py if callers import it by name.
  6. Return issue codes, not prose. StagingArea.stage_with_lens (micyte/state_machine/nimm/staging.py) calls validate_display before encode and stores the codes on the staged value. Use short snake_case codes, for example price_required, price_invalid, binary_text_invalid.

Tests

TestWhat it checks
micyte/tests/unit/test_state_machine_lens_registry.pyResolution order, fallback, the catalog.
micyte/tests/unit/test_fiat_lens.pyFiatCentsLens round trip and refusals.
fnd_app/tests/unit/test_lens_management.pyThe 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. resolve accepts flag_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.