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: object Messages:
def unrecognizedPredicateType(
candidate: String
): String = s"Unrecognized predicate type: '$candidate'"
def invalidPredicateType( def invalidPredicateType(
candidate: String, candidate: String,
expected: String expected: String
): String = ): String =
s"Received predicate type '$candidate' but expected '$expected'." 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 end Messages

View file

@ -1,6 +1,9 @@
package gs.predicate.v0.json package gs.predicate.v0.json
import gs.predicate.v0.api.Messages
import gs.predicate.v0.serde.json.JsonKeys import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder import io.circe.Encoder
import io.circe.Json import io.circe.Json
import java.time.LocalDate 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): case class Neq(target: Json) extends JsonComparison(Neq.Name):
def compare(input: Json): Boolean = !target.equals(input) 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) case class StringContains(target: String)
extends JsonComparison(StringContains.Name): 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) case class StringPrefix(target: String)
extends JsonComparison(StringPrefix.Name): 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) case class StringSuffix(target: String)
extends JsonComparison(StringSuffix.Name): 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): case class IntLessThan(target: Int) extends JsonComparison(IntLessThan.Name):
def compare(input: Json): Boolean = 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) case class IntLessThanOrEqualTo(target: Int)
extends JsonComparison(IntLessThanOrEqualTo.Name): 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) case class IntGreaterThan(target: Int)
extends JsonComparison(IntGreaterThan.Name): 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) case class IntGreaterThanOrEqualTo(target: Int)
extends JsonComparison(IntGreaterThanOrEqualTo.Name): 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( case class IntBetweenInclusive(
lower: Int, lower: Int,
upper: 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( case class IntBetweenExclusive(
lower: Int, lower: Int,
upper: 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): case class DateEq(target: LocalDate) extends JsonComparison(DateEq.Name):
def compare(input: Json): Boolean = 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) case class DateBefore(target: LocalDate)
extends JsonComparison(DateBefore.Name): 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) case class DateAfter(target: LocalDate)
extends JsonComparison(DateAfter.Name): 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( case class DateBetweenInclusive(
lower: LocalDate, lower: LocalDate,
upper: 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( case class DateBetweenExclusive(
lower: LocalDate, lower: LocalDate,
upper: 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] = private def asDate(input: String): Option[LocalDate] =
Try(LocalDate.parse(input)).toOption Try(LocalDate.parse(input)).toOption

View file

@ -2,8 +2,17 @@ package gs.predicate.v0.json
import cats.Applicative import cats.Applicative
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery 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 /** 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 * given key, and that blob contains the given query, and the result of the
@ -47,4 +56,36 @@ object JsonQueryComparison:
): JsonQueryComparison[F] = ): JsonQueryComparison[F] =
new JsonQueryComparison[F](key, query, comparison) 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 end JsonQueryComparison

View file

@ -2,8 +2,13 @@ package gs.predicate.v0.json
import cats.Applicative import cats.Applicative
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery 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 import io.circe.Json
/** Predicate that matches if the JSON provider contains a JSON blob with the /** Predicate that matches if the JSON provider contains a JSON blob with the
@ -46,4 +51,34 @@ object JsonQueryEquals:
): JsonQueryEquals[F] = ): JsonQueryEquals[F] =
new JsonQueryEquals[F](key, query, value) 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 end JsonQueryEquals

View file

@ -2,8 +2,13 @@ package gs.predicate.v0.json
import cats.Applicative import cats.Applicative
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery 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 import io.circe.Json
/** Predicate that matches if the JSON provider contains a JSON blob with the /** Predicate that matches if the JSON provider contains a JSON blob with the
@ -46,4 +51,34 @@ object JsonQueryIn:
): JsonQueryIn[F] = ): JsonQueryIn[F] =
new JsonQueryIn[F](key, query, values) 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 end JsonQueryIn

View file

@ -2,8 +2,13 @@ package gs.predicate.v0.json
import cats.Applicative import cats.Applicative
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery 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 import io.circe.Json
/** Predicate that matches if the JSON provider contains a JSON blob with the /** Predicate that matches if the JSON provider contains a JSON blob with the
@ -47,4 +52,34 @@ object JsonQueryNotEquals:
): JsonQueryNotEquals[F] = ): JsonQueryNotEquals[F] =
new JsonQueryNotEquals[F](key, query, value) 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 end JsonQueryNotEquals

View file

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

View file

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

View file

@ -1,10 +1,20 @@
package gs.predicate.v0.json.query package gs.predicate.v0.json.query
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json import io.circe.Json
import scala.util.Failure
import scala.util.Success
import scala.util.Try
/** Predicate-integrated queries for JSON objects. /** Predicate-integrated queries for JSON objects.
*/ */
trait JsonQuery: trait JsonQuery:
/** @return
* The raw query string.
*/
def raw: String
/** Query the given JSON value and evaluate the predicate as part of that /** Query the given JSON value and evaluate the predicate as part of that
* query. * query.
@ -48,4 +58,21 @@ object JsonQuery:
*/ */
def compile(query: String): JsonQuery = CompiledQuery.compile(query) 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 end JsonQuery

View file

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

View file

@ -3,10 +3,13 @@ package gs.predicate.v0.serde.json
import cats.Applicative import cats.Applicative
import gs.predicate.v0.api.And import gs.predicate.v0.api.And
import gs.predicate.v0.api.False import gs.predicate.v0.api.False
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Or import gs.predicate.v0.api.Or
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.predicate.v0.api.True import gs.predicate.v0.api.True
import gs.predicate.v0.json.JsonComparison
import gs.predicate.v0.json.JsonProvider import gs.predicate.v0.json.JsonProvider
import gs.predicate.v0.json.JsonQueryComparison
import gs.predicate.v0.kv.KeyExists import gs.predicate.v0.kv.KeyExists
import gs.predicate.v0.kv.KeyValueProvider import gs.predicate.v0.kv.KeyValueProvider
import gs.predicate.v0.kv.ValueContains import gs.predicate.v0.kv.ValueContains
@ -48,18 +51,56 @@ def encodeOr[F[_]](and: Or[F]): Json =
*/ */
given predicateEncoder[F[_]]: Encoder[Predicate[F]] = given predicateEncoder[F[_]]: Encoder[Predicate[F]] =
Encoder.instance { Encoder.instance {
case p: True[F] => Encoder[True[F]].apply(p) case p: True[F] => Encoder[True[F]].apply(p)
case p: False[F] => Encoder[False[F]].apply(p) case p: False[F] => Encoder[False[F]].apply(p)
case p: And[F] => encodeAnd[F](p) case p: And[F] => encodeAnd[F](p)
case p: Or[F] => encodeOr[F](p) case p: Or[F] => encodeOr[F](p)
case p: KeyExists[F] => Encoder[KeyExists[F]].apply(p) case p: KeyExists[F] => Encoder[KeyExists[F]].apply(p)
case p: ValueEquals[F] => Encoder[ValueEquals[F]].apply(p) case p: ValueEquals[F] => Encoder[ValueEquals[F]].apply(p)
case p: ValueContains[F] => Encoder[ValueContains[F]].apply(p) case p: ValueContains[F] => Encoder[ValueContains[F]].apply(p)
case p: ValueStartsWith[F] => Encoder[ValueStartsWith[F]].apply(p) case p: ValueStartsWith[F] => Encoder[ValueStartsWith[F]].apply(p)
case p: ValueEndsWith[F] => Encoder[ValueEndsWith[F]].apply(p) case p: ValueEndsWith[F] => Encoder[ValueEndsWith[F]].apply(p)
case p => case p: JsonQueryComparison[F] => Encoder[JsonQueryComparison[F]].apply(p)
case p =>
throw new IllegalArgumentException( 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) Decoder[ValueStartsWith[F]].apply(cursor)
case ValueEndsWith.PredicateType => case ValueEndsWith.PredicateType =>
Decoder[ValueEndsWith[F]].apply(cursor) Decoder[ValueEndsWith[F]].apply(cursor)
case predicateType => case JsonQueryComparison.PredicateType =>
Decoder[JsonQueryComparison[F]].apply(cursor)
case _ =>
Left( Left(
DecodingFailure(s"Unrecognized predicate type: '$predicateType'", Nil) DecodingFailure(
Messages.unrecognizedPredicateType(predicateType),
Nil
)
) )
yield predicate yield predicate
@ -127,3 +173,56 @@ def decodePredicate[F[_]: Applicative: KeyValueProvider: JsonProvider](
given predicateDecoder[F[_]: Applicative: KeyValueProvider: JsonProvider] given predicateDecoder[F[_]: Applicative: KeyValueProvider: JsonProvider]
: Decoder[Predicate[F]] = : Decoder[Predicate[F]] =
Decoder.instance[Predicate[F]](cursor => decodePredicate[F](cursor)) 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 cats.effect.IO
import gs.predicate.v0.api.And import gs.predicate.v0.api.And
import gs.predicate.v0.api.False import gs.predicate.v0.api.False
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Or import gs.predicate.v0.api.Or
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.predicate.v0.api.True import gs.predicate.v0.api.True
@ -15,6 +16,23 @@ import munit.FunSuite
class ApiCodecTests extends 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") { test("should serialize and deserialize a True predicate") {
val p = True[IO] val p = True[IO]
val encoded = Encoder[True[IO]].apply(p) 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)
)
}