TinyRuleKit

Core Concepts

TinyRuleKit separates serialized rule data from executable Java behavior. JSON defines what should happen; Java resolvers define how each condition and action is compiled.

Rule

A rule has:

  • trigger: token used when firing the engine.
  • id: optional diagnostic name.
  • enabled: optional flag, default true.
  • conditions: required list with at least one condition.
  • actions: required list with at least one action.

Disabled rules are ignored before compilation, so their resolvers are not called and they are not indexed.

JSON
{
  "id": "join_reward",
  "enabled": true,
  "trigger": "PLAYER_JOINED",
  "conditions": [
    {
      "type": "LEVEL_AT_LEAST",
      "params": {
        "level": 5
      }
    }
  ],
  "actions": [
    {
      "type": "GIVE_COINS",
      "params": {
        "amount": 25
      }
    }
  ]
}

An enabled rule without at least one condition and one action fails during engine compilation. A rule with "enabled": false is skipped before this validation.

RuleContext

RuleContext is the runtime object passed to engine.fire(...). It should contain the domain data that conditions and actions need for one trigger execution:

JAVA
record GameContext(Player player, Wallet wallet) implements RuleContext {
    @Override
    public RuleVariables ruleVariables() {
        return new RuleVariables()
                .put("player", player)
                .put("wallet", wallet);
    }
}

Static conditions and actions can read the typed context directly:

JAVA
(context, variables) -> context.player().level() >= level

ruleVariables() exposes named values for expression conditions and action guards. TinyRuleKit creates those variables lazily during fire, so static rule sets that never read variables do not pay the cost of materializing them.

Keep the context focused on data for the current trigger execution. Services, repositories, and infrastructure should usually stay behind compiled Java conditions or actions instead of being exposed through variables.

Precompilation

JAVA
RuleEngine<GameContext> engine = RuleEngine.builder(GameContext.class)
        .conditions(GameRules::condition)
        .actions(GameRules::action)
        .buildFromJson(json);

At runtime, rules are cached by trigger, so fire only scans rules registered for the requested trigger.

Token Enums

Rule files use strings because they are JSON, but the application should expose those strings through enum collections:

JAVA
enum GameTrigger implements RuleToken {
    PLAYER_JOINED,
    QUEST_COMPLETED
}

enum GameCondition implements RuleToken {
    LEVEL_AT_LEAST,
    HAS_PERMISSION
}

enum GameAction implements RuleToken {
    GIVE_COINS,
    SEND_MESSAGE
}

This keeps the rule vocabulary explicit and avoids consumer code that switches directly on raw strings.

Resolver Factories

Resolver factories turn serialized rule definitions into executable Java functions. A condition factory compiles a condition token:

JAVA
static CompiledCondition<GameContext> condition(String type, RuleParams params) {
    return switch (RuleToken.enumValue(GameCondition.class, type)) {
        case LEVEL_AT_LEAST -> {
            int level = params.getInt("level");
            yield (context, variables) -> context.player().level() >= level;
        }
        case HAS_PERMISSION -> {
            String permission = params.getString("permission");
            yield (context, variables) -> context.player().hasPermission(permission);
        }
    };
}

An action factory compiles an action token:

JAVA
static CompiledAction<GameContext> action(String type, RuleParams params) {
    return switch (RuleToken.enumValue(GameAction.class, type)) {
        case GIVE_COINS -> {
            int amount = params.getInt("amount", 1);
            yield (context, variables) -> context.wallet().addCoins(amount);
        }
        case SEND_MESSAGE -> {
            String message = params.getString("message");
            yield (context, variables) -> context.messages().send(message);
        }
    };
}

Factories run when the engine is built. Prefer doing validation and parameter conversion there so runtime execution stays simple.