Skip to content

Commit b0de6f4

Browse files
committed
add README.md documentation file
1 parent 0369741 commit b0de6f4

File tree

3 files changed

+291
-35
lines changed

3 files changed

+291
-35
lines changed

.img/boolean_logical_tree.png

64 KB
Loading

.img/logical_expression.png

9.51 KB
Loading

README.md

Lines changed: 291 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,325 @@
11
# Json Logic Scala
22
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.
35

4-
## Problem Statement
6+
Due to Scala's strong static typed language nature, json-logic-scala requires JSON to add tell type in json.
57

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.
712

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.
1015

11-
## Configuration
16+
### Scala Versions
1217

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
1422

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"}
17137
```
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.
18141

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**
20143

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.
21223
```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)
26227
```
27228

28-
## Scala Versions
229+
The `reduce` method applies two sub-methods depending if the Node is an Internal Node or a Leaf Node.
29230

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`.
31235

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**.
39241

40242

41-
## Usage
243+
*Json-logic-scala* comes with built-in comparators which are split into 3 different categories:
244+
`CompareOperator`, `ContainsOperator`, `BooleanOperator`.
42245

43-
To use **Json Logic Scala**, you should import it and call it...
44246

45-
## Scaladoc API
247+
There are several ways to define a **custom `reduceComposeLogic`** method:
46248

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**.
48252

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.
50255

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.
51298
```scala
52-
package org.example
53299

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+
}
55307

56-
case object MyObject {
57-
// ...
308+
def ifCondition(conditions: Array[JsonLogicCore]): Any = {
309+
...
310+
}
58311
}
59312
```
60313

61-
## Wishlist
314+
## 6. More examples
315+
316+
[More detailed examples can found here](examples/)
317+
62318

63-
Below is a list of features we would like to one day include in this project
64319

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).
67323

68324
## License
69325

0 commit comments

Comments
 (0)