Build complex rules, serialize them as JSON, and execute them in Scala.
Json-logic-scala enables you to serialize in JSON format logical expressions. It also enables you to load a scala object from a logical expression/JSON.
Due to Scala's strong static typed language nature, json-logic-scala requires JSON to add tell type in json.
The JsonLogic format is designed to allow you to share rules (logic) between front-end and back-end code (regardless of language difference), even to store logic along with a record in a database.
Logic that has been exported from another language can be applied quickly on scala.
This project is compiled, tested, and published for the following Scala versions:
- 2.11.12
- 2.12.6
- 2.13.2
-
Main concepts: Boolean-Algebra-Tree
-
4.1 Type information
To get started, add json-logic-scala as a dependency to your project:
-
sbt
libraryDependencies += "com.celadari" %% "json-logic-scala" % "2.0"
-
Gradle
compile group: 'com.celadari', name: 'json-logic-scala_2.12', version: '2.0'
-
Maven
<dependency> <groupId>com.celadari</groupId> <artifactId>play-json_2.12</artifactId> <version>2.0</version> </dependency>
Json-logic-scala supports Scala 2.11 and 2.12. Choosing the right JAR is automatically managed in sbt. If you're using Gradle or Maven then you need to use the correct version in the artifactId.
Boolean expressions are complex boolean statements composed of atoms, unary, binary and multiple operators. Atoms are assigned a value, and can be fed to a binary or unary expression. For example, the logical expression
can be parsed to the following Abstract Syntax Tree:
A tree representation of the logical expression is very convenient. After isolating the outermost operator of the expression (the operator which is enclosed with the fewest amount of parentheses), the logical expression can be split on said operator into different branches representing themselves logical expressions. These different expressions can be further split into different branches until reaching leaves Node which represent single atoms.
Evaluating the logical expression in its tree representation is evaluated recursively. Each Internal Node needs to have its children nodes evaluated before being evaluated. Leaf Nodes represent variables/values.
A boolean decision tree is represented by the JsonLogicCore
class - which has two subtypes:
A ComposeLogic
class is an Internal Node in the boolean-algebra-tree.
It is defined by two fields:
operator
:String
the codename of the operator.conditions
:Array[JsonLogicCore]
array of sub-conditions this node applies to.
It represents a basic value for an operand in order to produce a condition. It is defined by two fields.
operator
:String
whose value is supposed to be always"var"
.valueOpt
:Option[T]
the value object itself to feed an operand.typeOpt
:Option[TypeValue]
the type associated to this value.variableNameOpt
:Option[String]
name of variable it references if inside composition function.pathNameOpt
:Option[String]
key associated to this value in json snippet.
Let's suppose you have a parquet/csv file on disk and you want to remember/transfer filtering rules before loading it.
price (€) | quantity | label | label2 | clientID | date |
---|---|---|---|---|---|
54 | 2 | t-shirts | t-shirts | 245698 | 2018-01-12 09:12:00 |
68 | 1 | pants | shoes | 478965 | 2019-07-24 15:24:00 |
10 | 2 | sockets | hat | 478963 | 2020-02-14 16:22:00 |
........... | .......... | .......... | .......... | .......... | ..................... |
Let's suppose we are only interested in rows which satisfy logical expression:
If you want to store the logic (logical expression) in an universal format that can be shared between scala, R, python code you can store in jsonLogic format.For the logic:
{
"and": [{
"<=": [
{"var": "colA", "type": "column"},
{"var": "valA", "type": "value"}
]
},
{
"!=": [
{"var": "colB", "type": "column"},
{"var": "colC", "type": "column"}
]
}
]
}
For the values:
{
"colA": {"name": "price (€)"},
"valA": {"value": 20, "type": "int"},
"colB": {"name": "label"},
"colC": {"name": "label2"}
}
To use Json Logic Scala, you should start by defining or importing a
JsonLogicCore
instance (we'll see how to evaluate it latter below).
Type is annotated after the field "type"
in a "var"
operator JSON
(i.e. the leaf node in corresponding syntax tree).
A simple type is simply defined by its codename
field value.
[{
"...": [
{"var": "price_value", "type": {"codename": "int"}}
]
},
{
"price_value": ...
}]
A higher type is a composition of simple and/or higher types. A higher type represents generic types in Scala like arrays, options, and maps.
It is recursively defined by its codename
field value and its paramType
field value.
**
In the following example, the variable price_values
is to be parsed as an Array[Int]
[{
"...": [
{"var": "price_values", "type": {"codename": "array", "paramType": {"codename": "int"}}}
]
},
{
"price_values": ...
}]
Json-logic-scala comes with built-in naming convention for basic types
"type" field |
Scala type |
---|---|
"byte" |
Byte |
"short" |
Short |
"int" |
Int |
"long" |
Long |
"string" |
String |
"float" |
Float |
"double" |
Double |
"boolean" |
Boolean |
"array" |
Array |
"map" |
Map |
"option" |
Option |
The Deserializer
utility class converts JsonLogic-Typed data
into Scala data structure.
Configure the Deserializer
class with the DeserializerConf
object.
This object defines how types in JsonLogic-Typed map to a Scala data structure.
If no custom object is provided, the default DeserializerConf
is used .
import play.api.libs.json.Json
val jsonString: String = ...
implicit val deserializerConf = DeserializerConf.createConf(...)
implicit val deserializer = new Deserializer()
val jsonLogicCore = deserializer.deserialize(jsonString)
The Serializer
utility class converts a Scala data structure to JsonLogic-Typed
data.
Configure theSerializer
class with the SerializerConf
object.
This object defines how Scala data structures map to the JsonLogic-Typed format.
If no custom object is provided, the default SerializerConf
is used .
import play.api.libs.json.Json
val jsonLogicCore: JsonLogicCore = ...
implicit val serializerConf = SerializerConf.createConf(...)
implicit val serializer = new Serializer()
val jsonString = serializer.serialize(jsonLogicCore)
Evaluating a logical expression and getting its result is the main goal in most cases. Generally, logic/rules are received from another language/application and we want to apply this logic to our Scala program. Evaluating the logical expression is performed by applying a reduce function to the boolean-algebra-tree.
For more information please check online documentation.
For more information please check online documentation.
For more information please check online documentation.
API Documentation available here.
- Charles-Edouard LADARI
- Matt DODSON
Json Logic Scala is licensed under the MIT License.
MIT License
Copyright (c) 2019 celadari
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.