TinyRuleKit
Expressions
Expressions are optional. Static rules do not require an expression engine or MVEL on the
classpath. When expressions are used, TinyRuleKit compiles them during engine creation and evaluates them with
variables provided by RuleContext.
The default expression engine uses MVEL. Projects that want the default engine must add
org.mvel:mvel2; projects that use only static rules or configure a custom
ExpressionEngine do not need it.
When the builder explicitly enables expressions and still uses the default engine, TinyRuleKit loads that
default engine during build and reuses the same loaded engine for later builders. Static rule documents do not
trigger this load unless they actually contain an expr condition or action when
guard.
Use expressions for small, data-oriented checks. Keep domain behavior in Java conditions and actions when the logic needs validation, services, side effects, or complex decisions.
Expression Conditions
A condition with type expr must return a boolean:
{
"type": "expr",
"expr": "player.level() >= 5 && wallet.coins() < 100"
}
Expression conditions are useful when the check only reads values already exposed by the context.
Action Guards With when
Actions can define a when expression. This is a subcondition for that action only, evaluated
after the rule-level conditions pass:
{
"trigger": "PLAYER_JOINED",
"conditions": [
{
"type": "LEVEL_AT_LEAST",
"params": {
"level": 5
}
}
],
"actions": [
{
"type": "SEND_MESSAGE",
"when": "player.hasPermission('vip')",
"params": {
"message": "Welcome VIP"
}
},
{
"type": "GIVE_COINS",
"when": "wallet.coins() < 100",
"params": {
"amount": 25
}
}
]
}
Use when to avoid creating several triggers or duplicating rules just because some actions
have extra requirements. Rule-level conditions decide whether the rule matches; action guards decide which
actions inside that matched rule run.
Variables
Expressions read values from RuleVariables:
record GameContext(Player player, Wallet wallet) implements RuleContext {
@Override
public RuleVariables ruleVariables() {
return RuleVariables.create()
.put("player", player)
.put("wallet", wallet)
.putScalar("environment", "production");
}
}
The variable names in JSON must match the names inserted into RuleVariables. Use
put for objects that implement ExpressionAccessible; TinyRuleKit stores their safe
expression view. Use putScalar explicitly for primitive wrapper values and strings. Other
objects are rejected so expressions cannot inspect domain objects, services, or infrastructure APIs without
a safe view.
See Expression Views before exposing domain objects.
Custom Expression Engine
Use expressionEngine to plug in another expression framework or a small project-owned
expression language:
ExpressionEngine engine = expression -> {
RuleExpression compiled = variables -> switch (expression) {
case "vip" -> variables.get("permission").orElse("").equals("vip");
case "highLevel" -> ((Integer) variables.get("level").orElse(0)) >= 10;
default -> throw new IllegalArgumentException("Unknown expression: " + expression);
};
return compiled;
};
RuleEngine<GameContext> rules = RuleEngine.builder(GameContext.class)
.actions(GameRules::action)
.expressionEngine(engine)
.buildFromJson(json);
The engine is used only when an expression condition or action when guard is compiled. Static
rule sets do not call it.
When To Use Expressions
Good candidates:
- simple comparisons over values already exposed by the context
- small feature flags or thresholds stored in JSON
- action guards that should not duplicate a whole rule
Prefer Java resolver factories when:
- logic needs services, repositories, IO, or side effects
- the expression would become long or hard to review
- invalid parameters should fail with a domain-specific message
- the operation must be strongly typed and easy to test
Next Step
Continue with Expression Views to expose readable objects safely.