Skip to content

Commit 9e300a6

Browse files
committed
enable writing with encoder
1 parent 8d9e172 commit 9e300a6

File tree

6 files changed

+156
-20
lines changed

6 files changed

+156
-20
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,53 @@ to a `JsonLogicCore` instance.
212212
A decoder defines how to write a JSON string/[JsValue](https://www.playframework.com/documentation/latest/api/scala/play/api/libs/json/JsValue.html)
213213
from a `JsonLogicCore` instance.
214214

215+
* **`Encoder` class must be instantiated** if you need to stringify a Scala object.
216+
```scala
217+
implicit val encoder: Encoder = new Encoder // useless if only basic built-in types
218+
val json = Json.stringify(Json.toJson(json).as[JsonLogicCore])
219+
```
220+
*Json-logic-scala* comes with built-in `Encoder` for basic/built-in types.
221+
222+
* **Custom scala object/classes**:
223+
- You *don't need to change built-in naming convention*.
224+
Just instantiate `Encoder` class and define its `customValueAndType` method.
225+
```scala
226+
implicit val encoder = new Encoder{
227+
override def customValueAndType(value: Any): (String, JsValue) =
228+
value match {
229+
case value: Car => ("car", Json.toJson(value))
230+
case value: Plane => ("plane", Json.toJson(value))
231+
case _ => throw new IllegalArgumentException("Wrong argument.")
232+
}
233+
}
234+
```
235+
To do so, you just need to indicate new `"type"` field value along with their Scala class/type.
236+
You can also override `encode` method instead of `customValueAndType`.
237+
238+
- You *need to change built-in naming convention*.
239+
Just instantiate `Encoder` class and override its `getJsValueAndType` method.
240+
```scala
241+
implicit val encoder = new Encoder{
242+
override def getJsValueAndType(value: Any): (String, JsValue) = {
243+
value match {
244+
case value: String => ("my_custom_string", JsString(value))
245+
case value: MyCustomClass => ("my_custom_class", Json.toJson(value))
246+
...
247+
}
248+
}
249+
```
250+
251+
- Take note that you must provide Play JSON library a `Writes` typeclass to define how to write your specific type.
252+
For [more information on defining a `Writes` typeclass](https://github.com/playframework/play-json#reading-and-writing-objects).
253+
Fortunately, you usually don't need to implement a `Writes` typleclass directly.
254+
Play JSON comes equipped with some convenient macros to convert to and from case classes.
255+
In the following, you just need to define in *companion object* of `case class Car`:
256+
```scala
257+
object Car {
258+
implicit val carWrites: Writes[Car] = Json.writes[Car]
259+
}
260+
```
261+
215262
## 5. Evaluating logical expression: reduce
216263
Evaluating a logical expression and getting its result if the main interest for most cases.
217264
Generally, logic/rules are received from another language/application and we want to apply this logic

src/main/scala/com/github/celadari/jsonlogicscala/core/ComposeLogic.scala

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,27 @@ package com.github.celadari.jsonlogicscala.core
22

33
import play.api.libs.json._
44

5-
65
object ComposeLogic {
76

87
object BINARY_OPERATORS {
9-
val LTEQ = "<="
10-
val LT = "<"
11-
val GTEQ = ">="
12-
val GT = ">"
13-
val EQ = "=="
14-
val DIFF = "!="
15-
val IN = "in"
16-
val NOT_IN = "not in"
17-
val ALL = Array(LTEQ, LT, GTEQ, GT, EQ, DIFF)
8+
val LTEQ: String = "<="
9+
val LT: String = "<"
10+
val GTEQ: String = ">="
11+
val GT: String = ">"
12+
val EQ: String = "=="
13+
val DIFF: String = "!="
14+
val IN: String = "in"
15+
val NOT_IN: String = "not in"
16+
val ALL: Array[String] = Array(LTEQ, LT, GTEQ, GT, EQ, DIFF)
1817
}
1918

2019
object MULTIPLE_OPERATORS {
21-
val OR = "or"
22-
val AND = "and"
23-
val XOR = "xor"
24-
val MAX = "max"
25-
val MIN = "min"
26-
val ALL = Array(OR, AND, XOR, MAX, MIN)
20+
val OR: String = "or"
21+
val AND: String = "and"
22+
val XOR: String = "xor"
23+
val MAX: String = "max"
24+
val MIN: String = "min"
25+
val ALL: Array[String] = Array(OR, AND, XOR, MAX, MIN)
2726
}
2827
val OPERATORS: Array[String] = BINARY_OPERATORS.ALL ++ MULTIPLE_OPERATORS.ALL
2928

@@ -40,7 +39,6 @@ object ComposeLogic {
4039
ComposeLogic(operator, decodeArrayOfConditions((jsonLogic \ operator).get, jsonLogicData)(decoder))
4140
}
4241

43-
4442
private[core] def decodeArrayOfConditions(json: JsValue, jsonLogicData: JsObject)(implicit decoder: Decoder): Array[JsonLogicCore] = {
4543
val jsArray = json.asInstanceOf[JsArray]
4644
jsArray
@@ -51,6 +49,22 @@ object ComposeLogic {
5149
.toArray
5250
}
5351

52+
private[core] def encode(composeLogic: ComposeLogic)
53+
(implicit encoder: Encoder): (JsValue, JsObject) = {
54+
// retrieve compose logic attributes
55+
val operator = composeLogic.operator
56+
val conditions = composeLogic.conditions
57+
58+
// create js map operator -> conditions
59+
val (jsonLogic, jsonLogicData) = encodeArrayOfConditions(conditions)(encoder)
60+
(JsObject(Map(operator -> jsonLogic)), jsonLogicData)
61+
}
62+
63+
private[core] def encodeArrayOfConditions(conditions: Array[JsonLogicCore])(implicit encoder: Encoder): (JsValue, JsObject) = {
64+
val (jsonLogics, jsonLogicData) = conditions.map(jsonLogic => JsonLogicCore.encode(jsonLogic)(encoder)).unzip
65+
(JsArray(jsonLogics), jsonLogicData.reduce(_ ++ _))
66+
}
67+
5468
}
5569

5670
case class ComposeLogic(override val operator: String, conditions: Array[JsonLogicCore]) extends JsonLogicCore(operator)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.github.celadari.jsonlogicscala.core
2+
3+
import play.api.libs.json._
4+
5+
object Encoder {
6+
implicit val defaultEncoder: Encoder = new Encoder {
7+
override def customValueAndType(value: Any): (String, JsValue) =
8+
throw new NotImplementedError("You must provide a decoding method for this object.")
9+
}
10+
}
11+
12+
abstract class Encoder {
13+
def customValueAndType(value: Any): (String, JsValue)
14+
15+
def getJsValueAndType(value: Any): (String, JsValue) = {
16+
value match {
17+
case value: String => ("string", JsString(value))
18+
case value: Byte => ("byte", JsNumber(value.toInt))
19+
case value: Short => ("short", JsNumber(value.toInt))
20+
case value: Int => ("int", JsNumber(value))
21+
case value: Long => ("long", JsNumber(value))
22+
case value: Float => ("float", JsNumber(value.toDouble))
23+
case value: Double => ("double", JsNumber(value))
24+
case value: Boolean => ("boolean", JsBoolean(value))
25+
case arr: Array[_] => {
26+
if (arr.isEmpty) ("array[]", JsArray.empty)
27+
else {
28+
val (typesArr, jsArrs) = arr.map(getJsValueAndType).unzip
29+
(s"array[${typesArr.head}]", JsArray(jsArrs))
30+
}
31+
}
32+
case otherType => customValueAndType(otherType)
33+
}
34+
}
35+
36+
def encode(valueLogic: ValueLogic[_]): (String, String, JsValue) = {
37+
val codenameData = valueLogic.codename
38+
39+
val (typeData, jsValue) = getJsValueAndType(valueLogic.value)
40+
(typeData, codenameData, jsValue)
41+
}
42+
}

src/main/scala/com/github/celadari/jsonlogicscala/core/JsonLogicCore.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ object JsonLogicCore {
2121
ComposeLogic.decode(jsonLogic, jsonLogicData)(decoder)
2222
}
2323

24+
private[core] def encode(jsonLogic: JsonLogicCore)(implicit encoder: Encoder): (JsValue, JsObject) = {
25+
// if operator is data access
26+
jsonLogic match {
27+
case valueLogic: ValueLogic[_] => ValueLogic.encode(valueLogic)(encoder)
28+
case composeLogic: ComposeLogic => ComposeLogic.encode(composeLogic)(encoder)
29+
}
30+
}
31+
2432
implicit def jsonLogicCoreReads(implicit decoder: Decoder): Reads[JsonLogicCore] = new Reads[JsonLogicCore] {
2533

2634
override def reads(json: JsValue): JsResult[JsonLogicCore] = {
@@ -33,6 +41,17 @@ object JsonLogicCore {
3341
JsSuccess(decode(jsonLogic, jsonLogicData)(decoder))
3442
}
3543
}
44+
45+
implicit def jsonLogicCoreWrites(implicit encoder: Encoder): Writes[JsonLogicCore] = new Writes[JsonLogicCore] {
46+
47+
override def writes(jsonLogicCore: JsonLogicCore): JsValue = {
48+
// apply writing
49+
val (jsonLogic, jsonLogicData) = encode(jsonLogicCore)(encoder)
50+
51+
// return final result
52+
JsArray(Array(jsonLogic, jsonLogicData))
53+
}
54+
}
3655
}
3756

3857

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.github.celadari.jsonlogicscala.core
22

3-
3+
import java.util.UUID.randomUUID
44
import play.api.libs.json._
55

66
object ValueLogic {
@@ -9,6 +9,20 @@ object ValueLogic {
99
decoder.decode(jsonLogic, jsonLogicData)
1010
}
1111

12+
private[core] def encode(valueLogic: ValueLogic[_])(implicit encoder: Encoder): (JsValue, JsObject) = {
13+
// retrieve valueLogic information
14+
val (typeData, codenameData, jsonData) = encoder.encode(valueLogic)
15+
16+
// construct jsonLogic component and jsonLogicData component
17+
val jsonLogic = JsObject(Map("var" -> JsString(codenameData), "type" -> JsString(typeData)))
18+
val jsonLogicData = JsObject(Map(codenameData -> jsonData))
19+
(jsonLogic, jsonLogicData)
20+
}
21+
1222
}
1323

14-
case class ValueLogic[T](override val operator: String, value: T) extends JsonLogicCore(operator)
24+
case class ValueLogic[T](
25+
override val operator: String,
26+
value: T,
27+
codename: String = randomUUID.toString
28+
) extends JsonLogicCore(operator)

src/main/scala/com/github/celadari/jsonlogicscala/operators/ContainsOperator.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class ContainsOperator {
2525

2626
def contains(a: Any, b: Any): Any = {
2727
a match {
28-
case a: Array[_] => a.contains(b)
28+
case a: Array[Any] => a.contains(b)
2929
case a: List[_] => a.contains(b)
3030
case a: ArrayBuffer[_] => a.contains(b)
3131
case a: mutable.Seq[_] => a.contains(b)

0 commit comments

Comments
 (0)