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
|
else
|
||||||
ps match
|
ps match
|
||||||
case Nil =>
|
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)
|
path.json.getOption(json).map(p).getOrElse(false)
|
||||||
case Single(key) :: rest =>
|
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)
|
evalRec(rest, json, path.selectDynamic(key), p, depth + 1)
|
||||||
case ArrayAny(key) :: rest =>
|
case ArrayAny(key) :: rest =>
|
||||||
path
|
path
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,16 @@ object JsonKeys:
|
||||||
*/
|
*/
|
||||||
val values: String = "values"
|
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
|
end JsonKeys
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import gs.predicate.v0.kv.KeyValueProvider
|
||||||
import gs.predicate.v0.kv.ValueContains
|
import gs.predicate.v0.kv.ValueContains
|
||||||
import gs.predicate.v0.kv.ValueEndsWith
|
import gs.predicate.v0.kv.ValueEndsWith
|
||||||
import gs.predicate.v0.kv.ValueEquals
|
import gs.predicate.v0.kv.ValueEquals
|
||||||
|
import gs.predicate.v0.kv.ValueNotEquals
|
||||||
import gs.predicate.v0.kv.ValueStartsWith
|
import gs.predicate.v0.kv.ValueStartsWith
|
||||||
import io.circe.*
|
import io.circe.*
|
||||||
import io.circe.syntax.*
|
import io.circe.syntax.*
|
||||||
|
|
@ -106,6 +107,8 @@ def decodePredicate[F[_]: Applicative: KeyValueProvider: JsonProvider](
|
||||||
case Or.PredicateType => decodeOr[F](cursor)
|
case Or.PredicateType => decodeOr[F](cursor)
|
||||||
case KeyExists.PredicateType => Decoder[KeyExists[F]].apply(cursor)
|
case KeyExists.PredicateType => Decoder[KeyExists[F]].apply(cursor)
|
||||||
case ValueEquals.PredicateType => Decoder[ValueEquals[F]].apply(cursor)
|
case ValueEquals.PredicateType => Decoder[ValueEquals[F]].apply(cursor)
|
||||||
|
case ValueNotEquals.PredicateType =>
|
||||||
|
Decoder[ValueNotEquals[F]].apply(cursor)
|
||||||
case ValueContains.PredicateType =>
|
case ValueContains.PredicateType =>
|
||||||
Decoder[ValueContains[F]].apply(cursor)
|
Decoder[ValueContains[F]].apply(cursor)
|
||||||
case ValueStartsWith.PredicateType =>
|
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