Advanced — New in 2.1.0

Introspection & Metadata

As of 2.1.0, everything a Rule, RuleSet, or RuleFlow is made of can be read off the built object without running it. Conditions, actions, and functions describe their expression; RuleFlow commands describe their structure; and definitions carry input parameters, categories, tags, and the source location each artifact was declared at. This is the foundation that tools like rulii-explorer build on — and it is just as useful for your own documentation, auditing, and admin tooling.

ExpressionInfo — What a Condition Is Made Of

Condition, Action, and Function all expose getExpression(), returning an ExpressionInfo alongside the existing getDefinition(). There are three kinds:

Kind What it carries
SCRIPT The language and the source text as written (plus the resolved text the compiler saw, when known)
COMPILED The MethodDefinition of the lambda, method, or class — or null when not introspectable
COMPOSITE The operator (and, or, xor, not, andThen, compose) and the operand expressions in evaluation order
// A composed condition reports its structure
Condition cond = condition((Integer total) -> total >= 100)
        .and(condition((Boolean vip) -> vip));

ExpressionInfo info = cond.getExpression();
info.kind();      // COMPOSITE
info.operator();  // "and"
info.operands();  // the two operand expressions, in order

// A script-backed condition keeps its script
Condition scripted = Condition.builder().build(
        Script.builder().with("js", "ctx.total >= 100").build());

ExpressionInfo scriptInfo = scripted.getExpression();
scriptInfo.kind();        // SCRIPT
scriptInfo.language();    // "js"
scriptInfo.sourceText();  // "ctx.total >= 100"

Script-backed runnables are now ScriptCondition, ScriptAction, and ScriptFunction and keep their Script. Script.getSourceText() returns the text as written — before ${...} placeholder resolution — while getScript() is the resolved text the compiler saw. Chained actions and composed functions expose their parts too, including the result binding name.

CommandInfo — What a RuleFlow Does

Every RuleFlowCommand describes itself through getInfo(), which returns a CommandInfo — one of Run, Apply, Execute, AsyncRun, Await, Bind, When, ForEach, Scope, Exit, or Custom. Container bodies, exception handlers, and continuations are included, so the whole flow is walkable as a tree. RuleFlowDefinition.getCommands() returns that tree for the whole flow:

RuleFlowDefinition def = ruleFlow.getDefinition();

for (CommandInfo cmd : def.getCommands()) {
    if (cmd instanceof CommandInfo.When when) {
        when.condition();   // ExpressionInfo of the condition
        when.then();        // nested commands in the then-branch
        when.otherwise();   // nested commands in the otherwise-branch
    }
}
  • Bind records the names and types a step binds — never the values
  • When, ForEach, and the returning command expose their condition, source, stop condition, and result extractor
  • A custom command can override getInfo() to report its real structure

The definitions themselves got richer, too. RuleFlowDefinition carries the global exception handler, the finalizer and returning expressions, the result type, and getContextLabel() — the name of the context configurator. RuleSetDefinition carries the input parameters and error handler, reports isValidating(), and getStopConditionDefinition() replaces the misnamed getStopActionDefinition(). Input parameters now take a description: param(name, type, required, description).

Category & Tags

Rules, RuleSets, and RuleFlows can say where they belong and what they are about, so tools can group and filter them. The Categorized interface — implemented by RuleDefinition, RuleSetDefinition, and RuleFlowDefinition — has two methods:

  • getCategory() — at most one; / separates the levels of a hierarchy (e.g. Pricing/Discounts); null when none
  • getTags() — any number, each once, in the order given; never null

Declaratively, use the @Category and @Tags annotations beside @Description:

@Rule(name = "DiscountRule")
@Description("Applies the VIP discount to qualifying orders")
@Category("Pricing/Discounts")
@Tags({"vip", "discount"})
public class DiscountRule {
    // given / then as usual
}

Programmatically, the rule, validation rule, rule set, and rule flow builders all take category(String) and tags(String...) / tags(Collection); repeated tags(...) calls add to what was given before:

Rule rule = Rule.builder().with(DiscountRule.class)
        .category("Pricing/Discounts")
        .tags("vip", "discount")
        .build();

rule.getDefinition().getCategory();  // "Pricing/Discounts"
rule.getDefinition().getTags();      // [vip, discount]

Purely descriptive. Categories and tags are never read when rules run — nothing is inferred from packages, files, or names. Values are normalized on the definition: category levels are trimmed and empty levels dropped, tags are trimmed, blanks dropped, and duplicates kept once.

Source Tracking

Every definition carries a SourceDefinition saying where the artifact was declared. In 2.1.0 this finally points at the right place: SourceDefinition.build() records the real caller's location (it used to point at itself), and class-based rules record their rule class rather than the code that called the builder. Loaders that know better can say so explicitly — the rule, validation rule, rule set, and rule flow builders all accept source(SourceDefinition):

// A loader that knows where the artifact was declared
Rule rule = Rule.builder().with(DiscountRule.class)
        .source(SourceDefinition.forFile("classpath:rules/pricing.xml", 42))
        .build();

// Factories: forFile(fileName, line), forClass(type)

rulii-spring 2.1.0 uses exactly this to record the XML file and line for every XML-declared rule, ruleset, and ruleflow — see Rule Scanning.

Registry Names

RuleRegistry.getNames() returns the keys every runnable can be looked up by — the natural starting point for a tool that enumerates everything registered. Note for implementers: the method is abstract, so custom registries must implement it.

Spring users: rulii-spring 2.1.0 adds the Spring side of all of this — XML file/line provenance on bean definitions, category/tags attributes with file-level defaults, and getNames() over all rule beans. See XML Rule Definitions and Rule Scanning.