Skip to content

Commit 0369741

Browse files
author
Charles LADARI
committed
Implement reduce function for JsonLogicCore
Reduce function returns the logical result out of the JsonLogic instance. Returned value is of type Any as not every result is of type Boolean. Example: Columns objects in spark.sql col("some_column") == 5.
1 parent c1b14a3 commit 0369741

File tree

10 files changed

+271
-154
lines changed

10 files changed

+271
-154
lines changed

build.sbt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ osgiSettings
6666

6767
OsgiKeys.bundleSymbolicName := "com.github.celadari.jsonlogicscala"
6868

69-
OsgiKeys.exportPackage := Seq("com.github.celadari.jsonlogicscala")
69+
OsgiKeys.exportPackage := Seq("com.github.celadari.jsonlogicscala",
70+
"com.github.celadari.jsonlogicscala.core",
71+
"com.github.celadari.jsonlogicscala.operators"
72+
)
7073

7174
OsgiKeys.privatePackage := Seq()
7275

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

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,30 @@ package com.github.celadari.jsonlogicscala.core
22

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

5-
object ComposeLogic {
6-
7-
val OPERATORS = Array("<=", "<", ">=", ">", "==", "!=", "or", "and", "xor")
85

9-
private[core] def parse[T](jsonLogic: JsObject, jsonLogicData: JsObject)(implicit fmt: Reads[T]): ComposeLogic = {
10-
// check for operator field
11-
val fields = jsonLogic.fields
6+
object ComposeLogic {
127

13-
// check for compose logic operator field
14-
if (fields.length > 1) throw new Error("JSON object is supposed to have only one operator field.")
15-
val operator = fields.head._1
16-
if (!OPERATORS.contains(operator)) throw new Error(s"Invalid parsed operator: $operator")
8+
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)
18+
}
1719

18-
// if operator is compose logic
19-
ComposeLogic(operator, readArrayOfConditions[T]((jsonLogic \ operator).get, jsonLogicData)(fmt))
20+
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)
2027
}
28+
val OPERATORS: Array[String] = BINARY_OPERATORS.ALL ++ MULTIPLE_OPERATORS.ALL
2129

