More tests, more serde, prep for refactor again

This commit is contained in:
Pat Garrity 2025-11-26 12:50:30 -06:00
parent d0d2925ee4
commit 05e619fae7
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
15 changed files with 1106 additions and 13 deletions

View file

@ -2,10 +2,24 @@ package gs.predicate.v0.api
object Messages:
def unrecognizedPredicateType(
candidate: String
): String = s"Unrecognized predicate type: '$candidate'"
def invalidPredicateType(
candidate: String,
expected: String
): String =
s"Received predicate type '$candidate' but expected '$expected'."
def unrecognizedJsonComparison(
candidate: String
): String = s"Unrecognized JSON comparison: '$candidate'"
def invalidJsonComparison(
candidate: String,
expected: String
): String =
s"Received JSON comparison name '$candidate' but expected '$expected'."
end Messages

View file

@ -1,6 +1,9 @@
package gs.predicate.v0.json
import gs.predicate.v0.api.Messages
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
import java.time.LocalDate
@ -29,6 +32,21 @@ object JsonComparison:
)
}
given Decoder[Eq] = Decoder.instance[Eq] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[Json]
yield Eq(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class Neq(target: Json) extends JsonComparison(Neq.Name):
def compare(input: Json): Boolean = !target.equals(input)
@ -42,6 +60,21 @@ object JsonComparison:
)
}
given Decoder[Neq] = Decoder.instance[Neq] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[Json]
yield Neq(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class StringContains(target: String)
extends JsonComparison(StringContains.Name):
@ -58,6 +91,21 @@ object JsonComparison:
)
}
given Decoder[StringContains] = Decoder.instance[StringContains] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[String]
yield StringContains(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class StringPrefix(target: String)
extends JsonComparison(StringPrefix.Name):
@ -74,6 +122,21 @@ object JsonComparison:
)
}
given Decoder[StringPrefix] = Decoder.instance[StringPrefix] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[String]
yield StringPrefix(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class StringSuffix(target: String)
extends JsonComparison(StringSuffix.Name):
@ -90,6 +153,21 @@ object JsonComparison:
)
}
given Decoder[StringSuffix] = Decoder.instance[StringSuffix] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[String]
yield StringSuffix(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class IntLessThan(target: Int) extends JsonComparison(IntLessThan.Name):
def compare(input: Json): Boolean =
@ -105,6 +183,21 @@ object JsonComparison:
)
}
given Decoder[IntLessThan] = Decoder.instance[IntLessThan] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[Int]
yield IntLessThan(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class IntLessThanOrEqualTo(target: Int)
extends JsonComparison(IntLessThanOrEqualTo.Name):
@ -122,6 +215,22 @@ object JsonComparison:
)
}
given Decoder[IntLessThanOrEqualTo] =
Decoder.instance[IntLessThanOrEqualTo] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[Int]
yield IntLessThanOrEqualTo(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class IntGreaterThan(target: Int)
extends JsonComparison(IntGreaterThan.Name):
@ -138,6 +247,21 @@ object JsonComparison:
)
}
given Decoder[IntGreaterThan] = Decoder.instance[IntGreaterThan] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[Int]
yield IntGreaterThan(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class IntGreaterThanOrEqualTo(target: Int)
extends JsonComparison(IntGreaterThanOrEqualTo.Name):
@ -155,6 +279,22 @@ object JsonComparison:
)
}
given Decoder[IntGreaterThanOrEqualTo] =
Decoder.instance[IntGreaterThanOrEqualTo] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[Int]
yield IntGreaterThanOrEqualTo(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class IntBetweenInclusive(
lower: Int,
upper: Int
@ -177,6 +317,24 @@ object JsonComparison:
)
}
given Decoder[IntBetweenInclusive] =
Decoder.instance[IntBetweenInclusive] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for
lower <- cursor.downField(JsonKeys.lower).as[Int]
upper <- cursor.downField(JsonKeys.upper).as[Int]
yield IntBetweenInclusive(lower, upper)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class IntBetweenExclusive(
lower: Int,
upper: Int
@ -199,6 +357,24 @@ object JsonComparison:
)
}
given Decoder[IntBetweenExclusive] =
Decoder.instance[IntBetweenExclusive] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for
lower <- cursor.downField(JsonKeys.lower).as[Int]
upper <- cursor.downField(JsonKeys.upper).as[Int]
yield IntBetweenExclusive(lower, upper)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class DateEq(target: LocalDate) extends JsonComparison(DateEq.Name):
def compare(input: Json): Boolean =
@ -214,6 +390,21 @@ object JsonComparison:
)
}
given Decoder[DateEq] = Decoder.instance[DateEq] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[LocalDate]
yield DateEq(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class DateBefore(target: LocalDate)
extends JsonComparison(DateBefore.Name):
@ -230,6 +421,21 @@ object JsonComparison:
)
}
given Decoder[DateBefore] = Decoder.instance[DateBefore] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[LocalDate]
yield DateBefore(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class DateAfter(target: LocalDate)
extends JsonComparison(DateAfter.Name):
@ -246,6 +452,21 @@ object JsonComparison:
)
}
given Decoder[DateAfter] = Decoder.instance[DateAfter] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[LocalDate]
yield DateAfter(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class DateBetweenInclusive(
lower: LocalDate,
upper: LocalDate
@ -273,6 +494,24 @@ object JsonComparison:
)
}
given Decoder[DateBetweenInclusive] =
Decoder.instance[DateBetweenInclusive] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for
lower <- cursor.downField(JsonKeys.lower).as[LocalDate]
upper <- cursor.downField(JsonKeys.upper).as[LocalDate]
yield DateBetweenInclusive(lower, upper)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class DateBetweenExclusive(
lower: LocalDate,
upper: LocalDate
@ -295,6 +534,24 @@ object JsonComparison:
)
}
given Decoder[DateBetweenExclusive] =
Decoder.instance[DateBetweenExclusive] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for
lower <- cursor.downField(JsonKeys.lower).as[LocalDate]
upper <- cursor.downField(JsonKeys.upper).as[LocalDate]
yield DateBetweenExclusive(lower, upper)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
private def asDate(input: String): Option[LocalDate] =
Try(LocalDate.parse(input)).toOption

View file

@ -2,8 +2,17 @@ package gs.predicate.v0.json
import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery
import gs.predicate.v0.serde.json.JsonKeys
import gs.predicate.v0.serde.json.jsonComparisonDecoder
import gs.predicate.v0.serde.json.jsonComparisonEncoder
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
import io.circe.syntax.*
/** 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
@ -47,4 +56,36 @@ object JsonQueryComparison:
): JsonQueryComparison[F] =
new JsonQueryComparison[F](key, query, comparison)
given jsonQueryComparisonEncoder[F[_]]: Encoder[JsonQueryComparison[F]] =
Encoder.instance[JsonQueryComparison[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.query, Json.fromString(p.query.toString())),
(JsonKeys.comparison, p.comparison.asJson)
)
}
given jsonQueryComparisonDecoder[F[_]: Applicative: JsonProvider]
: Decoder[JsonQueryComparison[F]] =
Decoder.instance[JsonQueryComparison[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
query <- cursor.downField(JsonKeys.query).as[JsonQuery]
comparison <- cursor
.downField(JsonKeys.comparison)
.as[JsonComparison]
yield new JsonQueryComparison(key, query, comparison)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end JsonQueryComparison

View file

@ -2,8 +2,13 @@ package gs.predicate.v0.json
import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
/** Predicate that matches if the JSON provider contains a JSON blob with the
@ -46,4 +51,34 @@ object JsonQueryEquals:
): JsonQueryEquals[F] =
new JsonQueryEquals[F](key, query, value)
given jsonQueryEqualsEncoder[F[_]]: Encoder[JsonQueryEquals[F]] =
Encoder.instance[JsonQueryEquals[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.query, Json.fromString(p.query.toString())),
(JsonKeys.value, p.value)
)
}
given jsonQueryEqualsDecoder[F[_]: Applicative: JsonProvider]
: Decoder[JsonQueryEquals[F]] =
Decoder.instance[JsonQueryEquals[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
query <- cursor.downField(JsonKeys.query).as[JsonQuery]
value <- cursor.downField(JsonKeys.value).as[Json]
yield new JsonQueryEquals(key, query, value)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end JsonQueryEquals

View file

@ -2,8 +2,13 @@ package gs.predicate.v0.json
import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
/** Predicate that matches if the JSON provider contains a JSON blob with the
@ -46,4 +51,34 @@ object JsonQueryIn:
): JsonQueryIn[F] =
new JsonQueryIn[F](key, query, values)
given jsonQueryInEncoder[F[_]]: Encoder[JsonQueryIn[F]] =
Encoder.instance[JsonQueryIn[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.query, Json.fromString(p.query.toString())),
(JsonKeys.values, Json.arr(p.values.toSeq*))
)
}
given jsonQueryInDecoder[F[_]: Applicative: JsonProvider]
: Decoder[JsonQueryIn[F]] =
Decoder.instance[JsonQueryIn[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
query <- cursor.downField(JsonKeys.query).as[JsonQuery]
values <- cursor.downField(JsonKeys.value).as[List[Json]]
yield new JsonQueryIn(key, query, values.toSet)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end JsonQueryIn

View file

@ -2,8 +2,13 @@ package gs.predicate.v0.json
import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
/** Predicate that matches if the JSON provider contains a JSON blob with the
@ -47,4 +52,34 @@ object JsonQueryNotEquals:
): JsonQueryNotEquals[F] =
new JsonQueryNotEquals[F](key, query, value)
given jsonQueryNotEqualsEncoder[F[_]]: Encoder[JsonQueryNotEquals[F]] =
Encoder.instance[JsonQueryNotEquals[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.query, Json.fromString(p.query.toString())),
(JsonKeys.value, p.value)
)
}
given jsonQueryNotEqualsDecoder[F[_]: Applicative: JsonProvider]
: Decoder[JsonQueryNotEquals[F]] =
Decoder.instance[JsonQueryNotEquals[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
query <- cursor.downField(JsonKeys.query).as[JsonQuery]
value <- cursor.downField(JsonKeys.value).as[Json]
yield new JsonQueryNotEquals(key, query, value)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end JsonQueryNotEquals

View file

@ -122,6 +122,7 @@ import scala.util.matching.Regex
* Maximum allowed query depth.
*/
final class CompiledQuery private (
val raw: String,
private val parts: List[CompiledQuery.QueryPart],
private val reader: Json => List[Json],
private val maxQueryDepth: Int
@ -237,12 +238,14 @@ object CompiledQuery:
)
else if queryParts.exists(_.hasListTraversal) then
new CompiledQuery(
query,
queryParts,
readerWithTraversal(queryParts),
maxQueryDepth
)
else
new CompiledQuery(
query,
queryParts,
readerWithoutTraversal(queryParts),
maxQueryDepth

View file

@ -5,6 +5,9 @@ import io.circe.Json
/** Implementation of [[JsonQuery]] that always refers to the input value as-is.
*/
object EmptyQuery extends JsonQuery:
/** @inheritDocs
*/
final val raw: String = ""
/** @inheritDocs
*/

View file

@ -1,10 +1,20 @@
package gs.predicate.v0.json.query
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
import scala.util.Failure
import scala.util.Success
import scala.util.Try
/** Predicate-integrated queries for JSON objects.
*/
trait JsonQuery:
/** @return
* The raw query string.
*/
def raw: String
/** Query the given JSON value and evaluate the predicate as part of that
* query.
@ -48,4 +58,21 @@ object JsonQuery:
*/
def compile(query: String): JsonQuery = CompiledQuery.compile(query)
given Encoder[JsonQuery] =
Encoder.instance[JsonQuery](jq => Json.fromString(jq.raw))
given Decoder[JsonQuery] = Decoder.instance[JsonQuery] { cursor =>
cursor.as[String].flatMap { raw =>
Try(compile(raw)) match
case Success(query) => Right(query)
case Failure(exception) =>
Left(
DecodingFailure(
s"Failed to parse JsonQuery - could not compile the query: '${exception.getMessage()}'",
Nil
)
)
}
}
end JsonQuery

View file

@ -36,4 +36,12 @@ object JsonKeys:
*/
val upper: String = "upper"
/** Represents JSON queries.
*/
val query: String = "query"
/** Represents JSON comparisons.
*/
val comparison: String = "comparison"
end JsonKeys

View file

@ -3,10 +3,13 @@ package gs.predicate.v0.serde.json
import cats.Applicative
import gs.predicate.v0.api.And
import gs.predicate.v0.api.False
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Or
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.api.True
import gs.predicate.v0.json.JsonComparison
import gs.predicate.v0.json.JsonProvider
import gs.predicate.v0.json.JsonQueryComparison
import gs.predicate.v0.kv.KeyExists
import gs.predicate.v0.kv.KeyValueProvider
import gs.predicate.v0.kv.ValueContains
@ -48,18 +51,56 @@ def encodeOr[F[_]](and: Or[F]): Json =
*/
given predicateEncoder[F[_]]: Encoder[Predicate[F]] =
Encoder.instance {
case p: True[F] => Encoder[True[F]].apply(p)
case p: False[F] => Encoder[False[F]].apply(p)
case p: And[F] => encodeAnd[F](p)
case p: Or[F] => encodeOr[F](p)
case p: KeyExists[F] => Encoder[KeyExists[F]].apply(p)
case p: ValueEquals[F] => Encoder[ValueEquals[F]].apply(p)
case p: ValueContains[F] => Encoder[ValueContains[F]].apply(p)
case p: ValueStartsWith[F] => Encoder[ValueStartsWith[F]].apply(p)
case p: ValueEndsWith[F] => Encoder[ValueEndsWith[F]].apply(p)
case p =>
case p: True[F] => Encoder[True[F]].apply(p)
case p: False[F] => Encoder[False[F]].apply(p)
case p: And[F] => encodeAnd[F](p)
case p: Or[F] => encodeOr[F](p)
case p: KeyExists[F] => Encoder[KeyExists[F]].apply(p)
case p: ValueEquals[F] => Encoder[ValueEquals[F]].apply(p)
case p: ValueContains[F] => Encoder[ValueContains[F]].apply(p)
case p: ValueStartsWith[F] => Encoder[ValueStartsWith[F]].apply(p)
case p: ValueEndsWith[F] => Encoder[ValueEndsWith[F]].apply(p)
case p: JsonQueryComparison[F] => Encoder[JsonQueryComparison[F]].apply(p)
case p =>
throw new IllegalArgumentException(
s"Unsupported predicate type: ${p.predicateType}"
s"Unsupported predicate type: '${p.predicateType}'"
)
}
given jsonComparisonEncoder: Encoder[JsonComparison] =
Encoder.instance[JsonComparison] {
case jc: JsonComparison.Eq => Encoder[JsonComparison.Eq].apply(jc)
case jc: JsonComparison.Neq => Encoder[JsonComparison.Neq].apply(jc)
case jc: JsonComparison.StringContains =>
Encoder[JsonComparison.StringContains].apply(jc)
case jc: JsonComparison.StringPrefix =>
Encoder[JsonComparison.StringPrefix].apply(jc)
case jc: JsonComparison.StringSuffix =>
Encoder[JsonComparison.StringSuffix].apply(jc)
case jc: JsonComparison.IntLessThan =>
Encoder[JsonComparison.IntLessThan].apply(jc)
case jc: JsonComparison.IntLessThanOrEqualTo =>
Encoder[JsonComparison.IntLessThanOrEqualTo].apply(jc)
case jc: JsonComparison.IntGreaterThan =>
Encoder[JsonComparison.IntGreaterThan].apply(jc)
case jc: JsonComparison.IntGreaterThanOrEqualTo =>
Encoder[JsonComparison.IntGreaterThanOrEqualTo].apply(jc)
case jc: JsonComparison.IntBetweenInclusive =>
Encoder[JsonComparison.IntBetweenInclusive].apply(jc)
case jc: JsonComparison.IntBetweenExclusive =>
Encoder[JsonComparison.IntBetweenExclusive].apply(jc)
case jc: JsonComparison.DateEq => Encoder[JsonComparison.DateEq].apply(jc)
case jc: JsonComparison.DateBefore =>
Encoder[JsonComparison.DateBefore].apply(jc)
case jc: JsonComparison.DateAfter =>
Encoder[JsonComparison.DateAfter].apply(jc)
case jc: JsonComparison.DateBetweenInclusive =>
Encoder[JsonComparison.DateBetweenInclusive].apply(jc)
case jc: JsonComparison.DateBetweenExclusive =>
Encoder[JsonComparison.DateBetweenExclusive].apply(jc)
case jc =>
throw new IllegalArgumentException(
s"Unsupported JSON comparison: '${jc.name}'"
)
}
@ -115,9 +156,14 @@ def decodePredicate[F[_]: Applicative: KeyValueProvider: JsonProvider](
Decoder[ValueStartsWith[F]].apply(cursor)
case ValueEndsWith.PredicateType =>
Decoder[ValueEndsWith[F]].apply(cursor)
case predicateType =>
case JsonQueryComparison.PredicateType =>
Decoder[JsonQueryComparison[F]].apply(cursor)
case _ =>
Left(
DecodingFailure(s"Unrecognized predicate type: '$predicateType'", Nil)
DecodingFailure(
Messages.unrecognizedPredicateType(predicateType),
Nil
)
)
yield predicate
@ -127,3 +173,56 @@ def decodePredicate[F[_]: Applicative: KeyValueProvider: JsonProvider](
given predicateDecoder[F[_]: Applicative: KeyValueProvider: JsonProvider]
: Decoder[Predicate[F]] =
Decoder.instance[Predicate[F]](cursor => decodePredicate[F](cursor))
/** Given some JSON cursor, decode any known JSON comparison.
*
* @param cursor
* The cursor.
* @return
* The decoded [[JsonComparison]] or a decoding failure.
*/
def decodeJsonComparison(cursor: HCursor)
: Either[DecodingFailure, JsonComparison] =
for
name <- cursor.downField(JsonKeys.name).as[String]
comparison <- name match
case JsonComparison.Eq.Name => Decoder[JsonComparison.Eq].apply(cursor)
case JsonComparison.Neq.Name => Decoder[JsonComparison.Neq].apply(cursor)
case JsonComparison.StringContains.Name =>
Decoder[JsonComparison.StringContains].apply(cursor)
case JsonComparison.StringPrefix.Name =>
Decoder[JsonComparison.StringPrefix].apply(cursor)
case JsonComparison.StringSuffix.Name =>
Decoder[JsonComparison.StringSuffix].apply(cursor)
case JsonComparison.IntLessThan.Name =>
Decoder[JsonComparison.IntLessThan].apply(cursor)
case JsonComparison.IntLessThanOrEqualTo.Name =>
Decoder[JsonComparison.IntLessThanOrEqualTo].apply(cursor)
case JsonComparison.IntGreaterThan.Name =>
Decoder[JsonComparison.IntGreaterThan].apply(cursor)
case JsonComparison.IntGreaterThanOrEqualTo.Name =>
Decoder[JsonComparison.IntGreaterThanOrEqualTo].apply(cursor)
case JsonComparison.IntBetweenInclusive.Name =>
Decoder[JsonComparison.IntBetweenInclusive].apply(cursor)
case JsonComparison.IntBetweenExclusive.Name =>
Decoder[JsonComparison.IntBetweenExclusive].apply(cursor)
case JsonComparison.DateEq.Name =>
Decoder[JsonComparison.DateEq].apply(cursor)
case JsonComparison.DateBefore.Name =>
Decoder[JsonComparison.DateBefore].apply(cursor)
case JsonComparison.DateAfter.Name =>
Decoder[JsonComparison.DateAfter].apply(cursor)
case JsonComparison.DateBetweenInclusive.Name =>
Decoder[JsonComparison.DateBetweenInclusive].apply(cursor)
case JsonComparison.DateBetweenExclusive.Name =>
Decoder[JsonComparison.DateBetweenExclusive].apply(cursor)
case _ =>
Left(
DecodingFailure(Messages.unrecognizedJsonComparison(name), Nil)
)
yield comparison
/** Generic decoder for any [[JsonComparison]].
*/
given jsonComparisonDecoder: Decoder[JsonComparison] =
Decoder.instance[JsonComparison](decodeJsonComparison)

View file

@ -0,0 +1,65 @@
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 JsonQueryComparisonTests extends IOSuite:
import JsonQueryComparisonTests.Data
import JsonQueryComparisonTests.newProvider
iotest("should find an exact match against some string value") {
val key = Data.keyGen.gen()
val value = Data.strValGen.gen()
val query = key
val blob = Json.obj(
key -> value
)
val jc = JsonComparison.Eq(value)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryComparison[IO](key, JsonQuery.compile(query), jc)
p.eval().map(result => assertEquals(result, Predicate.Result.matched()))
}
}
iotest("should fail to match against some non-equal string value") {
val key = Data.keyGen.gen()
val value = Data.strValGen.gen()
val searchValue = Data.strValGen.gen()
val query = key
val blob = Json.obj(
key -> value
)
val jc = JsonComparison.Eq(searchValue)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryComparison[IO](key, JsonQuery.compile(query), jc)
p.eval().map(result => assertEquals(result, Predicate.Result.missed()))
}
}
object JsonQueryComparisonTests:
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 JsonQueryComparisonTests

View file

@ -3,6 +3,7 @@ package gs.predicate.v0.serde.json
import cats.effect.IO
import gs.predicate.v0.api.And
import gs.predicate.v0.api.False
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Or
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.api.True
@ -15,6 +16,23 @@ import munit.FunSuite
class ApiCodecTests extends FunSuite:
test("should fail to decode an unknown predicate") {
given JsonProvider[IO] = JsonProvider.noop[IO]
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
val candidate = "unrecognized"
val json: Json = Json.obj(
JsonKeys.predicateType -> Json.fromString(candidate)
)
val decoded = Decoder[Predicate[IO]].decodeJson(json)
assertEquals(decoded.isLeft, true)
assertEquals(
decoded.left.toOption.map(_.message),
Some(Messages.unrecognizedPredicateType(candidate))
)
}
test("should serialize and deserialize a True predicate") {
val p = True[IO]
val encoded = Encoder[True[IO]].apply(p)

View file

@ -0,0 +1,50 @@
package gs.predicate.v0.serde.json
import cats.effect.IO
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.JsonComparison
import gs.predicate.v0.json.JsonProvider
import gs.predicate.v0.json.JsonQueryComparison
import gs.predicate.v0.json.query.JsonQuery
import gs.predicate.v0.kv.KeyValueProvider
import io.circe.Decoder
import io.circe.Encoder
import io.circe.Json
import io.circe.syntax.*
import munit.FunSuite
class JsonCodecTests extends FunSuite:
test("should serialize and deserialize a predicate: JsonComparison") {
given JsonProvider[IO] = JsonProvider.noop[IO]
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
val key = "x"
val value = Json.fromString("y")
val query = JsonQuery.compile(key)
val comparison = JsonComparison.Eq(value)
val p = JsonQueryComparison[IO](key, query, comparison)
val encoded = Encoder[JsonQueryComparison[IO]].apply(p)
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.predicateType,
Json.fromString(JsonQueryComparison.PredicateType)
),
(JsonKeys.key, Json.fromString(key)),
(JsonKeys.query, Json.fromString(query.raw)),
(JsonKeys.comparison, comparison.asJson)
)
)
assertEquals(
decoded.map {
case d: JsonQueryComparison[?] =>
(p.key, p.query.raw, p.comparison.asJson)
case _ => fail("Decoded an unexpected predicate.")
},
Right((key, query.raw, comparison.asJson))
)
}

View file

@ -0,0 +1,403 @@
package gs.predicate.v0.serde.json
import gs.predicate.v0.api.Messages
import gs.predicate.v0.json.JsonComparison
import io.circe.Decoder
import io.circe.Json
import io.circe.syntax._
import java.time.LocalDate
import munit.FunSuite
class JsonComparisonCodecTests extends FunSuite:
test("should fail to decode an unknown comparison") {
val candidate = "unrecognized"
val json: Json = Json.obj(
JsonKeys.name -> Json.fromString(candidate)
)
val decoded = Decoder[JsonComparison].decodeJson(json)
assertEquals(decoded.isLeft, true)
assertEquals(
decoded.left.toOption.map(_.message),
Some(Messages.unrecognizedJsonComparison(candidate))
)
}
test("should serialize and deserialize comparison: Eq") {
val target: Json = Json.obj("x" -> Json.fromString("y"))
val jc = JsonComparison.Eq(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.Eq.Name)),
(JsonKeys.value, target)
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.Eq => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: Neq") {
val target: Json = Json.obj("x" -> Json.fromString("y"))
val jc = JsonComparison.Neq(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.Neq.Name)),
(JsonKeys.value, target)
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.Neq => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: StringContains") {
val target = "target"
val jc = JsonComparison.StringContains(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.StringContains.Name)),
(JsonKeys.value, Json.fromString(target))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.StringContains => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: StringPrefix") {
val target = "target"
val jc = JsonComparison.StringPrefix(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.StringPrefix.Name)),
(JsonKeys.value, Json.fromString(target))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.StringPrefix => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: StringSuffix") {
val target = "target"
val jc = JsonComparison.StringSuffix(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.StringSuffix.Name)),
(JsonKeys.value, Json.fromString(target))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.StringSuffix => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: IntLessThan") {
val target = 10
val jc = JsonComparison.IntLessThan(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.IntLessThan.Name)),
(JsonKeys.value, Json.fromInt(target))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.IntLessThan => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: IntLessThanOrEqualTo") {
val target = 10
val jc = JsonComparison.IntLessThanOrEqualTo(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.name,
Json.fromString(JsonComparison.IntLessThanOrEqualTo.Name)
),
(JsonKeys.value, Json.fromInt(target))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.IntLessThanOrEqualTo => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: IntGreaterThan") {
val target = 10
val jc = JsonComparison.IntGreaterThan(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.IntGreaterThan.Name)),
(JsonKeys.value, Json.fromInt(target))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.IntGreaterThan => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: IntGreaterThanOrEqualTo") {
val target = 10
val jc = JsonComparison.IntGreaterThanOrEqualTo(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.name,
Json.fromString(JsonComparison.IntGreaterThanOrEqualTo.Name)
),
(JsonKeys.value, Json.fromInt(target))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.IntGreaterThanOrEqualTo => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: IntBetweenInclusive") {
val lower = 1
val upper = 10
val jc = JsonComparison.IntBetweenInclusive(lower, upper)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.name,
Json.fromString(JsonComparison.IntBetweenInclusive.Name)
),
(JsonKeys.lower, Json.fromInt(lower)),
(JsonKeys.upper, Json.fromInt(upper))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.IntBetweenInclusive => d.lower -> d.upper
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(lower -> upper)
)
}
test("should serialize and deserialize comparison: IntBetweenExclusive") {
val lower = 1
val upper = 10
val jc = JsonComparison.IntBetweenExclusive(lower, upper)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.name,
Json.fromString(JsonComparison.IntBetweenExclusive.Name)
),
(JsonKeys.lower, Json.fromInt(lower)),
(JsonKeys.upper, Json.fromInt(upper))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.IntBetweenExclusive => d.lower -> d.upper
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(lower -> upper)
)
}
test("should serialize and deserialize comparison: DateEq") {
val target = LocalDate.now()
val jc = JsonComparison.DateEq(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.DateEq.Name)),
(JsonKeys.value, Json.fromString(target.toString()))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.DateEq => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: DateBefore") {
val target = LocalDate.now()
val jc = JsonComparison.DateBefore(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.DateBefore.Name)),
(JsonKeys.value, Json.fromString(target.toString()))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.DateBefore => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: DateAfter") {
val target = LocalDate.now()
val jc = JsonComparison.DateAfter(target)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.DateAfter.Name)),
(JsonKeys.value, Json.fromString(target.toString()))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.DateAfter => d.target
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(target)
)
}
test("should serialize and deserialize comparison: DateBetweenInclusive") {
val lower = LocalDate.now().minusDays(1L)
val upper = LocalDate.now().plusDays(1L)
val jc = JsonComparison.DateBetweenInclusive(lower, upper)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.name,
Json.fromString(JsonComparison.DateBetweenInclusive.Name)
),
(JsonKeys.lower, Json.fromString(lower.toString())),
(JsonKeys.upper, Json.fromString(upper.toString()))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.DateBetweenInclusive => d.lower -> d.upper
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(lower -> upper)
)
}
test("should serialize and deserialize comparison: DateBetweenExclusive") {
val lower = LocalDate.now().minusDays(1L)
val upper = LocalDate.now().plusDays(1L)
val jc = JsonComparison.DateBetweenExclusive(lower, upper)
val encoded = jc.asJson
val decoded = Decoder[JsonComparison].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.name,
Json.fromString(JsonComparison.DateBetweenExclusive.Name)
),
(JsonKeys.lower, Json.fromString(lower.toString())),
(JsonKeys.upper, Json.fromString(upper.toString()))
)
)
assertEquals(decoded.isRight, true)
assertEquals(
decoded.map {
case d: JsonComparison.DateBetweenExclusive => d.lower -> d.upper
case _ => fail("Parsed an unexpected JsonComparison.")
},
Right(lower -> upper)
)
}