Package io.github.brantunger.unruly.api.language


@NullMarked package io.github.brantunger.unruly.api.language
The interfaces an expression language implements, so rule conditions and actions can be compiled and run, and the ones the engine passes to it. MVEL, from the unruly-engine artifact, is one such language. An engine has the languages its builder is given, or else those found with ServiceLoader.

Writing a language:

  1. Implement ExpressionLanguage: a name, and a new ExpressionCompiler for each rule list. Optionally, also ExpressionLanguage.prepare(), to initialize the language's classes before a first compile or run deep in a stack, and ExpressionLanguage.reservedFactNames(), the fact names it reserves: "output" by default, for every rule list unless ExpressionLanguage.reservesForEveryRuleList() says only for those that use the language.
  2. In the compiler, compile each Expression into a CompiledCondition or a CompiledAction, and throw InvalidExpressionException for one you reject, such as a condition that assigns to a fact. MessageText shortens and escapes text you didn't write for a message, as the engine does; Throwables reads what you catch as the engine does, a stack overflow and a fatal error included, so a language keeps no rule of its own for them; and ImportLimits bounds an import an expression makes inline as the engine bounds its own.
  3. Keep what changes while expressions run in a Session, one for each compiled copy of the rules, or return Session.none() when nothing does.
  4. Let an action change ActionContext.output() in place and return ActionResult.done(), or return ActionResult.set(java.util.Map) with properties for the engine to set; a language whose values aren't Java's, such as one whose numbers are all doubles, converts them for the output's setters in ExpressionCompiler.convertOutput(Object, Class). FactProperties reads a fact's property the way rules expect, whether the fact is a record, a bean or a map.
  5. Let the engine find the language: a META-INF/services file or a module's provides clause for ServiceLoader, or language(...) on the RulesEngineBuilder.
  6. Test it with the unruly-engine-test artifact: extend io.github.brantunger.unruly.test.ExpressionLanguageContractTest, and create the sealed contexts for unit tests with io.github.brantunger.unruly.test.LanguageTestContexts.

Who implements what:

  • Expression languages implement ExpressionLanguage, ExpressionCompiler, CompiledCondition, CompiledAction and Session. A method added to one of these is a default method, so a language written against an earlier 2.x release keeps compiling and working.
  • The engine implements CompileContext (the imports, the class loader, the output type, the declared facts, this language's options and its own imports, and a way to report warnings), EvaluationContext (the facts, and whether the run must stop) and ActionContext (the same, plus the output object). They're sealed to the engine's own implementations, so a language can't implement them, and they can gain any method.

What the engine enforces for every language: rule names are unique and conditions and actions aren't blank; rules are evaluated in priority order; a condition must evaluate to a Boolean; no fact has a blank name or a name one of the engine's languages reserves, which is "output" by default (see ExpressionLanguage.reservedFactNames()), for every rule list or, for a language that says so, for those with a rule in it (see ExpressionLanguage.reservesForEveryRuleList()); the facts a condition or action sees are read-only; a run is stopped between expressions when its thread is interrupted or its deadline passes; a failure while rules compile is reported as a RuleCompilationException, and a failure while they run as a RuleExecutionException, which the run's listeners see once the run has started; and a fact name a language rejects fails the run with an IllegalArgumentException, or the load, for a declared fact, unless the language's compiler says none of its rules reads the fact (see ExpressionCompiler.factNamesRead()).

Threads: a language instance may serve several engines at once. prepare() may be called on several threads at once, by engines built, or rule lists first loaded or validated, at the same time, so it must be safe to call concurrently. Each load() or validate() creates its own compiler and calls its compile methods on the thread that called it, so two loads at once compile on two threads; checkFactName is called on that thread for each declared fact, and by runs on many threads at once; newSession is called by runs on many threads at once. Compiled conditions and actions are shared by every run, each with its own session, which one run uses at a time.

Types in this package are non-null unless annotated Nullable.

See Also:
  • Class
    Description
    What an action runs against: the run's facts and the output object.
    What an action did: changed the output object itself (ActionResult.done()), or produced properties for the engine to set on it (ActionResult.set(Map)).
    An action a language registered with EvaluationContext.onCancel(Runnable), to be run when its run passes its deadline.
    What every compilation of a rule list is given: the imports the engine was built with, from RulesEngineBuilder.imports(String...), the class loader to look classes up with, the type of the output object, this language's options and its own imports, and a way to report warnings.
    A compiled action, shared by every run of its rule list.
    A compiled condition, shared by every run of its rule list.
    What evaluating a condition returned, and, from a language that can explain it, why: the value, which must be a Boolean, and a detail.
    What a condition is evaluated against.
    One condition or action of a rule, as an ExpressionCompiler compiles it: which rule it belongs to, whether it's the condition or the action, and its text.
    Compiles the conditions and actions of one rule list, checks the names of the facts they run against, and creates the sessions they run with.
    A language that rule conditions and actions are written in, such as MVEL.
    Reads a property of a fact the way rules expect: a record component, a JavaBean getter or a Map key.
    How large an import may be, as the engine bounds the imports it is built with, for a language that reads an import in an expression's own text: it bounds that import the same way, and rejects it with the same reason, rather than keeping a copy of the limits that could drift.
    Shortens and escapes text a language didn't write, such as a fact name, an option value, an import or its library's own error text, for a message, as the engine does in its own messages.
    A language's state for one run of a rule list at a time, such as a single-threaded interpreter context.
    Reads what an expression language, a rule or the application's code threw, as the engine reads it: which errors the engine rethrows unchanged, what a stack overflow looks like once the JDK or a library has wrapped it, and the message and causes of an exception the language didn't create, whose getMessage() or getCause() may throw.