Rules vocabulary
Rules is the string-free authoring surface over the declarative Rule and Reduction data. Import it and
reference fields by member expression, so a guard reads as one English-shaped line and still compiles to
inspectable data the machine can export to its IR.
using static Trax.Effect.StateMachine.Rules;A field is selected with Input(...) (the trigger input) or Field(...) (the snapshot context), then a
terminal method produces the Rule or Reduction:
.When(Input((CoinInput i) => i.Coin).IsOneOf("quarter", "dollar"))
.Reduce(Set((UnlockedContext u) => u.PaidWith).FromInput((CoinInput i) => i.Coin))The selector (i => i.Coin) is an expression tree, resolved to its JSON key (camelCase). There are no
field-name strings, so a rename refactors the rule with the record.
Field selectors
| Method | Returns | Selects |
|---|---|---|
Input<TInput, TField>(Expression<Func<TInput, TField>> selector) | FieldMatcher | a field on the trigger input |
Field<TContext, TField>(Expression<Func<TContext, TField>> selector) | FieldMatcher | a field on the snapshot context |
Guard predicates
Terminal methods on FieldMatcher, each returning a Rule:
| Method | Holds when |
|---|---|
Present() | the field exists and is not null |
Absent() | the field is missing or null |
NonEmpty() | a non-empty string or array |
IsOneOf(params string[] values) | a string equal to one of values |
OfType(JsonFieldType type) | the field is present and of that JSON type |
GreaterThan(double value) | a number greater than value |
EqualTo(double value) | a number equal to value |
CountGreaterThan(int value) | an array with more than value elements |
CountAtLeast(int value) | an array with at least value elements |
LengthGreaterThan(int value) | a string whose length is more than value |
LengthAtLeast(int value) | a string whose length is at least value |
IsTrue() / IsFalse() | a boolean field equal to true / false |
ArrayOf(JsonFieldType type) | an array whose every element is of type |
Every predicate is total: a missing or wrong-typed field is false, never an exception. That is what lets
guards run against an in-progress draft without throwing.
Combinators
| Method | Returns | Holds when |
|---|---|---|
All(params Rule[] rules) | Rule | every sub-rule holds (an empty set is vacuously true) |
Any(params Rule[] rules) | Rule | at least one sub-rule holds (an empty set is false) |
.When(All(
Field((CartContext c) => c.Items).CountAtLeast(1),
Field((CartContext c) => c.Total).GreaterThan(0)))Reductions
A reducer computes the destination context. Build one with these factories:
| Factory | Produces |
|---|---|
Keep() | the current context, unchanged (the default when no reducer is declared) |
Clear() | an empty context |
Reset() | the machine's initial context |
Set<TContext, TField>(Expression<Func<TContext, TField>> field) | a SetBuilder; complete it below |
Set(...) clones the context and assigns one field:
| Completion | Sets the field to |
|---|---|
.FromInput<TInput, TField>(Expression<Func<TInput, TField>> selector) | a field copied from the trigger input |
.ToValue(JsonNode? value) | a literal JSON value |
Beyond the vocabulary
The vocabulary covers the common guards and reducers on purpose: a small, fixed set keeps the exported IR a
declarative contract rather than a serialized-logic DSL. Logic that does not fit (a formula reducer, a
multi-field invariant) has two homes. Rule.Custom(name) and Reduction.Custom(name) keep it in the IR by name,
with a handler bound per runtime (CustomGuard / CustomReducer in C#). The delegate overloads,
When(Func...) and Reduce(Func...), run the same in C# but are lost to the export: the edge is exported
with no guard or reducer, as an unconditional edge that keeps the context, so a generated twin disagrees with
the server on it. Use the delegates only on a machine with no twin. See
Delegate vs declarative.