Skip to content

Commit

Permalink
feat: graaljs evaluator (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
rapatao authored Jun 9, 2024
1 parent 9f10714 commit bb169b3
Show file tree
Hide file tree
Showing 17 changed files with 348 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
java-version: '17'
cache: 'gradle'

- name: install gpg
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
java-version: '17'
cache: 'gradle'

- name: run project tests
Expand Down
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,4 @@ nbdist/
classes
out/
.scannerwork/

create-order-db.sql
create-sku-db.sql
bench_*.txt
100 changes: 81 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@ Simple yet powerful rules engine that offers the flexibility of using the built-

Below are the available engines that can be used to evaluate expressions:

### Mozilla Rhino (JavaScript) engine implementation

[Mozilla Rhino](https://github.com/mozilla/rhino) is an open-source, embeddable JavaScript interpreter from Mozilla.
This engine implementation supports using JavaScript expressions inside the rule operands and is particularly useful
when rules contain complex logic or when you want to leverage JavaScript's extensive library of functions.

### Kotlin (internal) engine implementation
### Kotlin engine implementation

This engine uses only Kotlin code to support all Operator functions, offering expressive performance. Although it
doesn't support Kotlin expressions inside the expression operands, it can be a suitable choice for simpler rule sets or
Expand All @@ -29,41 +23,104 @@ Supported types:
4. lists
5. arrays

## Get started
```kotlin
val engine = KotlinEvalEngine()
```

#### Gradle

```groovy
implementation "com.rapatao.ruleset:kotlin-evaluator:$rulesetVersion"
```

#### Maven

```xml

<dependency>
<groupId>com.rapatao.ruleset</groupId>
<artifactId>kotlin-evaluator</artifactId>
<version>$rulesetVersion</version>
</dependency>
```

### Mozilla Rhino (JavaScript) engine implementation

[Mozilla Rhino](https://github.com/mozilla/rhino) is an open-source, embeddable JavaScript interpreter from Mozilla.
This engine implementation supports using JavaScript expressions inside the rule operands and is particularly useful
when rules contain complex logic or when you want to leverage JavaScript's extensive library of functions.

To get started, add the following dependency:
```kotlin
val engine = RhinoEvalEngine()
```

#### Gradle

```groovy
implementation "com.rapatao.ruleset:rhino-evaluator:$rulesetVersion"
```

#### Maven

```xml

<dependency>
<groupId>com.rapatao.ruleset</groupId>
<artifactId>rhino-evaluator</artifactId>
<version>$rulesetVersion</version>
</dependency>
```

### GraalVM (JavaScript) engine implementation

[GraalJS](https://www.graalvm.org/latest/reference-manual/js/) is a high-performance JavaScript engine.
This engine implementation supports using JavaScript expressions inside the rule operands and is particularly useful
when rules contain complex logic or when you want to leverage JavaScript's extensive library of functions.

```kotlin
val engine = GraalJSEvalEngine()
```

### Gradle
#### Gradle

```groovy
implementation "com.rapatao.ruleset:ruleset-engine:$rulesetVersion"
implementation "com.rapatao.ruleset:graaljs-evaluator:$rulesetVersion"
```

### Maven
#### Maven

```xml

<dependency>
<groupId>com.rapatao.ruleset</groupId>
<artifactId>ruleset-engine</artifactId>
<artifactId>graaljs-evaluator</artifactId>
<version>$rulesetVersion</version>
</dependency>
```

### Usage
## Get started

After adding the desired engine as the application dependency, copy and past the following code, replacing
the `val engine: EvalEngine = ...` by the desired engine initialization instruction.

The following example initializes an `Evaluator`, and check if the given `rule` is valid to the given `input` data,
printing the `result` in the default output.

### Code example

```kotlin

import com.rapatao.projects.ruleset.engine.Evaluator
import com.rapatao.projects.ruleset.engine.context.EvalEngine
import com.rapatao.projects.ruleset.engine.types.builder.equalsTo

val rule = "item.price" equalsTo 0
val input = mapOf("item" to mapOf("price" to 0))

// val engine = RhinoEvalEngine() // default engine
// val engine = KotlinEvalEngine()
val evaluator = Evaluator(/* engine = engine */)
val engine: EvalEngine = ...
val evaluator = Evaluator(engine = engine)

val result = evaluator.evaluate(rule, mapOf("item" to mapOf("price" to 0)))
val result = evaluator.evaluate(rule, input)
println(result) // true


Expand Down Expand Up @@ -171,7 +228,9 @@ Expression(
)
````

## JSON Serialization
## Expression serialization

### Jackson

All provided operations supports serialization using [Jackson](https://github.com/FasterXML/jackson) with the definition
of a Mixin.
Expand All @@ -191,3 +250,6 @@ val asMatcher: Expression = mapper.readValue(json)
```

Serialized examples can be checked [here](JSON.md)

> Although the example only uses `JSON` as reference, by using the given `Mix-in` class, it should support any
> serialization format provided by the Jackson library, like `YAML` and `XML`.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ allprojects {
withSourcesJar()
withJavadocJar()
toolchain {
languageVersion = JavaLanguageVersion.of(11)
languageVersion = JavaLanguageVersion.of(javaVersion)
}
}

Expand Down
38 changes: 38 additions & 0 deletions graaljs-evaluator/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import kotlinx.kover.gradle.plugin.dsl.MetricType

dependencies {
api project(":core")
api("org.graalvm.polyglot:polyglot:24.0.1")
api("org.graalvm.polyglot:js-community:24.0.1")

testImplementation project(":tests")
}

koverReport {
verify {
rule {
enabled = true
bound {
enabled = true
metric = MetricType.BRANCH
minValue = 80
}
bound {
enabled = true
metric = MetricType.LINE
minValue = 90
}
bound {
enabled = true
metric = MetricType.INSTRUCTION
minValue = 90
}
}
}

defaults {
html {
onCheck = true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.rapatao.projects.ruleset.engine.evaluator.graaljs

import com.rapatao.projects.ruleset.engine.context.EvalContext
import com.rapatao.projects.ruleset.engine.types.Expression
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.Source

/**
* GraalJSContext is a class that implements the EvalContext interface.
* It provides the ability to process expressions using the Graal JS engine.
*
* @property context the GraalJS context object.
*/
class GraalJSContext(
private val context: Context,
) : EvalContext {

/**
* Processes an expression.
*
* @param expression the expression to process
* @return true if the expression is successfully processed, false otherwise
* @throws Exception if the expression processing fails and onFailure is set to THROW
*/
override fun process(expression: Expression): Boolean {
return context.eval(expression.asScript()).asBoolean()
}

/**
* Return the Graal JS context.
*
* @return the Graal JS context.
*/
fun context() = context

private fun Expression.asScript(): Source {
val script = Parser.parse(this)

return Source.newBuilder(
"js",
"true == ($script)",
script
).buildLiteral()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.rapatao.projects.ruleset.engine.evaluator.graaljs

import com.rapatao.projects.ruleset.engine.context.EvalContext
import com.rapatao.projects.ruleset.engine.context.EvalEngine
import com.rapatao.projects.ruleset.engine.evaluator.graaljs.parameters.MapInjector
import com.rapatao.projects.ruleset.engine.evaluator.graaljs.parameters.TypedInjector
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.Engine
import org.graalvm.polyglot.HostAccess
import org.graalvm.polyglot.Value

open class GraalJSEvalEngine(
private val engine: Engine = Engine.newBuilder()
.option("engine.WarnInterpreterOnly", "false")
.build(),
private val contextBuilder: Context.Builder = Context.newBuilder()
.engine(engine)
.option("js.ecmascript-version", "2023")
.allowHostAccess(HostAccess.ALL).allowHostClassLookup { true }
.option("js.nashorn-compat", "true").allowExperimentalOptions(true)
) : EvalEngine {

override fun <T> call(inputData: Any, block: EvalContext.() -> T): T =
createContext().let {
parseParameters(
it.getBindings("js"),
inputData,
)
block(GraalJSContext(it))
}

private fun createContext(): Context {
return contextBuilder.build()
}

override fun name(): String = "GraalJS"

/**
* Parses parameters and injects them into the given scope based on the input data.
*
* @param bindings the values object where the parameters will be injected
* @param inputData the input data containing the parameters
*/
open fun parseParameters(bindings: Value, inputData: Any) {
when (inputData) {
is Map<*, *> -> MapInjector.inject(bindings, inputData)
else -> TypedInjector.inject(bindings, inputData)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.rapatao.projects.ruleset.engine.evaluator.graaljs

import com.rapatao.projects.ruleset.engine.types.Expression
import com.rapatao.projects.ruleset.engine.types.Operator

internal object Parser {

fun parse(expression: Expression): String {
return when (expression.operator) {
Operator.EQUALS -> "==".formatComparison(expression)
Operator.NOT_EQUALS -> "!=".formatComparison(expression)
Operator.GREATER_THAN -> ">".formatComparison(expression)
Operator.GREATER_OR_EQUAL_THAN -> ">=".formatComparison(expression)
Operator.LESS_THAN -> "<".formatComparison(expression)
Operator.LESS_OR_EQUAL_THAN -> "<=".formatComparison(expression)
Operator.STARTS_WITH -> "startsWith".formatWithOperation(expression)
Operator.ENDS_WITH -> "endsWith".formatWithOperation(expression)
Operator.CONTAINS -> formatContainsOperation(expression)
null -> error("when evaluation an expression, the operator cannot be null")
}
}

private fun String.formatComparison(expression: Expression) =
"(${expression.left}) $this (${expression.right})"

private fun String.formatWithOperation(expression: Expression) =
"${expression.left}.${this}(${expression.right})"

private fun formatContainsOperation(expression: Expression) =
"""
(function() {
if (Array.isArray(${expression.left})) {
return ${expression.left}.includes(${expression.right})
} else {
return ${expression.left}.indexOf(${expression.right}) !== -1
}
})()
""".trimIndent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.rapatao.projects.ruleset.engine.evaluator.graaljs.parameters

import org.graalvm.polyglot.Value

internal data object MapInjector : ParameterInjector<Map<*, *>> {

override fun inject(bindings: Value, input: Map<*, *>) {
input.forEach {
set(bindings, it.key.toString(), it.value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.rapatao.projects.ruleset.engine.evaluator.graaljs.parameters

import org.graalvm.polyglot.Value

internal interface ParameterInjector<T> {

fun inject(bindings: Value, input: T)

fun set(bindings: Value, name: String, value: Any?) {
bindings.putMember(name, value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.rapatao.projects.ruleset.engine.evaluator.graaljs.parameters

import org.graalvm.polyglot.Value
import kotlin.reflect.full.memberProperties

internal data object TypedInjector : ParameterInjector<Any> {

override fun inject(bindings: Value, input: Any) {
input.javaClass.kotlin.memberProperties.forEach {
set(bindings, it.name, it.get(input))
}
}
}
Loading

0 comments on commit bb169b3

Please sign in to comment.