diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 979cb22..478dd7e 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
- java-version: '11'
+ java-version: '17'
cache: 'gradle'
- name: install gpg
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f4e8827..af3a2f4 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -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
diff --git a/.gitignore b/.gitignore
index c2f84a2..06e8f5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,4 @@ nbdist/
classes
out/
.scannerwork/
-
-create-order-db.sql
-create-sku-db.sql
+bench_*.txt
diff --git a/README.md b/README.md
index 613369b..5a115e5 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
+
+
+ com.rapatao.ruleset
+ kotlin-evaluator
+ $rulesetVersion
+
+```
+
+### 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
+
+
+ com.rapatao.ruleset
+ rhino-evaluator
+ $rulesetVersion
+
+```
+
+### 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
com.rapatao.ruleset
- ruleset-engine
+ graaljs-evaluator
$rulesetVersion
```
-### 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
@@ -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.
@@ -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`.
diff --git a/build.gradle b/build.gradle
index 5e54ff0..1eb7401 100644
--- a/build.gradle
+++ b/build.gradle
@@ -38,7 +38,7 @@ allprojects {
withSourcesJar()
withJavadocJar()
toolchain {
- languageVersion = JavaLanguageVersion.of(11)
+ languageVersion = JavaLanguageVersion.of(javaVersion)
}
}
diff --git a/graaljs-evaluator/build.gradle b/graaljs-evaluator/build.gradle
new file mode 100644
index 0000000..0b17475
--- /dev/null
+++ b/graaljs-evaluator/build.gradle
@@ -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
+ }
+ }
+}
diff --git a/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/GraalJSContext.kt b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/GraalJSContext.kt
new file mode 100644
index 0000000..a009659
--- /dev/null
+++ b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/GraalJSContext.kt
@@ -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()
+ }
+}
diff --git a/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/GraalJSEvalEngine.kt b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/GraalJSEvalEngine.kt
new file mode 100644
index 0000000..f44fdfb
--- /dev/null
+++ b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/GraalJSEvalEngine.kt
@@ -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 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)
+ }
+ }
+}
diff --git a/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/Parser.kt b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/Parser.kt
new file mode 100644
index 0000000..a3df66a
--- /dev/null
+++ b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/Parser.kt
@@ -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()
+}
diff --git a/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/parameters/MapInjector.kt b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/parameters/MapInjector.kt
new file mode 100644
index 0000000..b8b7ef4
--- /dev/null
+++ b/graaljs-evaluator/src/main/kotlin/com/rapatao/projects/ruleset/engine/evaluator/graaljs/parameters/MapInjector.kt
@@ -0,0 +1,12 @@
+package com.rapatao.projects.ruleset.engine.evaluator.graaljs.parameters
+
+import org.graalvm.polyglot.Value
+
+internal data object MapInjector : ParameterInjector