Adding generalized JSON comparisons.
This commit is contained in:
parent
70976a2610
commit
d0d2925ee4
9 changed files with 893 additions and 6 deletions
313
src/main/scala/gs/predicate/v0/json/JsonComparison.scala
Normal file
313
src/main/scala/gs/predicate/v0/json/JsonComparison.scala
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
package gs.predicate.v0.json
|
||||
|
||||
import gs.predicate.v0.serde.json.JsonKeys
|
||||
import io.circe.Encoder
|
||||
import io.circe.Json
|
||||
import java.time.LocalDate
|
||||
import scala.util.Try
|
||||
|
||||
/** Serializable comparisons against single JSON objects.
|
||||
*
|
||||
* @param name
|
||||
* The name of the comparison - used for serialization.
|
||||
*/
|
||||
abstract class JsonComparison(val name: String):
|
||||
def compare(input: Json): Boolean
|
||||
|
||||
object JsonComparison:
|
||||
|
||||
case class Eq(target: Json) extends JsonComparison(Eq.Name):
|
||||
def compare(input: Json): Boolean = target.equals(input)
|
||||
|
||||
object Eq:
|
||||
final val Name: String = "="
|
||||
|
||||
given Encoder[Eq] = Encoder.instance[Eq] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> jc.target
|
||||
)
|
||||
}
|
||||
|
||||
case class Neq(target: Json) extends JsonComparison(Neq.Name):
|
||||
def compare(input: Json): Boolean = !target.equals(input)
|
||||
|
||||
object Neq:
|
||||
final val Name: String = "!="
|
||||
|
||||
given Encoder[Neq] = Encoder.instance[Neq] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> jc.target
|
||||
)
|
||||
}
|
||||
|
||||
case class StringContains(target: String)
|
||||
extends JsonComparison(StringContains.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString.exists(_.contains(target))
|
||||
|
||||
object StringContains:
|
||||
final val Name: String = "contains"
|
||||
|
||||
given Encoder[StringContains] = Encoder.instance[StringContains] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromString(jc.target)
|
||||
)
|
||||
}
|
||||
|
||||
case class StringPrefix(target: String)
|
||||
extends JsonComparison(StringPrefix.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString.exists(_.startsWith(target))
|
||||
|
||||
object StringPrefix:
|
||||
final val Name: String = "prefix"
|
||||
|
||||
given Encoder[StringPrefix] = Encoder.instance[StringPrefix] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromString(jc.target)
|
||||
)
|
||||
}
|
||||
|
||||
case class StringSuffix(target: String)
|
||||
extends JsonComparison(StringSuffix.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString.exists(_.endsWith(target))
|
||||
|
||||
object StringSuffix:
|
||||
final val Name: String = "suffix"
|
||||
|
||||
given Encoder[StringSuffix] = Encoder.instance[StringSuffix] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromString(jc.target)
|
||||
)
|
||||
}
|
||||
|
||||
case class IntLessThan(target: Int) extends JsonComparison(IntLessThan.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asNumber.flatMap(_.toInt).exists(_ < target)
|
||||
|
||||
object IntLessThan:
|
||||
final val Name: String = "<"
|
||||
|
||||
given Encoder[IntLessThan] = Encoder.instance[IntLessThan] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromInt(jc.target)
|
||||
)
|
||||
}
|
||||
|
||||
case class IntLessThanOrEqualTo(target: Int)
|
||||
extends JsonComparison(IntLessThanOrEqualTo.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asNumber.flatMap(_.toInt).exists(_ <= target)
|
||||
|
||||
object IntLessThanOrEqualTo:
|
||||
final val Name: String = "<="
|
||||
|
||||
given Encoder[IntLessThanOrEqualTo] =
|
||||
Encoder.instance[IntLessThanOrEqualTo] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromInt(jc.target)
|
||||
)
|
||||
}
|
||||
|
||||
case class IntGreaterThan(target: Int)
|
||||
extends JsonComparison(IntGreaterThan.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asNumber.flatMap(_.toInt).exists(_ > target)
|
||||
|
||||
object IntGreaterThan:
|
||||
final val Name: String = ">"
|
||||
|
||||
given Encoder[IntGreaterThan] = Encoder.instance[IntGreaterThan] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromInt(jc.target)
|
||||
)
|
||||
}
|
||||
|
||||
case class IntGreaterThanOrEqualTo(target: Int)
|
||||
extends JsonComparison(IntGreaterThanOrEqualTo.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asNumber.flatMap(_.toInt).exists(_ >= target)
|
||||
|
||||
object IntGreaterThanOrEqualTo:
|
||||
final val Name: String = ">="
|
||||
|
||||
given Encoder[IntGreaterThanOrEqualTo] =
|
||||
Encoder.instance[IntGreaterThanOrEqualTo] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromInt(jc.target)
|
||||
)
|
||||
}
|
||||
|
||||
case class IntBetweenInclusive(
|
||||
lower: Int,
|
||||
upper: Int
|
||||
) extends JsonComparison(IntBetweenInclusive.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asNumber
|
||||
.flatMap(_.toInt)
|
||||
.exists(value => value >= lower && value <= upper)
|
||||
|
||||
object IntBetweenInclusive:
|
||||
final val Name: String = "[]"
|
||||
|
||||
given Encoder[IntBetweenInclusive] =
|
||||
Encoder.instance[IntBetweenInclusive] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.lower -> Json.fromInt(jc.lower),
|
||||
JsonKeys.upper -> Json.fromInt(jc.upper)
|
||||
)
|
||||
}
|
||||
|
||||
case class IntBetweenExclusive(
|
||||
lower: Int,
|
||||
upper: Int
|
||||
) extends JsonComparison(IntBetweenExclusive.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asNumber
|
||||
.flatMap(_.toInt)
|
||||
.exists(value => value > lower && value < upper)
|
||||
|
||||
object IntBetweenExclusive:
|
||||
final val Name: String = "()"
|
||||
|
||||
given Encoder[IntBetweenExclusive] =
|
||||
Encoder.instance[IntBetweenExclusive] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.lower -> Json.fromInt(jc.lower),
|
||||
JsonKeys.upper -> Json.fromInt(jc.upper)
|
||||
)
|
||||
}
|
||||
|
||||
case class DateEq(target: LocalDate) extends JsonComparison(DateEq.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString.flatMap(asDate).exists(_.isEqual(target))
|
||||
|
||||
object DateEq:
|
||||
final val Name: String = "date="
|
||||
|
||||
given Encoder[DateEq] = Encoder.instance[DateEq] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromString(jc.target.toString())
|
||||
)
|
||||
}
|
||||
|
||||
case class DateBefore(target: LocalDate)
|
||||
extends JsonComparison(DateBefore.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString.flatMap(asDate).exists(_.isBefore(target))
|
||||
|
||||
object DateBefore:
|
||||
final val Name: String = "date<"
|
||||
|
||||
given Encoder[DateBefore] = Encoder.instance[DateBefore] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromString(jc.target.toString())
|
||||
)
|
||||
}
|
||||
|
||||
case class DateAfter(target: LocalDate)
|
||||
extends JsonComparison(DateAfter.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString.flatMap(asDate).exists(_.isAfter(target))
|
||||
|
||||
object DateAfter:
|
||||
final val Name: String = "date>"
|
||||
|
||||
given Encoder[DateAfter] = Encoder.instance[DateAfter] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.value -> Json.fromString(jc.target.toString())
|
||||
)
|
||||
}
|
||||
|
||||
case class DateBetweenInclusive(
|
||||
lower: LocalDate,
|
||||
upper: LocalDate
|
||||
) extends JsonComparison(DateBetweenInclusive.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString
|
||||
.flatMap(asDate)
|
||||
.exists(value =>
|
||||
dateLessThanOrEqualTo(value, upper) && dateGreaterThanOrEqualTo(
|
||||
value,
|
||||
lower
|
||||
)
|
||||
)
|
||||
|
||||
object DateBetweenInclusive:
|
||||
final val Name: String = "date[]"
|
||||
|
||||
given Encoder[DateBetweenInclusive] =
|
||||
Encoder.instance[DateBetweenInclusive] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.lower -> Json.fromString(jc.lower.toString()),
|
||||
JsonKeys.upper -> Json.fromString(jc.upper.toString())
|
||||
)
|
||||
}
|
||||
|
||||
case class DateBetweenExclusive(
|
||||
lower: LocalDate,
|
||||
upper: LocalDate
|
||||
) extends JsonComparison(DateBetweenExclusive.Name):
|
||||
|
||||
def compare(input: Json): Boolean =
|
||||
input.asString
|
||||
.flatMap(asDate)
|
||||
.exists(value => value.isAfter(lower) && value.isBefore(upper))
|
||||
|
||||
object DateBetweenExclusive:
|
||||
final val Name: String = "date()"
|
||||
|
||||
given Encoder[DateBetweenExclusive] =
|
||||
Encoder.instance[DateBetweenExclusive] { jc =>
|
||||
Json.obj(
|
||||
JsonKeys.name -> Json.fromString(Name),
|
||||
JsonKeys.lower -> Json.fromString(jc.lower.toString()),
|
||||
JsonKeys.upper -> Json.fromString(jc.upper.toString())
|
||||
)
|
||||
}
|
||||
|
||||
private def asDate(input: String): Option[LocalDate] =
|
||||
Try(LocalDate.parse(input)).toOption
|
||||
|
||||
private def dateLessThanOrEqualTo(
|
||||
input: LocalDate,
|
||||
target: LocalDate
|
||||
): Boolean =
|
||||
input.isBefore(target) || input.isEqual(target)
|
||||
|
||||
private def dateGreaterThanOrEqualTo(
|
||||
input: LocalDate,
|
||||
target: LocalDate
|
||||
): Boolean =
|
||||
input.isAfter(target) || input.isEqual(target)
|
||||
|
||||
end JsonComparison
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package gs.predicate.v0.json
|
||||
|
||||
import cats.Applicative
|
||||
import cats.syntax.all.*
|
||||
import gs.predicate.v0.api.Predicate
|
||||
import gs.predicate.v0.json.query.JsonQuery
|
||||
|
||||
/** Predicate that matches if the JSON provider contains a JSON blob with the
|
||||
* given key, and that blob contains the given query, and the result of the
|
||||
* query matches the given comparison function.
|
||||
*
|
||||
* This is the most general JSON predicate.
|
||||
*
|
||||
* @param key
|
||||
* The name of the JSON value that must satisfy the given query.
|
||||
* @param query
|
||||
* The [[JsonQuery]] that must be satisfied.
|
||||
* @param comparison
|
||||
* The JSON comparison that must match the query.
|
||||
*/
|
||||
final class JsonQueryComparison[F[_]: Applicative: JsonProvider](
|
||||
val key: String,
|
||||
val query: JsonQuery,
|
||||
val comparison: JsonComparison
|
||||
) extends Predicate[F]:
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
final override val predicateType: String = JsonQueryComparison.PredicateType
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def eval(): F[Predicate.Result] =
|
||||
JsonProvider[F].get(key).map {
|
||||
case None => Predicate.Result.missed()
|
||||
case Some(json) => Predicate.Result(query.eval(json, comparison.compare))
|
||||
}
|
||||
|
||||
object JsonQueryComparison:
|
||||
|
||||
final val PredicateType: String = "json-query-comparison"
|
||||
|
||||
def apply[F[_]: Applicative: JsonProvider](
|
||||
key: String,
|
||||
query: JsonQuery,
|
||||
comparison: JsonComparison
|
||||
): JsonQueryComparison[F] =
|
||||
new JsonQueryComparison[F](key, query, comparison)
|
||||
|
||||
end JsonQueryComparison
|
||||
50
src/main/scala/gs/predicate/v0/json/JsonQueryNotEquals.scala
Normal file
50
src/main/scala/gs/predicate/v0/json/JsonQueryNotEquals.scala
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package gs.predicate.v0.json
|
||||
|
||||
import cats.Applicative
|
||||
import cats.syntax.all.*
|
||||
import gs.predicate.v0.api.Predicate
|
||||
import gs.predicate.v0.json.query.JsonQuery
|
||||
import io.circe.Json
|
||||
|
||||
/** Predicate that matches if the JSON provider contains a JSON blob with the
|
||||
* given key, and that blob contains the given query, and the result of the
|
||||
* query matches the given value.
|
||||
*
|
||||
* @param key
|
||||
* The name of the JSON value that must satisfy the given query.
|
||||
* @param query
|
||||
* The [[JsonQuery]] that must be satisfied.
|
||||
* @param value
|
||||
* The JSON value that must match the query.
|
||||
*/
|
||||
final class JsonQueryNotEquals[F[_]: Applicative: JsonProvider](
|
||||
val key: String,
|
||||
val query: JsonQuery,
|
||||
val value: Json
|
||||
) extends Predicate[F]:
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
final override val predicateType: String = JsonQueryNotEquals.PredicateType
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def eval(): F[Predicate.Result] =
|
||||
JsonProvider[F].get(key).map {
|
||||
case None => Predicate.Result.missed()
|
||||
case Some(json) =>
|
||||
Predicate.Result(query.eval(json, v => !v.equals(value)))
|
||||
}
|
||||
|
||||
object JsonQueryNotEquals:
|
||||
|
||||
final val PredicateType: String = "json-query-not-equals"
|
||||
|
||||
def apply[F[_]: Applicative: JsonProvider](
|
||||
key: String,
|
||||
query: JsonQuery,
|
||||
value: Json
|
||||
): JsonQueryNotEquals[F] =
|
||||
new JsonQueryNotEquals[F](key, query, value)
|
||||
|
||||
end JsonQueryNotEquals
|
||||
|
|
@ -151,8 +151,12 @@ final class CompiledQuery private (
|
|||
else
|
||||
ps match
|
||||
case Nil =>
|
||||
// If we are at the end of some query, we reason about the selected
|
||||
// value. Note that this could be ANY JSON value. If the key is an
|
||||
// array, for example, this will get the entire array.
|
||||
path.json.getOption(json).map(p).getOrElse(false)
|
||||
case Single(key) :: rest =>
|
||||
// Selecting a key could lead to `Nil` (evaluate the value at the key)
|
||||
evalRec(rest, json, path.selectDynamic(key), p, depth + 1)
|
||||
case ArrayAny(key) :: rest =>
|
||||
path
|
||||
|
|
|
|||
|
|
@ -24,4 +24,16 @@ object JsonKeys:
|
|||
*/
|
||||
val values: String = "values"
|
||||
|
||||
/** Captures the name of comparison operations.
|
||||
*/
|
||||
val name: String = "name"
|
||||
|
||||
/** Lower bound of some range.
|
||||
*/
|
||||
val lower: String = "lower"
|
||||
|
||||
/** Upper bound of some range.
|
||||
*/
|
||||
val upper: String = "upper"
|
||||
|
||||
end JsonKeys
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import gs.predicate.v0.kv.KeyValueProvider
|
|||
import gs.predicate.v0.kv.ValueContains
|
||||
import gs.predicate.v0.kv.ValueEndsWith
|
||||
import gs.predicate.v0.kv.ValueEquals
|
||||
import gs.predicate.v0.kv.ValueNotEquals
|
||||
import gs.predicate.v0.kv.ValueStartsWith
|
||||
import io.circe.*
|
||||
import io.circe.syntax.*
|
||||
|
|
@ -100,12 +101,14 @@ def decodePredicate[F[_]: Applicative: KeyValueProvider: JsonProvider](
|
|||
for
|
||||
predicateType <- cursor.downField(JsonKeys.predicateType).as[String]
|
||||
predicate <- predicateType match
|
||||
case True.PredicateType => Right(True[F])
|
||||
case False.PredicateType => Right(False[F])
|
||||
case And.PredicateType => decodeAnd[F](cursor)
|
||||
case Or.PredicateType => decodeOr[F](cursor)
|
||||
case KeyExists.PredicateType => Decoder[KeyExists[F]].apply(cursor)
|
||||
case ValueEquals.PredicateType => Decoder[ValueEquals[F]].apply(cursor)
|
||||
case True.PredicateType => Right(True[F])
|
||||
case False.PredicateType => Right(False[F])
|
||||
case And.PredicateType => decodeAnd[F](cursor)
|
||||
case Or.PredicateType => decodeOr[F](cursor)
|
||||
case KeyExists.PredicateType => Decoder[KeyExists[F]].apply(cursor)
|
||||
case ValueEquals.PredicateType => Decoder[ValueEquals[F]].apply(cursor)
|
||||
case ValueNotEquals.PredicateType =>
|
||||
Decoder[ValueNotEquals[F]].apply(cursor)
|
||||
case ValueContains.PredicateType =>
|
||||
Decoder[ValueContains[F]].apply(cursor)
|
||||
case ValueStartsWith.PredicateType =>
|
||||
|
|
|
|||
210
src/test/scala/gs/predicate/v0/json/JsonComparisonTests.scala
Normal file
210
src/test/scala/gs/predicate/v0/json/JsonComparisonTests.scala
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package gs.predicate.v0.json
|
||||
|
||||
import gs.datagen.v0.Gen
|
||||
import gs.datagen.v0.generators.MinMax
|
||||
import gs.datagen.v0.generators.Size
|
||||
import io.circe.Json
|
||||
import java.time.LocalDate
|
||||
import munit.FunSuite
|
||||
|
||||
class JsonComparisonTests extends FunSuite:
|
||||
import JsonComparisonTests.*
|
||||
|
||||
test("should support Eq") {
|
||||
val key = keyGen.gen()
|
||||
val value = jsonStringGen.gen()
|
||||
val json = Json.obj(key -> value)
|
||||
val other = Json.obj("x" -> value)
|
||||
val jc = JsonComparison.Eq(json)
|
||||
assert(jc.compare(json))
|
||||
assert(!jc.compare(other))
|
||||
}
|
||||
|
||||
test("should support Neq") {
|
||||
val key = keyGen.gen()
|
||||
val value = jsonStringGen.gen()
|
||||
val json = Json.obj(key -> value)
|
||||
val other = Json.obj("x" -> value)
|
||||
val jc = JsonComparison.Neq(json)
|
||||
assert(!jc.compare(json))
|
||||
assert(jc.compare(other))
|
||||
}
|
||||
|
||||
test("should support StringContains") {
|
||||
val str = strGen.gen()
|
||||
val substr = str.substring(3, 7)
|
||||
val json = Json.fromString(str)
|
||||
val jc1 = JsonComparison.StringContains(substr)
|
||||
val jc2 = JsonComparison.StringContains(str)
|
||||
val jc3 = JsonComparison.StringContains(longStrGen.gen())
|
||||
assert(jc1.compare(json))
|
||||
assert(jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support StringPrefix") {
|
||||
val str = strGen.gen()
|
||||
val substr = str.take(4)
|
||||
val json = Json.fromString(str)
|
||||
val jc1 = JsonComparison.StringPrefix(substr)
|
||||
val jc2 = JsonComparison.StringPrefix(str)
|
||||
val jc3 = JsonComparison.StringPrefix(longStrGen.gen())
|
||||
assert(jc1.compare(json))
|
||||
assert(jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support StringSuffix") {
|
||||
val str = strGen.gen()
|
||||
val substr = str.takeRight(4)
|
||||
val json = Json.fromString(str)
|
||||
val jc1 = JsonComparison.StringSuffix(substr)
|
||||
val jc2 = JsonComparison.StringSuffix(str)
|
||||
val jc3 = JsonComparison.StringSuffix(longStrGen.gen())
|
||||
assert(jc1.compare(json))
|
||||
assert(jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support IntLessThan") {
|
||||
val int = intGen.gen()
|
||||
val target = 101
|
||||
val json = Json.fromInt(int)
|
||||
val jc1 = JsonComparison.IntLessThan(target)
|
||||
val jc3 = JsonComparison.IntLessThan(-1)
|
||||
assert(jc1.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support IntLessThanOrEqualTo") {
|
||||
val int = intGen.gen()
|
||||
val target = 101
|
||||
val json = Json.fromInt(int)
|
||||
val jc1 = JsonComparison.IntLessThanOrEqualTo(target)
|
||||
val jc2 = JsonComparison.IntLessThanOrEqualTo(int)
|
||||
val jc3 = JsonComparison.IntLessThan(-1)
|
||||
assert(jc1.compare(json))
|
||||
assert(jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support IntGreaterThan") {
|
||||
val int = intGen.gen()
|
||||
val target = -1
|
||||
val json = Json.fromInt(int)
|
||||
val jc1 = JsonComparison.IntGreaterThan(target)
|
||||
val jc3 = JsonComparison.IntGreaterThan(101)
|
||||
assert(jc1.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support IntGreaterThanOrEqualTo") {
|
||||
val int = intGen.gen()
|
||||
val target = -1
|
||||
val json = Json.fromInt(int)
|
||||
val jc1 = JsonComparison.IntGreaterThanOrEqualTo(target)
|
||||
val jc2 = JsonComparison.IntGreaterThanOrEqualTo(int)
|
||||
val jc3 = JsonComparison.IntGreaterThan(101)
|
||||
assert(jc1.compare(json))
|
||||
assert(jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support IntBetweenInclusive") {
|
||||
val int = intGen.gen()
|
||||
val lower = 0
|
||||
val upper = 100
|
||||
val json = Json.fromInt(int)
|
||||
val jc1 = JsonComparison.IntBetweenInclusive(lower, upper)
|
||||
val jc2 = JsonComparison.IntBetweenInclusive(int, int)
|
||||
val jc3 = JsonComparison.IntBetweenInclusive(1000, 10000)
|
||||
assert(jc1.compare(json))
|
||||
assert(jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support IntBetweenExclusive") {
|
||||
val int = intGen.gen()
|
||||
val lower = -1
|
||||
val upper = 101
|
||||
val json = Json.fromInt(int)
|
||||
val jc1 = JsonComparison.IntBetweenExclusive(lower, upper)
|
||||
val jc2 = JsonComparison.IntBetweenExclusive(int, int)
|
||||
val jc3 = JsonComparison.IntBetweenExclusive(1000, 10000)
|
||||
assert(jc1.compare(json))
|
||||
assert(!jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support DateEq") {
|
||||
val date = dateGen.gen()
|
||||
val target = date
|
||||
val json = Json.fromString(date.toString())
|
||||
val jc1 = JsonComparison.DateEq(target)
|
||||
val jc2 = JsonComparison.DateEq(target.plusDays(1L))
|
||||
assert(jc1.compare(json))
|
||||
assert(!jc2.compare(json))
|
||||
}
|
||||
|
||||
test("should support DateBefore") {
|
||||
val date = dateGen.gen()
|
||||
val target = date.plusDays(1L)
|
||||
val json = Json.fromString(date.toString())
|
||||
val jc1 = JsonComparison.DateBefore(target)
|
||||
val jc2 = JsonComparison.DateBefore(date)
|
||||
assert(jc1.compare(json))
|
||||
assert(!jc2.compare(json))
|
||||
}
|
||||
|
||||
test("should support DateAfter") {
|
||||
val date = dateGen.gen()
|
||||
val target = date.minusDays(1L)
|
||||
val json = Json.fromString(date.toString())
|
||||
val jc1 = JsonComparison.DateAfter(target)
|
||||
val jc2 = JsonComparison.DateAfter(date)
|
||||
assert(jc1.compare(json))
|
||||
assert(!jc2.compare(json))
|
||||
}
|
||||
|
||||
test("should support DateBetweenInclusive") {
|
||||
val date = dateGen.gen()
|
||||
val lower = date.minusDays(1L)
|
||||
val upper = date.plusDays(1L)
|
||||
val json = Json.fromString(date.toString())
|
||||
val jc1 = JsonComparison.DateBetweenInclusive(lower, upper)
|
||||
val jc2 = JsonComparison.DateBetweenInclusive(date, date)
|
||||
val jc3 = JsonComparison.DateBetweenInclusive(
|
||||
date.plusDays(1L),
|
||||
date.plusDays(1000L)
|
||||
)
|
||||
assert(jc1.compare(json))
|
||||
assert(jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
test("should support DateBetweenExclusive") {
|
||||
val date = dateGen.gen()
|
||||
val lower = date.minusDays(1L)
|
||||
val upper = date.plusDays(1L)
|
||||
val json = Json.fromString(date.toString())
|
||||
val jc1 = JsonComparison.DateBetweenExclusive(lower, upper)
|
||||
val jc2 = JsonComparison.DateBetweenExclusive(date, date)
|
||||
val jc3 = JsonComparison.DateBetweenExclusive(
|
||||
date.plusDays(1L),
|
||||
date.plusDays(1000L)
|
||||
)
|
||||
assert(jc1.compare(json))
|
||||
assert(!jc2.compare(json))
|
||||
assert(!jc3.compare(json))
|
||||
}
|
||||
|
||||
object JsonComparisonTests:
|
||||
|
||||
val keyGen: Gen[String] = Gen.string.alphaNumeric(Size.between(8, 16))
|
||||
val strGen: Gen[String] = Gen.string.alphaNumeric(Size.between(8, 16))
|
||||
val longStrGen: Gen[String] = Gen.string.alphaNumeric(Size.fixed(32))
|
||||
val jsonStringGen: Gen[Json] = strGen.map(Json.fromString)
|
||||
val intGen: Gen[Int] = Gen.integer.inRange(0, 100)
|
||||
val dateGen: Gen[LocalDate] = Gen.date.aroundToday(MinMax(1, 3))
|
||||
|
||||
end JsonComparisonTests
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package gs.predicate.v0.json
|
||||
|
||||
import cats.effect.IO
|
||||
import cats.effect.std.MapRef
|
||||
import gs.datagen.v0.Gen
|
||||
import gs.datagen.v0.generators.Size
|
||||
import gs.predicate.v0.api.Predicate
|
||||
import gs.predicate.v0.json.query.JsonQuery
|
||||
import io.circe.Json
|
||||
import support.IOSuite
|
||||
|
||||
class JsonQueryExistsTests extends IOSuite:
|
||||
|
||||
import JsonQueryExistsTests.Data
|
||||
import JsonQueryExistsTests.newProvider
|
||||
|
||||
iotest("should find a match for an existing key") {
|
||||
val key = Data.keyGen.gen()
|
||||
val value = Data.strValGen.gen()
|
||||
val query = key
|
||||
val blob = Json.obj(
|
||||
key -> value
|
||||
)
|
||||
|
||||
newProvider(Map(key -> blob)).flatMap { provider =>
|
||||
given JsonProvider[IO] = provider
|
||||
val p = JsonQueryExists[IO](key, JsonQuery.compile(query))
|
||||
p.eval().map(result => assertEquals(result, Predicate.Result.matched()))
|
||||
}
|
||||
}
|
||||
|
||||
iotest("should fail to match against some non-existing key") {
|
||||
val key = Data.keyGen.gen()
|
||||
val value = Data.strValGen.gen()
|
||||
val query = "somethingelse"
|
||||
val blob = Json.obj(
|
||||
key -> value
|
||||
)
|
||||
|
||||
newProvider(Map(key -> blob)).flatMap { provider =>
|
||||
given JsonProvider[IO] = provider
|
||||
val p = JsonQueryExists[IO](key, JsonQuery.compile(query))
|
||||
p.eval().map(result => assertEquals(result, Predicate.Result.missed()))
|
||||
}
|
||||
}
|
||||
|
||||
object JsonQueryExistsTests:
|
||||
|
||||
object Data:
|
||||
|
||||
val keyGen: Gen[String] = Gen.string.alphaNumeric(Size.between(4, 16))
|
||||
|
||||
val strValGen: Gen[Json] =
|
||||
Gen.string.uppercaseAlpha(Size.fixed(8)).map(Json.fromString)
|
||||
|
||||
end Data
|
||||
|
||||
def newProvider(data: Map[String, Json]): IO[JsonProvider[IO]] =
|
||||
for map <- MapRef.ofSingleImmutableMap[IO, String, Json](data)
|
||||
yield new MemoryMapJsonProvider(map)
|
||||
|
||||
end JsonQueryExistsTests
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
package gs.predicate.v0.serde.json
|
||||
|
||||
import cats.effect.IO
|
||||
import gs.datagen.v0.Gen
|
||||
import gs.datagen.v0.generators.Size
|
||||
import gs.predicate.v0.api.Predicate
|
||||
import gs.predicate.v0.json.JsonProvider
|
||||
import gs.predicate.v0.kv.KeyExists
|
||||
import gs.predicate.v0.kv.KeyValueProvider
|
||||
import gs.predicate.v0.kv.ValueContains
|
||||
import gs.predicate.v0.kv.ValueEndsWith
|
||||
import gs.predicate.v0.kv.ValueEquals
|
||||
import gs.predicate.v0.kv.ValueNotEquals
|
||||
import gs.predicate.v0.kv.ValueStartsWith
|
||||
import io.circe.Decoder
|
||||
import io.circe.Encoder
|
||||
import io.circe.Json
|
||||
import munit.FunSuite
|
||||
|
||||
class KeyValueCodecTests extends FunSuite:
|
||||
|
||||
test("should serialize and deserialize a KeyExists predicate") {
|
||||
given JsonProvider[IO] = JsonProvider.noop[IO]
|
||||
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
|
||||
|
||||
val key = Gen.string.alphaNumeric(Size.fixed(8)).gen()
|
||||
val p = KeyExists.apply[IO](key)
|
||||
val encoded = Encoder[KeyExists[IO]].apply(p)
|
||||
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
|
||||
assertEquals(
|
||||
encoded,
|
||||
Json.obj(
|
||||
(JsonKeys.predicateType, Json.fromString(KeyExists.PredicateType)),
|
||||
(JsonKeys.key, Json.fromString(key))
|
||||
)
|
||||
)
|
||||
assertEquals(decoded.isRight, true)
|
||||
assertEquals(
|
||||
decoded.map {
|
||||
case parsed: KeyExists[?] => parsed.key
|
||||
case _ => ""
|
||||
},
|
||||
Right(key)
|
||||
)
|
||||
}
|
||||
|
||||
test("should serialize and deserialize a ValueEquals predicate") {
|
||||
given JsonProvider[IO] = JsonProvider.noop[IO]
|
||||
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
|
||||
|
||||
val key = Gen.string.alphaNumeric(Size.fixed(8)).gen()
|
||||
val value = Gen.string.alphaNumeric(Size.between(8, 16)).gen()
|
||||
val p = ValueEquals.apply[IO](key, value)
|
||||
val encoded = Encoder[ValueEquals[IO]].apply(p)
|
||||
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
|
||||
assertEquals(
|
||||
encoded,
|
||||
Json.obj(
|
||||
(JsonKeys.predicateType, Json.fromString(ValueEquals.PredicateType)),
|
||||
(JsonKeys.key, Json.fromString(key)),
|
||||
(JsonKeys.value, Json.fromString(value))
|
||||
)
|
||||
)
|
||||
assertEquals(decoded.isRight, true)
|
||||
assertEquals(
|
||||
decoded.map {
|
||||
case parsed: ValueEquals[?] => parsed.key -> parsed.value
|
||||
case _ => ""
|
||||
},
|
||||
Right(key -> value)
|
||||
)
|
||||
}
|
||||
|
||||
test("should serialize and deserialize a ValueNotEquals predicate") {
|
||||
given JsonProvider[IO] = JsonProvider.noop[IO]
|
||||
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
|
||||
|
||||
val key = Gen.string.alphaNumeric(Size.fixed(8)).gen()
|
||||
val value = Gen.string.alphaNumeric(Size.between(8, 16)).gen()
|
||||
val p = ValueNotEquals.apply[IO](key, value)
|
||||
val encoded = Encoder[ValueNotEquals[IO]].apply(p)
|
||||
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
|
||||
assertEquals(
|
||||
encoded,
|
||||
Json.obj(
|
||||
(JsonKeys.predicateType, Json.fromString(ValueNotEquals.PredicateType)),
|
||||
(JsonKeys.key, Json.fromString(key)),
|
||||
(JsonKeys.value, Json.fromString(value))
|
||||
)
|
||||
)
|
||||
assertEquals(decoded.isRight, true)
|
||||
assertEquals(
|
||||
decoded.map {
|
||||
case parsed: ValueNotEquals[?] => parsed.key -> parsed.value
|
||||
case _ => ""
|
||||
},
|
||||
Right(key -> value)
|
||||
)
|
||||
}
|
||||
|
||||
test("should serialize and deserialize a ValueContains predicate") {
|
||||
given JsonProvider[IO] = JsonProvider.noop[IO]
|
||||
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
|
||||
|
||||
val key = Gen.string.alphaNumeric(Size.fixed(8)).gen()
|
||||
val value = Gen.string.alphaNumeric(Size.between(8, 16)).gen()
|
||||
val p = ValueContains.apply[IO](key, value)
|
||||
val encoded = Encoder[ValueContains[IO]].apply(p)
|
||||
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
|
||||
assertEquals(
|
||||
encoded,
|
||||
Json.obj(
|
||||
(JsonKeys.predicateType, Json.fromString(ValueContains.PredicateType)),
|
||||
(JsonKeys.key, Json.fromString(key)),
|
||||
(JsonKeys.value, Json.fromString(value))
|
||||
)
|
||||
)
|
||||
assertEquals(decoded.isRight, true)
|
||||
assertEquals(
|
||||
decoded.map {
|
||||
case parsed: ValueContains[?] => parsed.key -> parsed.containedValue
|
||||
case _ => ""
|
||||
},
|
||||
Right(key -> value)
|
||||
)
|
||||
}
|
||||
|
||||
test("should serialize and deserialize a ValueStartsWith predicate") {
|
||||
given JsonProvider[IO] = JsonProvider.noop[IO]
|
||||
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
|
||||
|
||||
val key = Gen.string.alphaNumeric(Size.fixed(8)).gen()
|
||||
val value = Gen.string.alphaNumeric(Size.between(8, 16)).gen()
|
||||
val p = ValueStartsWith.apply[IO](key, value)
|
||||
val encoded = Encoder[ValueStartsWith[IO]].apply(p)
|
||||
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
|
||||
assertEquals(
|
||||
encoded,
|
||||
Json.obj(
|
||||
(
|
||||
JsonKeys.predicateType,
|
||||
Json.fromString(ValueStartsWith.PredicateType)
|
||||
),
|
||||
(JsonKeys.key, Json.fromString(key)),
|
||||
(JsonKeys.value, Json.fromString(value))
|
||||
)
|
||||
)
|
||||
assertEquals(decoded.isRight, true)
|
||||
assertEquals(
|
||||
decoded.map {
|
||||
case parsed: ValueStartsWith[?] => parsed.key -> parsed.prefix
|
||||
case _ => ""
|
||||
},
|
||||
Right(key -> value)
|
||||
)
|
||||
}
|
||||
|
||||
test("should serialize and deserialize a ValueEndsWith predicate") {
|
||||
given JsonProvider[IO] = JsonProvider.noop[IO]
|
||||
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
|
||||
|
||||
val key = Gen.string.alphaNumeric(Size.fixed(8)).gen()
|
||||
val value = Gen.string.alphaNumeric(Size.between(8, 16)).gen()
|
||||
val p = ValueEndsWith.apply[IO](key, value)
|
||||
val encoded = Encoder[ValueEndsWith[IO]].apply(p)
|
||||
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
|
||||
assertEquals(
|
||||
encoded,
|
||||
Json.obj(
|
||||
(JsonKeys.predicateType, Json.fromString(ValueEndsWith.PredicateType)),
|
||||
(JsonKeys.key, Json.fromString(key)),
|
||||
(JsonKeys.value, Json.fromString(value))
|
||||
)
|
||||
)
|
||||
assertEquals(decoded.isRight, true)
|
||||
assertEquals(
|
||||
decoded.map {
|
||||
case parsed: ValueEndsWith[?] => parsed.key -> parsed.suffix
|
||||
case _ => ""
|
||||
},
|
||||
Right(key -> value)
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue