Skip to content

Typed variants for direction, layer, and currency on tx_template entries #848

Description

@davisv7

Follow-up to #847 .

Motivation

#847 added a build-time validator on NewTxTemplate that catches bad enum-literal expressions like .direction("'Settled'") at template construction. It's not a structural fix — a caller can still express the bug and it fails at build time via runtime evaluation of a stub context.

The stronger fix is to make it impossible to type the bug.

The bug class

Today, NewTxTemplateEntry accepts free-form CEL strings for all fields:

NewTxTemplateEntry::builder()
    .direction("'DEBIT'")       // valid
    .direction("'Settled'")     // valid syntactically, wrong enum — silently accepted before the validator
    .direction("params.dir")    // valid dynamic ref

direction, layer, and currency are the three fields where this pattern is a real trap: most callers use string literals for what are semantically enum values, so a typo ('Settled' for 'SETTLED', 'BTS' for 'BTC') compiles fine and only fails at post time — or worse, at build time only because the previous PR's validator catches it.

Proposal

Introduce variant enums:

pub enum DirectionExpr {
    Literal(DebitOrCredit),
    Cel(String),
}
impl From<DebitOrCredit> for DirectionExpr { /* Literal */ }
impl From<&str> for DirectionExpr        { /* Cel */ }
// same shape for LayerExpr(Layer), CurrencyExpr(Currency)

Callers become:

.direction(DebitOrCredit::Debit)     // typed — new preferred form
.direction("params.dir")             // dynamic — still works via From<&str>

Persistence unchanged: DirectionExpr::Literal renders to CelExpression("'DEBIT'") at the From<NewTxTemplateEntry> for TxTemplateEntry boundary. Storage layer stays CelExpression.

Two variants of the proposal

Additive. Keep From<&str> for DirectionExpr. Any existing .direction("'DEBIT'") still compiles as a Cel variant. The runtime-based check from the previous PR still catches literal-shaped strings with bad values. No downstream breakage. Downside: two ways to express a literal; string API remains a footgun the validator has to keep watching.

Breaking (0.24.x). Remove From<&str> and require callers to disambiguate — either .direction(DebitOrCredit::Debit) or .direction_cel("params.dir"). Bug class becomes a compile error. Every call site inside the repo updates (6+ files: entity.rs, helpers.rs, cala-perf/*, examples/rust, lib.rs doctest); downstream users hit a compile error with a mechanical fix.

Cost estimate

  • New enum types + From impls: ~90 lines
  • Field/setter changes + serialization conversion: ~30 lines
  • Test additions: ~40 lines
  • Call site updates: ~15 lines across 6 files (additive) or ~30 lines (breaking, mostly identical mechanical edits)

Total additive: ~180 lines. Breaking is similar plus caller updates.

What this leaves in place

  • Identifier check from the previous PR (param.foo, params.typo) — orthogonal to this proposal, still valuable.
  • Value-dependent runtime bugs (div-by-zero, dynamic-ref bad values) — can't be caught at build time regardless.

Open questions for maintainers

  1. Additive vs breaking? Cala is 0.x, so a breaking change is politically feasible. But it does impose migration work on downstream users.
  2. Should currency be included? Currency has hundreds of valid variants (all ISO + crypto); typed variant is bulkier but same shape. Or scope it to direction/layer only for now.
  3. Same treatment for units? units is Decimal | CelExpression<Decimal>. Analogous shape, but broader field surface — worth its own conversation.
  4. Where should the variant enums live? In cala-ledger-core-types alongside the underlying enums, or in cala-ledger where the builder is?

Happy to open a PR if there's interest in the additive shape, or to leave this as a design conversation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions