Adding generalized JSON comparisons.

This commit is contained in:
Pat Garrity 2025-11-26 10:29:06 -06:00
parent 70976a2610
commit d0d2925ee4
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
9 changed files with 893 additions and 6 deletions

View 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

View 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
/** 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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 =>

View 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

View file

@ -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

View file

@ -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)
)
}