2230
private[core] def decode(jsonLogic: JsObject, jsonLogicData: JsObject)(implicit decoder: Decoder): ComposeLogic = {
2331
// check for operator field
@@ -32,15 +40,6 @@ object ComposeLogic {
3240
ComposeLogic(operator, decodeArrayOfConditions((jsonLogic \ operator).get, jsonLogicData)(decoder))
3341
}
3442

35-
private[core] def readArrayOfConditions[T](json: JsValue, jsonLogicData: JsObject)(implicit fmt: Reads[T]): Array[JsonLogicCore] = {
36-
val jsArray = json.asInstanceOf[JsArray]
37-
jsArray
38-
.value
39-
.map(jsValue => {
40-
JsonLogicCore.parse[T](jsValue.asInstanceOf[JsObject], jsonLogicData)(fmt)
41-
})
42-
.toArray
43-
}
4443

4544
private[core] def decodeArrayOfConditions(json: JsValue, jsonLogicData: JsObject)(implicit decoder: Decoder): Array[JsonLogicCore] = {
4645
val jsArray = json.asInstanceOf[JsArray]
@@ -52,19 +51,6 @@ object ComposeLogic {
5251
.toArray
5352
}
5453

55-
56-
implicit def composeLogicReads[T](implicit fmt: Reads[T]): Reads[ComposeLogic] = new Reads[ComposeLogic] {
57-
58-
override def reads(json: JsValue): JsResult[ComposeLogic] = {
59-
// split json in two components jsonLogic and jsonLogicData
60-
val jsonLogic = (json \ 0).getOrElse(JsObject.empty).asInstanceOf[JsObject]
61-
val jsonLogicData = (json \ 1).getOrElse(JsObject.empty).asInstanceOf[JsObject]
62-
63-
// apply reading with distinguished components: logic and data
64-
JsSuccess(parse[T](jsonLogic, jsonLogicData)(fmt))
65-
}
66-
}
67-
6854
}
6955

70-
case class ComposeLogic(operator: String, conditions: Array[JsonLogicCore]) extends JsonLogicCore(operator)
56+
case class ComposeLogic(override val operator: String, conditions: Array[JsonLogicCore]) extends JsonLogicCore(operator)

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

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,10 @@
11
package com.github.celadari.jsonlogicscala.core
22

3+
import com.github.celadari.jsonlogicscala.operators.ReduceLogic
34
import play.api.libs.json._
45

56
object JsonLogicCore {
67

7-
private[core] def parse[T](jsonLogic: JsObject, jsonLogicData: JsObject)(implicit fmt: Reads[T]): JsonLogicCore = {
8-
// check for operator field
9-
val fields = jsonLogic.fields
10-
11-
// if operator is data access
12-
if (fields.map(_._1).contains("var")) return ValueLogic.parse[T](jsonLogic, jsonLogicData)(fmt)
13-
14-
// check for compose logic operator field
15-
if (fields.length > 1) throw new Error("JSON object is supposed to have only one operator field.")
16-
val operator = fields.head._1
17-
if (!ComposeLogic.OPERATORS.contains(operator)) throw new Error(s"Invalid parsed operator: $operator")
18-
19-
// if operator is compose logic
20-
ComposeLogic.parse[T](jsonLogic, jsonLogicData)(fmt)
21-
}
22-
238
private[core] def decode(jsonLogic: JsObject, jsonLogicData: JsObject)(implicit decoder: Decoder): JsonLogicCore = {
249
// check for operator field
2510
val fields = jsonLogic.fields
@@ -51,4 +36,9 @@ object JsonLogicCore {
5136
}
5237

5338

54-
abstract class JsonLogicCore(operator: String)
39+
abstract class JsonLogicCore(val operator: String) {
40+
41+
def reduce(implicit reducer: ReduceLogic): Any = {
42+
reducer.reduce(this)
43+
}
44+
}
Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
package com.github.celadari.jsonlogicscala.core
22

3+
34
import play.api.libs.json._
45

56
object ValueLogic {
67

7-
private[core] def parse[T](jsonLogic: JsObject, jsonLogicData: JsObject)(implicit fmt: Reads[T]): ValueLogic[T] = {
8-
val pathData = (jsonLogic \ "var").as[String]
9-
ValueLogic("var", (jsonLogicData \ pathData).as[T])
10-
}
11-
128
private[core] def decode(jsonLogic: JsObject, jsonLogicData: JsObject)(implicit decoder: Decoder): ValueLogic[_] = {
13-
ValueLogic("var", decoder.decode(jsonLogic, jsonLogicData))
9+
decoder.decode(jsonLogic, jsonLogicData)
1410
}
1511

16-
implicit def valueLogicReads[T](implicit fmt: Reads[T]): Reads[ValueLogic[T]] = new Reads[ValueLogic[T]] {
17-
override def reads(json: JsValue): JsResult[ValueLogic[T]] = {
18-
JsSuccess(ValueLogic("var", (json \ "var").as[T]))
19-
}
20-
}
2112
}
2213

23-
case class ValueLogic[T](operator: String, value: T) extends JsonLogicCore(operator)
14+
case class ValueLogic[T](override val operator: String, value: T) extends JsonLogicCore(operator)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.github.celadari.jsonlogicscala.operators
2+
3+
object BooleanOperator {
4+
implicit val booleanOperator: BooleanOperator = new BooleanOperator
5+
}
6+
7+
8+
class BooleanOperator {
9+
10+
// custom operators to be override by user
11+
def andCustom(a: Any, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: ${(a, b)}")
12+
def andCustomBoolean(a: Boolean, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: ${(a, b)}")
13+
def orCustom(a: Any, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: ${(a, b)}")
14+
def orCustomBoolean(a: Boolean, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: ${(a, b)}")
15+
def negateCustom(value: Any): Any = throw new IllegalArgumentException(s"Invalid argument: $value")
16+
17+
// operators for user
18+
def negate(value: Any): Any = {
19+
value match {
20+
case value: Boolean => !value
21+
case other => negateCustom(other)
22+
}
23+
}
24+
25+
def andBoolean(a: Boolean, b: Any): Any = {
26+
b match {
27+
case b: Boolean => a && b
28+
case other => andCustomBoolean(a, other)
29+
}
30+
}
31+
32+
def orBoolean(a: Boolean, b: Any): Any = {
33+
b match {
34+
case b: Boolean => a || b
35+
case other => orCustomBoolean(a, other)
36+
}
37+
}
38+
39+
def and(a: Any, b: Any): Any = {
40+
a match {
41+
case a: Boolean => andBoolean(a, b)
42+
case other => andCustom(other, b)
43+
}
44+
}
45+
46+
def or(a: Any, b: Any): Any = {
47+
a match {
48+
case a: Boolean => orBoolean(a, b)
49+
case other => orCustom(other, b)
50+
}
51+
}
52+
53+
def xor(a: Any, b: Any): Any = or(and(negate(a), b), and(a, negate(b)))
54+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.github.celadari.jsonlogicscala.operators
2+
3+
4+
object CompareOperator {
5+
implicit val cmpOperator: CompareOperator = new CompareOperator
6+
}
7+
8+
class CompareOperator {
9+
10+
// custom operators to be override by user
11+
def negateCustom(value: Any): Any = throw new IllegalArgumentException(s"Invalid argument: $value")
12+
def cmpCustomLong(a: Long, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: $b")
13+
def cmpCustomDouble(a: Double, b: Any) = throw new IllegalArgumentException(s"Invalid argument: $b")
14+
def cmpCustom(a: Any, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: ${(a, b)}")
15+
def eqCustomLong(a: Long, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: $b")
16+
def eqCustomDouble(a: Double, b: Any) = throw new IllegalArgumentException(s"Invalid argument: $b")
17+
def eqCustom(a: Any, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: ${(a, b)}")
18+
19+
// operators for user
20+
def lt(a: Any, b: Any): Any = cmp(a, b)
21+
def lteq(a: Any, b: Any): Any = negate(gt(a, b))
22+
def gteq(a: Any, b: Any): Any = negate(lt(a, b))
23+
def gt(a: Any, b: Any): Any = lt(b, a)
24+
25+
def diff(a: Any, b: Any): Any = negate(eq(a, b))
26+
27+
def eq(a: Any, b: Any): Any = {
28+
a match{
29+
case a: Byte => eqLong(a.asInstanceOf[Long], b)
30+
case a: Short => eqLong(a.asInstanceOf[Long], b)
31+
case a: Int => eqLong(a.asInstanceOf[Long], b)
32+
case a: Long => eqLong(a, b)
33+
case a: Float => eqDouble(a.asInstanceOf[Double], b)
34+
case a: Double => eqDouble(a, b)
35+
case other => eqCustom(other, b)
36+
}
37+
}
38+
39+
def negate(value: Any): Any = {
40+
value match {
41+
case value: Boolean => !value
42+
case other => negateCustom(other)
43+
}
44+
}
45+
46+
def eqLong(a: Long, b: Any): Any = {
47+
b match {
48+
case b: Byte => a == b
49+
case b: Short => a == b
50+
case b: Int => a == b
51+
case b: Long => a == b
52+
case b: Float => a == b
53+
case b: Double => a == b
54+
case other => eqCustomLong(a, other)
55+
}
56+
}
57+
58+
def eqDouble(a: Double, b: Any): Any = {
59+
b match {
60+
case b: Byte => a == b
61+
case b: Short => a == b
62+
case b: Int => a == b
63+
case b: Long => a == b
64+
case b: Float => a == b
65+
case b: Double => a == b
66+
case other => eqCustomDouble(a, other)
67+
}
68+
}
69+
70+
def cmpLong(a: Long, b: Any): Any = {
71+
b match {
72+
case b: Byte => a < b
73+
case b: Short => a < b
74+
case b: Int => a < b
75+
case b: Long => a < b
76+
case b: Float => a < b
77+
case b: Double => a < b
78+
case other => cmpCustomLong(a, other)
79+
}
80+
}
81+
82+
def cmpDouble(a: Double, b: Any): Any = {
83+
b match {
84+
case b: Byte => a < b
85+
case b: Short => a < b
86+
case b: Int => a < b
87+
case b: Long => a < b
88+
case b: Float => a < b
89+
case b: Double => a < b
90+
case other => cmpCustomDouble(a, other)
91+
}
92+
}
93+
94+
def cmp(a: Any, b: Any): Any = {
95+
a match {
96+
case a: Byte => cmpLong(a.asInstanceOf[Long], b)
97+
case a: Short => cmpLong(a.asInstanceOf[Long], b)
98+
case a: Int => cmpLong(a.asInstanceOf[Long], b)
99+
case a: Long => cmpLong(a, b)
100+
case a: Float => cmpDouble(a.asInstanceOf[Double], b)
101+
case a: Double => cmpDouble(a, b)
102+
case other => cmpCustom(other, b)
103+
}
104+
}
105+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.github.celadari.jsonlogicscala.operators
2+
3+
import scala.collection.mutable
4+
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
5+
6+
object ContainsOperator {
7+
implicit val containsOperator: ContainsOperator = new ContainsOperator
8+
}
9+
10+
class ContainsOperator {
11+
12+
// custom operators to be override by user
13+
def containsCustom(a: Any, b: Any): Any = throw new IllegalArgumentException(s"Invalid argument: ${(a, b)}")
14+
def negateCustom(value: Any): Any = throw new IllegalArgumentException(s"Invalid argument: $value")
15+
16+
// operators for user
17+
def containsNot(a: Any, b: Any): Any = negate(contains(a, b))
18+
19+
def negate(value: Any): Any = {
20+
value match {
21+
case value: Boolean => !value
22+
case other => negateCustom(other)
23+
}
24+
}
25+
26+
def contains(a: Any, b: Any): Any = {
27+
a match {
28+
case a: Array[_] => a.contains(b)
29+
case a: List[_] => a.contains(b)
30+
case a: ArrayBuffer[_] => a.contains(b)
31+
case a: mutable.Seq[_] => a.contains(b)
32+
case a: ListBuffer[_] => a.contains(b)
33+
case a: mutable.Stack[_] => a.contains(b)
34+
case a: mutable.Queue[_] => a.contains(b)
35+
case other => containsCustom(other, b)
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)