|
1 | 1 | # Json Logic Scala |
2 | 2 | Build complex rules, serialize them as JSON, and execute them in Scala |
| 3 | +Json-logic-scala enables you to serialize in JSON format logical expressions. |
| 4 | +It also enables you to load a scala object from a logical expression/JSON. |
3 | 5 |
|
4 | | -## Problem Statement |
| 6 | +Due to Scala's strong static typed language nature, json-logic-scala requires JSON to add tell type in json. |
5 | 7 |
|
6 | | -This little project aims to solve the following problems: |
| 8 | +### Why would you use json-logic-scala ? |
| 9 | +The [JsonLogic format](http://jsonlogic.com/) is designed to allow you to share rules (logic) between |
| 10 | +front-end and back-end code (regardless of language difference), even to store |
| 11 | +logic along with a record in a database. |
7 | 12 |
|
8 | | -1. Make stuff more awesome. |
9 | | -2. Remove the less awesome stuff from your project. |
| 13 | +Logic that has been exported from another language can be applied quickly on |
| 14 | +scala. |
10 | 15 |
|
11 | | -## Configuration |
| 16 | +### Scala Versions |
12 | 17 |
|
13 | | -Add the Sonatype.org Releases repo as a resolver in your `build.sbt` or `Build.scala` as appropriate. |
| 18 | +This project is compiled, tested, and published for the following Scala versions: |
| 19 | +1. 2.10.3 |
| 20 | +2. 2.11.12 |
| 21 | +3. 2.12.6 |
14 | 22 |
|
15 | | -```scala |
16 | | -resolvers += "Sonatype.org Releases" at "https://oss.sonatype.org/content/repositories/releases/" |
| 23 | +## 1. Installation |
| 24 | + |
| 25 | +To get started, add json-logic-scala as a dependency to your project: |
| 26 | + |
| 27 | +* sbt |
| 28 | + ```sbt |
| 29 | + libraryDependencies += "com.github.celadari" %% "json-logic-scala" % "latest.integration" |
| 30 | + ``` |
| 31 | + |
| 32 | +* Gradle |
| 33 | + ```gradle |
| 34 | + compile group: 'com.github.celadari', name: 'json-logic-scala_2.12', version: 'latest.integration' |
| 35 | + ``` |
| 36 | +* Maven |
| 37 | + ```maven |
| 38 | + <dependency> |
| 39 | + <groupId>com.github.celadari</groupId> |
| 40 | + <artifactId>play-json_2.12</artifactId> |
| 41 | + <version>latest.integration</version> |
| 42 | + </dependency> |
| 43 | + ``` |
| 44 | +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. |
| 45 | + |
| 46 | +## 2. Main concepts: Boolean-Algebra-Tree |
| 47 | +Boolean expressions are complex boolean statements composed of atoms, unary, binary and multiple operators. |
| 48 | +Atoms are assigned a value, and can be fed to a binary or unary expression. |
| 49 | +For example, the logical expression |
| 50 | + |
| 51 | +<p align="center"> |
| 52 | + <img src=".img/logical_expression.png" alt="drawing" width="300"/> |
| 53 | +</p> |
| 54 | + |
| 55 | +can be parsed to the following Abstract Syntax Tree: |
| 56 | + |
| 57 | +<p align="center"> |
| 58 | + <img src=".img/boolean_logical_tree.png" alt="drawing" width="500"/> |
| 59 | +</p> |
| 60 | + |
| 61 | +A tree representation of the logical expression is very convenient. After isolating the outermost operator of the |
| 62 | +expression (the operator which is enclosed with the fewest amount of parentheses), the logical expression can be split on |
| 63 | +said operator into different branches representing themselves logical expressions. These different expressions can be further |
| 64 | +split into different branches until reaching leaves Node which represent single atoms. |
| 65 | + |
| 66 | +Evaluating the logical expression in |
| 67 | +its tree representation is evaluated recursively. Each Internal Node needs to have its children nodes evaluated before |
| 68 | +being evaluated. Leaf Nodes represent variables/values. |
| 69 | + |
| 70 | +A boolean decision tree is represented by the `JsonLogicCore` class - which has two subtypes: |
| 71 | + |
| 72 | +### 2.1 `ComposeLogic`: Internal Node. |
| 73 | +A `ComposeLogic` class is an Internal Node in the boolean-algebra-tree. |
| 74 | +It is defined by two fields: |
| 75 | +- `operator`: `String` the codename of the operator. |
| 76 | +- `conditions`: `Array[JsonLogicCore]` array of sub-conditions this node applies to. |
| 77 | + |
| 78 | +### 2.2 `ValueLogic`: Leaf Node. |
| 79 | +It represents a basic value for an operand in order to produce a condition. |
| 80 | +It is defined by two fields. |
| 81 | +- `operator`: `String` whose value is supposed to be always `"var"`. |
| 82 | +- `value`: `T` the value object itself to feed an operand. |
| 83 | + |
| 84 | +## 3. Example |
| 85 | +Let's suppose you have a parquet/csv file on disk and you want to remember/transfer |
| 86 | +filtering rules before loading it. |
| 87 | + |
| 88 | +| price (€) | quantity | label | label2 | clientID | date | |
| 89 | +|-----------|----------|----------|----------|----------|---------------------| |
| 90 | +| 54 | 2 | t-shirts | t-shirts | 245698 | 2018-01-12 09:12:00 | |
| 91 | +| 68 | 1 | pants | shoes | 478965 | 2019-07-24 15:24:00 | |
| 92 | +| 10 | 2 | sockets | hat | 478963 | 2020-02-14 16:22:00 | |
| 93 | +|...........|..........|..........|..........|..........|.....................| |
| 94 | + |
| 95 | +Let's suppose we are only interested in rows which satisfy logical expression: |
| 96 | +$price \gte 20 \and label \neq label2$. |
| 97 | +If you want to store the logic (logical expression) in an universal format that can |
| 98 | +be shared between scala, R, python code you can store in jsonLogic format. |
| 99 | + |
| 100 | +For the logic: |
| 101 | +```json |
| 102 | +{ |
| 103 | + "and": [{ |
| 104 | + "<=": [ |
| 105 | + {"var": "colA", "type": "column"}, |
| 106 | + {"var": "valA", "type": "value"} |
| 107 | + ] |
| 108 | + }, |
| 109 | + { |
| 110 | + "!=": [ |
| 111 | + {"var": "colB", "type": "column"}, |
| 112 | + {"var": "colC", "type": "column"} |
| 113 | + ] |
| 114 | + } |
| 115 | + ] |
| 116 | +} |
| 117 | +``` |
| 118 | +For the values: |
| 119 | +```json |
| 120 | +{ |
| 121 | + "colA": {"name": "price (€)"}, |
| 122 | + "valA": {"value": 20, "type": "int"}, |
| 123 | + "colB": {"name": "label"}, |
| 124 | + "colC": {"name": "label2"} |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +## 4. Read/Write json |
| 129 | + |
| 130 | +To use **Json Logic Scala**, you should start by defining or importing a |
| 131 | +`JsonLogicCore` instance (we'll see how to evaluate it latter below). |
| 132 | + |
| 133 | +### Type information |
| 134 | +A Leaf Node has the following json-logic-scala format |
| 135 | +```json |
| 136 | +{"var": {...}, "type": "something"} |
17 | 137 | ``` |
| 138 | +The `"var"` field represents the Leaf Node itself while the `"type"` fields is a string naming the type of the Leaf Node. |
| 139 | +This is due to Scala being a strong static type language and types just cannot be inferred automatically from a json string. |
| 140 | +Thus, the `"type"` fields is required for telling the JVM how to parse the `"var"` field. |
18 | 141 |
|
19 | | -Add **Json Logic Scala** as a dependency in your `build.sbt` or `Build.scala` as appropriate. |
| 142 | +*Json-logic-scala* **comes with built-in naming convention for basic types** |
20 | 143 |
|
| 144 | +| `"type"` field | Scala type | |
| 145 | +|:--------------:|:----------:| |
| 146 | +|`"byte"` | `Byte` | |
| 147 | +|`"short"` | `Short` | |
| 148 | +|`"int"` | `Int` | |
| 149 | +|`"long"` | `Long` | |
| 150 | +|`"string"` | `String` | |
| 151 | +|`"float"` | `Float` | |
| 152 | +|`"double"` | `Double` | |
| 153 | +|`"boolean"` | `Boolean` | |
| 154 | + |
| 155 | + |
| 156 | +### 4.1 Read json: Define Decoder |
| 157 | +A decoder defines how to read/parse a JSON string/[JsValue](https://www.playframework.com/documentation/latest/api/scala/play/api/libs/json/JsValue.html) |
| 158 | +to a `JsonLogicCore` instance. |
| 159 | + |
| 160 | +* **`Decoder` class must be instantiated** if you need to parse a Json string to a Scala object. |
| 161 | + ```scala |
| 162 | + implicit val decoder = new Decoder |
| 163 | + val myVal = Json.parse(json).as[JsonLogicCore] |
| 164 | + ``` |
| 165 | + |
| 166 | +* **Custom scala object/classes**: |
| 167 | + - You *don't need to change built-in naming convention*. |
| 168 | + Just instantiate `Decoder` class and define its `customDecode` method. |
| 169 | + ```scala |
| 170 | + implicit val decoder = new Decoder{ |
| 171 | + override def customDecode(json: JsValue, otherType: String): Any = |
| 172 | + otherType match { |
| 173 | + case "car" => json.as[Car] |
| 174 | + case "plane" => json.as[Plane] |
| 175 | + case _ => throw new IllegalArgumentException("Wrong argument.") |
| 176 | + } |
| 177 | + } |
| 178 | + ``` |
| 179 | + To do so, you just need to indicate new `"type"` field value along with their Scala class/type. |
| 180 | + |
| 181 | + - You *need to change built-in naming convention*. |
| 182 | + Just instantiate `Decoder` class and override its `decode` method. |
| 183 | + ```scala |
| 184 | + implicit val decoder = new Decoder{ |
| 185 | + override def decode(jsonLogic: JsObject, jsonLogicData: JsObject): Any = |
| 186 | + val typeData = (jsonLogic \ "type").as[String] |
| 187 | + val pathData = (jsonLogic \ "var").as[String] |
| 188 | + val jsValue = (jsonLogicData \ pathData).get |
| 189 | + |
| 190 | + val value = typeData match { |
| 191 | + case "my_custom_byte_name" => jsValue.as[Byte] |
| 192 | + case "my_custom_int_name" => jsValue.as[Int] |
| 193 | + ... |
| 194 | + } |
| 195 | + ValueLogic("var", value) |
| 196 | + } |
| 197 | + ``` |
| 198 | + |
| 199 | + - Take note that you must provide Play JSON library a `Reads` typeclass to define how to read your specific type. |
| 200 | + For [more information on defining a `Reads` typeclass](https://github.com/playframework/play-json#reading-and-writing-objects). |
| 201 | + Fortunately, you usually don't need to implement a `Reads` typleclass directly. |
| 202 | + Play JSON comes equipped with some convenient macros to convert to and from case classes. |
| 203 | + In the following, you just need to define in *companion object* of `case class Car`: |
| 204 | + ```scala |
| 205 | + object Car { |
| 206 | + implicit val carReads: Reads[Car] = Json.reads[Car] |
| 207 | + } |
| 208 | + ``` |
| 209 | + |
| 210 | +### 4.2 Write json: Define Encoder |
| 211 | +A decoder defines how to write a JSON string/[JsValue](https://www.playframework.com/documentation/latest/api/scala/play/api/libs/json/JsValue.html) |
| 212 | +from a `JsonLogicCore` instance. |
| 213 | + |
| 214 | +## 5. Evaluating logical expression: reduce |
| 215 | +Evaluating a logical expression and getting its result if the main interest for most cases. |
| 216 | +Generally, logic/rules are received from another language/application and we want to apply this logic |
| 217 | +to our Scala program. |
| 218 | +Evaluating the logical expression is performed by applying a |
| 219 | +[reduce](https://en.wikipedia.org/wiki/Fold_(higher-order_function)) function to the boolean-algebra-tree. |
| 220 | + |
| 221 | +### `ReduceLogic` class |
| 222 | +Evaluating boolean-algebra-tree can be done by instantiating `ReduceLogic` class and applying `reduce` method on your `JsonLogicCore` instance. |
21 | 223 | ```scala |
22 | | -libraryDependencies ++= Seq( |
23 | | - // Other dependencies ... |
24 | | - "com.github.celadari" %% "JsonLogicScala" % "0.0.1" % "compile" |
25 | | -) |
| 224 | +val condition: JsonLogicCore = ... |
| 225 | +val reducer = new ReduceLogic |
| 226 | +val result = reducer.reduce(condition) |
26 | 227 | ``` |
27 | 228 |
|
28 | | -## Scala Versions |
| 229 | +The `reduce` method applies two sub-methods depending if the Node is an Internal Node or a Leaf Node. |
29 | 230 |
|
30 | | -This project is compiled, tested, and published for the following Scala versions: |
| 231 | +### 5.1 `reduceValueLogic` method |
| 232 | +- `def reduceValueLogic(condition: ValueLogic[_]): Any` |
| 233 | +- It is called by the `reduce` method on `ValueLogic` conditions. |
| 234 | +- Current built-in json-logic-scala implementation returns the Leaf Node `ValueLogic` instance's `value`. |
31 | 235 |
|
32 | | -1. 2.9.1 |
33 | | -2. 2.9.1-1 |
34 | | -3. 2.9.2 |
35 | | -4. 2.9.3 |
36 | | -5. 2.10.3 |
37 | | -6. 2.11.12 |
38 | | -7. 2.12.6 |
| 236 | +### 5.2 `reduceComposeLogic` method |
| 237 | +- `def reduceComposeLogic(condition: ComposeLogic): Any` |
| 238 | +- It is called by the `reduce` method on `ComposeLogic` conditions. |
| 239 | +- Defines for which `operator` string value, which Scala comparator function should be applied. |
| 240 | +- Comes with **built-in naming convention for operators** and **built-in Scala comparators function**. |
39 | 241 |
|
40 | 242 |
|
41 | | -## Usage |
| 243 | +*Json-logic-scala* comes with built-in comparators which are split into 3 different categories: |
| 244 | +`CompareOperator`, `ContainsOperator`, `BooleanOperator`. |
42 | 245 |
|
43 | | -To use **Json Logic Scala**, you should import it and call it... |
44 | 246 |
|
45 | | -## Scaladoc API |
| 247 | +There are several ways to define a **custom `reduceComposeLogic`** method: |
46 | 248 |
|
47 | | -The Scaladoc API for this project can be found [here](http://celadari.github.io/json-logic-scala/latest/api). |
| 249 | +#### 5.2.1 Define custom comparators methods |
| 250 | +Good option if you **need to add new types** but **don't need to change the comparator functions for built-in/basic types** and |
| 251 | +**don't need to change built-in naming convention for operators**. |
48 | 252 |
|
49 | | -## Examples |
| 253 | +*Json-logic-scala* provides comparator functions for basic types: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`. |
| 254 | +Those comparators are implicit parameters of `ReduceLogic` loaded at instantiation. |
50 | 255 |
|
| 256 | +If you need to define comparison behavior to compare a new type to other types you need to implement methods |
| 257 | +among the following: |
| 258 | + |
| 259 | +| package | Scala function | Behavior that it defines | |
| 260 | +|:------------: |:----------------------------------------------|:-----------------------------------------------------------:| |
| 261 | +| CompareOperator |`def negateCustom(value: Any): Any` |negate operator for custom types in CompareOperator package | |
| 262 | +| CompareOperator |`def cmpCustomLong(a: Long, b: Any): Any` | <= operator between `Long` and custom types | |
| 263 | +| CompareOperator |`def cmpCustomDouble(a: Double, b: Any): Any` | <= operator between `Double` and custom types | |
| 264 | +| CompareOperator |`def cmpCustom(a: Any, b: Any): Any` | <= operator between custom types themselves | |
| 265 | +| CompareOperator |`def eqCustomLong(a: Long, b: Any): Any` | = operator between `Long` and custom types | |
| 266 | +| CompareOperator |`def eqCustomDouble(a: Double, b: Any): Any` | <= operator between `Double` and custom types | |
| 267 | +| CompareOperator |`def eqCustom(a: Any, b: Any): Any` | = operator between custom types themselves | |
| 268 | +| ContainsOperator |`def containsCustom(a: Any, b: Any): Any` | contains operator between custom types themselves | |
| 269 | +| ContainsOperator |`def negateCustom(value: Any): Any` | negate operator for custom types in ContainsOperator package| |
| 270 | +| BooleanOperator |`def andCustom(a: Any, b: Any): Any` | and operator between custom types | |
| 271 | +| BooleanOperator |`def andCustomBoolean(a: Boolean, b: Any): Any`| and operator between `Boolean` type and custom types | |
| 272 | +| BooleanOperator |`def orCustom(a: Any, b: Any): Any` | or operator between custom types | |
| 273 | +| BooleanOperator |`def orCustomBoolean(a: Boolean, b: Any): Any` | or operator between `Boolean` type and custom types | |
| 274 | +| BooleanOperator |`def negateCustom(value: Any): Any` | negate operator for custom types in BooleanOperator package | |
| 275 | + |
| 276 | +#### 5.2.2 Redefine main comparators methods |
| 277 | +Good option if you **need to change the comparator functions for built-in/basic types** and |
| 278 | +**don't need to change built-in naming convention for operators**. |
| 279 | + |
| 280 | +You just need to override among the following comparator methods: |
| 281 | + |
| 282 | +| package | Scala function | Behavior that it defines | |
| 283 | +|:----------------:|:----------------------------------------------|:----------------------------------------------------------------------:| |
| 284 | +| CompareOperator |`def eq(a: Any, b: Any): Any` | = operator for all different types in CompareOperator package | |
| 285 | +| CompareOperator |`def negate(value: Any): Any` | ! operator for all different types in CompareOperator package | |
| 286 | +| CompareOperator |`def cmp(a: Any, b: Any): Any` | <= operator for all different types in CompareOperator package | |
| 287 | +| ContainsOperator |`def contains(a: Any, b: Any): Any` | contains operator for all different types in ContainsOperator package | |
| 288 | +| ContainsOperator |`def negate(value: Any): Any` | ! operator for all different types in ContainsOperator package | |
| 289 | +| BooleanOperator |`def and(a: Any, b: Any): Any` | ! operator for all different types in ContainsOperator package | |
| 290 | +| BooleanOperator |`def or(a: Any, b: Any): Any` | ! operator for all different types in ContainsOperator package | |
| 291 | +| BooleanOperator |`def negate(value: Any): Any` | ! operator for all different types in ContainsOperator package | |
| 292 | + |
| 293 | + |
| 294 | +#### 5.2.3 Redefine `reduceComposeLogic` method |
| 295 | +Good option if you **need to add new operators**. |
| 296 | + |
| 297 | +*Example*: let's imagine we just want to add the `"if"` condition. |
51 | 298 | ```scala |
52 | | -package org.example |
53 | 299 |
|
54 | | -import com.github.celadari.jsonlogicscala._ |
| 300 | +implicit val reducer = new ReduceLogic() { |
| 301 | + override def reduceComposeLogic(condition: ComposeLogic): Any = { |
| 302 | + condition.operator match { |
| 303 | + case "if" => ifCondition(condition.conditions) |
| 304 | + case other => super.reduceComposeLogic(condition) |
| 305 | + } |
| 306 | + } |
55 | 307 |
|
56 | | -case object MyObject { |
57 | | - // ... |
| 308 | + def ifCondition(conditions: Array[JsonLogicCore]): Any = { |
| 309 | + ... |
| 310 | + } |
58 | 311 | } |
59 | 312 | ``` |
60 | 313 |
|
61 | | -## Wishlist |
| 314 | +## 6. More examples |
| 315 | + |
| 316 | +[More detailed examples can found here](examples/) |
| 317 | + |
62 | 318 |
|
63 | | -Below is a list of features we would like to one day include in this project |
64 | 319 |
|
65 | | -1. Support more awesome. |
66 | | -2. Decimate the not-awesome. |
| 320 | +## Scaladoc API |
| 321 | + |
| 322 | +The Scaladoc API for this project can be found [here](http://celadari.github.io/json-logic-scala/latest/api). |
67 | 323 |
|
68 | 324 | ## License |
69 | 325 |
|
|
0 commit comments