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
- Additive vs breaking? Cala is 0.x, so a breaking change is politically feasible. But it does impose migration work on downstream users.
- 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.
- Same treatment for
units? units is Decimal | CelExpression<Decimal>. Analogous shape, but broader field surface — worth its own conversation.
- 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.
Follow-up to #847 .
Motivation
#847 added a build-time validator on
NewTxTemplatethat 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,
NewTxTemplateEntryaccepts free-form CEL strings for all fields:direction,layer, andcurrencyare 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:
Callers become:
Persistence unchanged:
DirectionExpr::Literalrenders toCelExpression("'DEBIT'")at theFrom<NewTxTemplateEntry> for TxTemplateEntryboundary. Storage layer staysCelExpression.Two variants of the proposal
Additive. Keep
From<&str> for DirectionExpr. Any existing.direction("'DEBIT'")still compiles as aCelvariant. 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.rsdoctest); downstream users hit a compile error with a mechanical fix.Cost estimate
Fromimpls: ~90 linesTotal additive: ~180 lines. Breaking is similar plus caller updates.
What this leaves in place
param.foo,params.typo) — orthogonal to this proposal, still valuable.Open questions for maintainers
Currencyhas hundreds of valid variants (all ISO + crypto); typed variant is bulkier but same shape. Or scope it to direction/layer only for now.units?unitsisDecimal | CelExpression<Decimal>. Analogous shape, but broader field surface — worth its own conversation.cala-ledger-core-typesalongside the underlying enums, or incala-ledgerwhere the builder is?Happy to open a PR if there's interest in the additive shape, or to leave this as a design conversation.