refactored entire library

This commit is contained in:
Pat Garrity 2025-11-26 22:46:06 -06:00
parent 05e619fae7
commit 71d5d72d35
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
57 changed files with 1571 additions and 2484 deletions

View file

@ -21,11 +21,6 @@ val sharedSettings = Seq(
)
val Deps = new {
val Cats = new {
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.6.3"
}
val Circe = new {
val Core: ModuleID = "io.circe" %% "circe-core" % "0.14.15"
val Generic: ModuleID = "io.circe" %% "circe-generic" % "0.14.15"
@ -54,8 +49,6 @@ lazy val `gs-predicate` = project
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Core,
Deps.Cats.Effect,
Deps.Circe.Core,
Deps.Circe.Generic,
Deps.Circe.Optics,

View file

@ -1,7 +1,5 @@
package gs.predicate.v0.api
import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
@ -15,9 +13,9 @@ import io.circe.syntax._
* @param ps
* The predicates to evaluate.
*/
final class And[F[_]: Applicative](
val ps: List[Predicate[F]]
) extends Predicate[F]:
final class And(
val ps: List[Predicate]
) extends Predicate:
/** @inheritDocs
*/
@ -25,46 +23,46 @@ final class And[F[_]: Applicative](
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
ps.map(_.eval()).sequence.map(_.allMatch())
override def eval(input: Any): Predicate.Result =
ps.map(p => p.eval(input)).allMatch()
object And:
final val PredicateType: String = "and"
def empty[F[_]: Applicative]: Predicate[F] = False[F]
def empty: Predicate = False
def apply[F[_]: Applicative](ps: Predicate[F]*): Predicate[F] =
def apply(ps: Predicate*): Predicate =
ps.toList match
case Nil => False[F]
case Nil => False
case p :: Nil => p
case list => new And(list)
given andEncoder[F[_]](
using
Encoder[Predicate[F]]
): Encoder[And[F]] =
Encoder.instance[And[F]] { p =>
Encoder[Predicate]
): Encoder[And] =
Encoder.instance[And] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.predicates, p.ps.asJson)
)
}
private def consumeCursor[F[_]: Applicative](
private def consumeCursor(
cursor: HCursor
)(
using
Decoder[Predicate[F]]
): Either[DecodingFailure, And[F]] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
Decoder[Predicate]
): Either[DecodingFailure, And] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate]]
yield new And(ps)
given andDecoder[F[_]: Applicative](
given andDecoder(
using
Decoder[Predicate[F]]
): Decoder[And[F]] =
Decoder.instance[And[F]] { cursor =>
Decoder[Predicate]
): Decoder[And] =
Decoder.instance[And] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType => consumeCursor(cursor)
case candidate =>

View file

@ -1,6 +1,5 @@
package gs.predicate.v0.api
import cats.Applicative
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
@ -9,7 +8,7 @@ import io.circe.Json
/** Always returns a miss.
*/
final class False[F[_]: Applicative] extends Predicate[F]:
object False extends Predicate:
/** @inheritDocs
*/
@ -17,26 +16,20 @@ final class False[F[_]: Applicative] extends Predicate[F]:
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.missed())
end False
object False:
override def eval(input: Any): Predicate.Result =
Predicate.Result.missed()
final val PredicateType: String = "false"
def apply[F[_]: Applicative]: False[F] = new False[F]
given falseEncoder[F[_]]: Encoder[False[F]] =
Encoder.instance[False[F]] { p =>
given falseEncoder: Encoder[False.type] =
Encoder.instance[False.type] { p =>
Json.obj((JsonKeys.predicateType, Json.fromString(p.predicateType)))
}
given falseDecoder[F[_]: Applicative]: Decoder[False[F]] =
Decoder.instance[False[F]] { cursor =>
given falseDecoder: Decoder[False.type] =
Decoder.instance[False.type] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType => Right(False[F])
case PredicateType => Right(False)
case candidate =>
Left(
DecodingFailure(

View file

@ -12,6 +12,16 @@ object Messages:
): String =
s"Received predicate type '$candidate' but expected '$expected'."
def unrecognizedStringComparison(
candidate: String
): String = s"Unrecognized string comparison: '$candidate'"
def invalidStringComparison(
candidate: String,
expected: String
): String =
s"Received string comparison name '$candidate' but expected '$expected'."
def unrecognizedJsonComparison(
candidate: String
): String = s"Unrecognized JSON comparison: '$candidate'"

View file

@ -1,7 +1,5 @@
package gs.predicate.v0.api
import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
@ -14,9 +12,9 @@ import io.circe.syntax.*
* @param ps
* The predicates to evaluate.
*/
final class Or[F[_]: Applicative](
val ps: List[Predicate[F]]
) extends Predicate[F]:
final class Or(
val ps: List[Predicate]
) extends Predicate:
/** @inheritDocs
*/
@ -24,40 +22,40 @@ final class Or[F[_]: Applicative](
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
ps.map(_.eval()).sequence.map(_.anyMatch())
override def eval(input: Any): Predicate.Result =
ps.map(p => p.eval(input)).anyMatch()
object Or:
final val PredicateType: String = "or"
def empty[F[_]: Applicative]: Predicate[F] = False[F]
def empty: Predicate = False
def apply[F[_]: Applicative](ps: Predicate[F]*): Predicate[F] =
def apply(ps: Predicate*): Predicate =
ps.toList match
case Nil => False[F]
case Nil => False
case p :: Nil => p
case list => new Or(list)
given orEncoder[F[_]](
using
Encoder[Predicate[F]]
): Encoder[Or[F]] =
Encoder.instance[Or[F]] { p =>
Encoder[Predicate]
): Encoder[Or] =
Encoder.instance[Or] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.predicates, p.ps.asJson)
)
}
given orDecoder[F[_]: Applicative](
given orDecoder(
using
Decoder[Predicate[F]]
): Decoder[Or[F]] =
Decoder.instance[Or[F]] { cursor =>
Decoder[Predicate]
): Decoder[Or] =
Decoder.instance[Or] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate]]
yield new Or(ps)
case candidate =>
Left(

View file

@ -1,14 +1,12 @@
package gs.predicate.v0.api
import cats.Applicative
/** A _Predicate_ is some function that accepts any input and emits some
* [[Predicate.Result]] (whether the predicate matched).
*
* Predicates evaluate input extracted from context to see if the predicate
* matches that input.
*/
trait Predicate[F[_]]:
trait Predicate:
/** @return
* The serializable predicate type.
*/
@ -20,15 +18,15 @@ trait Predicate[F[_]]:
* Some [[Predicate.Result]] that describes whether the input matched the
* predicate.
*/
def eval(): F[Predicate.Result]
def eval(input: Any): Predicate.Result
end Predicate
object Predicate:
def alwaysTrue[F[_]: Applicative]: Predicate[F] = True[F]
def alwaysTrue: Predicate = True
def alwaysFalse[F[_]: Applicative]: Predicate[F] = False[F]
def alwaysFalse: Predicate = False
/** The result of evaluating a [[Predicate]] is a Boolean value where:
*

View file

@ -1,6 +1,5 @@
package gs.predicate.v0.api
import cats.Applicative
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
@ -9,7 +8,7 @@ import io.circe.Json
/** Always returns a match.
*/
final class True[F[_]: Applicative] extends Predicate[F]:
object True extends Predicate:
/** @inheritDocs
*/
@ -17,26 +16,20 @@ final class True[F[_]: Applicative] extends Predicate[F]:
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.matched())
end True
object True:
override def eval(input: Any): Predicate.Result =
Predicate.Result.matched()
final val PredicateType: String = "true"
def apply[F[_]: Applicative]: True[F] = new True[F]
given trueEncoder[F[_]]: Encoder[True[F]] =
Encoder.instance[True[F]] { p =>
given trueEncoder: Encoder[True.type] =
Encoder.instance[True.type] { p =>
Json.obj((JsonKeys.predicateType, Json.fromString(p.predicateType)))
}
given trueDecoder[F[_]: Applicative]: Decoder[True[F]] =
Decoder.instance[True[F]] { cursor =>
given trueDecoder: Decoder[True.type] =
Decoder.instance[True.type] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType => Right(True[F])
case PredicateType => Right(True)
case candidate =>
Left(
DecodingFailure(

View file

@ -19,6 +19,79 @@ abstract class JsonComparison(val name: String):
object JsonComparison:
object True extends JsonComparison("true"):
final val Name: String = "true"
def compare(input: Json): Boolean = true
given Encoder[True.type] = Encoder.instance[True.type] { jc =>
Json.obj(
JsonKeys.name -> Json.fromString(Name)
)
}
given Decoder[True.type] = Decoder.instance[True.type] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name => Right(True)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
object False extends JsonComparison("false"):
final val Name: String = "false"
def compare(input: Json): Boolean = false
given Encoder[False.type] = Encoder.instance[False.type] { jc =>
Json.obj(
JsonKeys.name -> Json.fromString(Name)
)
}
given Decoder[False.type] = Decoder.instance[False.type] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name => Right(False)
case candidate =>
Left(
DecodingFailure(
Messages.invalidJsonComparison(candidate, Name),
Nil
)
)
}
}
case class And(cs: List[JsonComparison]) extends JsonComparison(And.Name):
def compare(input: Json): Boolean = cs.map(_.compare(input)).foldLeft(true) {
(
acc,
item
) => acc && item
}
object And:
final val Name: String = "and"
case class Or(cs: List[JsonComparison]) extends JsonComparison(Or.Name):
def compare(input: Json): Boolean =
cs.map(_.compare(input)).foldLeft(false) {
(
acc,
item
) => acc || item
}
object Or:
final val Name: String = "or"
case class Eq(target: Json) extends JsonComparison(Eq.Name):
def compare(input: Json): Boolean = target.equals(input)

View file

@ -0,0 +1,76 @@
package gs.predicate.v0.json
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 given JSON input matches the comparison
* function when the given query is applied.
*
* @param query
* The [[JsonQuery]] that must be satisfied.
* @param comparison
* The JSON comparison that must match the values identified by the query.
*/
final class JsonComparisonPredicate(
val query: JsonQuery,
val comparison: JsonComparison
) extends JsonPredicate:
/** @inheritDocs
*/
final override val predicateType: String =
JsonComparisonPredicate.PredicateType
/** @inheritDocs
*/
override def evalJson(input: Json): Predicate.Result =
Predicate.Result(query.eval(input, comparison.compare))
object JsonComparisonPredicate:
final val PredicateType: String = "json-query-comparison"
def apply(
query: JsonQuery,
comparison: JsonComparison
): JsonComparisonPredicate = new JsonComparisonPredicate(query, comparison)
given jsonQueryComparisonEncoder: Encoder[JsonComparisonPredicate] =
Encoder.instance[JsonComparisonPredicate] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.query, Json.fromString(p.query.toString())),
(JsonKeys.comparison, p.comparison.asJson)
)
}
given jsonQueryComparisonDecoder: Decoder[JsonComparisonPredicate] =
Decoder.instance[JsonComparisonPredicate] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
query <- cursor.downField(JsonKeys.query).as[JsonQuery]
comparison <- cursor
.downField(JsonKeys.comparison)
.as[JsonComparison]
yield new JsonComparisonPredicate(query, comparison)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end JsonComparisonPredicate

View file

@ -1,63 +0,0 @@
package gs.predicate.v0.json
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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
* given key.
*
* @param key
* The name of the JSON value that should exist.
*/
final class JsonExists[F[_]: Functor: JsonProvider](
val key: String
) extends JsonPredicate[F]:
/** @inheritDocs
*/
final override val predicateType: String = JsonExists.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
getJson(key).map(_.isDefined).map(Predicate.Result.apply)
object JsonExists:
final val PredicateType: String = "json-exists"
def apply[F[_]: Functor: JsonProvider](key: String): JsonExists[F] =
new JsonExists[F](key)
given jsonExistsEncoder[F[_]]: Encoder[JsonExists[F]] =
Encoder.instance[JsonExists[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key))
)
}
given jsonExistsDecoder[F[_]: Functor: JsonProvider]: Decoder[JsonExists[F]] =
Decoder.instance[JsonExists[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for key <- cursor.downField(JsonKeys.key).as[String]
yield new JsonExists(key)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end JsonExists

View file

@ -3,5 +3,11 @@ package gs.predicate.v0.json
import gs.predicate.v0.api.Predicate
import io.circe.Json
abstract class JsonPredicate[F[_]: JsonProvider] extends Predicate[F]:
def getJson(key: String): F[Option[Json]] = JsonProvider[F].get(key)
abstract class JsonPredicate extends Predicate:
override def eval(input: Any): Predicate.Result =
input match
case json: Json => evalJson(json)
case _ => Predicate.Result.missed()
def evalJson(input: Json): Predicate.Result

View file

@ -1,37 +0,0 @@
package gs.predicate.v0.json
import cats.Applicative
import io.circe.Json
/** Interface for anything that can fetch values for stored keys.
*/
trait JsonProvider[F[_]]:
/** Get the JSON value associated with some key.
*
* @param key
* The key to fetch.
* @return
* The value stored for the key, or `None` if no such value exists.
*/
def get(key: String): F[Option[Json]]
object JsonProvider:
def apply[F[_]](
using
jp: JsonProvider[F]
): JsonProvider[F] = jp
/** @return
* New instance of the no-op [[JsonProvider]] implementation.
*/
def noop[F[_]: Applicative]: JsonProvider[F] = new Noop[F]
/** No-op implementation that never contains data.
*/
final class Noop[F[_]: Applicative] extends JsonProvider[F]:
/** @inheritDocs
*/
override def get(key: String): F[Option[Json]] = Applicative[F].pure(None)
end JsonProvider

View file

@ -1,91 +0,0 @@
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
* 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)
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

@ -1,84 +0,0 @@
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
* 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 JsonQueryEquals[F[_]: Applicative: JsonProvider](
val key: String,
val query: JsonQuery,
val value: Json
) extends Predicate[F]:
/** @inheritDocs
*/
final override val predicateType: String = JsonQueryEquals.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, _.equals(value)))
}
object JsonQueryEquals:
final val PredicateType: String = "json-query-equals"
def apply[F[_]: Applicative: JsonProvider](
key: String,
query: JsonQuery,
value: Json
): 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

@ -1,41 +0,0 @@
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.
*
* @param key
* The name of the JSON value that must satisfy the given query.
*/
final class JsonQueryExists[F[_]: Applicative: JsonProvider](
val key: String,
val query: JsonQuery
) extends Predicate[F]:
/** @inheritDocs
*/
final override val predicateType: String = JsonQueryExists.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, _ => true))
}
object JsonQueryExists:
final val PredicateType: String = "json-exists"
def apply[F[_]: Applicative: JsonProvider](
key: String,
query: JsonQuery
): JsonQueryExists[F] =
new JsonQueryExists[F](key, query)
end JsonQueryExists

View file

@ -1,84 +0,0 @@
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
* given key, and that blob contains the given query, and the result of the
* query matches one of the given values.
*
* @param key
* The name of the JSON value that must satisfy the given query.
* @param query
* The [[JsonQuery]] that must be satisfied.
* @param values
* The set of JSON values, such that one value should match the input.
*/
final class JsonQueryIn[F[_]: Applicative: JsonProvider](
val key: String,
val query: JsonQuery,
val values: Set[Json]
) extends Predicate[F]:
/** @inheritDocs
*/
final override val predicateType: String = JsonQueryIn.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, values.contains))
}
object JsonQueryIn:
final val PredicateType: String = "json-query-in"
def apply[F[_]: Applicative: JsonProvider](
key: String,
query: JsonQuery,
values: Set[Json]
): 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

@ -1,85 +0,0 @@
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
* 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)
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

@ -1,20 +0,0 @@
package gs.predicate.v0.json
import cats.effect.std.MapRef
import io.circe.Json
/** Provides keys and JSON values, given some in-memory map.
*
* @param map
* The underlying map.
*/
final class MemoryMapJsonProvider[F[_]](
private val map: MapRef[F, String, Option[Json]]
) extends JsonProvider[F]:
/** @inheritDocs
*/
override def get(key: String): F[Option[Json]] =
map.apply(key).get
end MemoryMapJsonProvider

View file

@ -1,65 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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 some [[KeyValueProvider]] contains the given key.
*
* @param id
* The unique identifier of this [[Predicate]].
* @param key
* The key that should exist.
*/
final class KeyExists[F[_]: Functor: KeyValueProvider](
val key: String
) extends KeyValuePredicate[F]:
/** @inheritDocs
*/
override def predicateType: String = KeyExists.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
KeyValueProvider[F].exists(key).map(Predicate.Result.apply)
object KeyExists:
final val PredicateType: String = "kv-key-exists"
def apply[F[_]: Functor: KeyValueProvider](key: String): KeyExists[F] =
new KeyExists[F](key)
given keyExistsEncoder[F[_]]: Encoder[KeyExists[F]] =
Encoder.instance[KeyExists[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key))
)
}
given keyExistsDecoder[F[_]: Functor: KeyValueProvider]
: Decoder[KeyExists[F]] =
Decoder.instance[KeyExists[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for key <- cursor.downField(JsonKeys.key).as[String]
yield new KeyExists(key)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end KeyExists

View file

@ -1,8 +0,0 @@
package gs.predicate.v0.kv
import gs.predicate.v0.api.Predicate
abstract class KeyValuePredicate[F[_]: KeyValueProvider] extends Predicate[F]:
def keyExists(key: String): F[Boolean] = KeyValueProvider[F].exists(key)
def getValue(key: String): F[Option[String]] = KeyValueProvider[F].get(key)

View file

@ -1,49 +0,0 @@
package gs.predicate.v0.kv
import cats.Applicative
/** Interface for anything that can fetch values for stored keys.
*/
trait KeyValueProvider[F[_]]:
/** Determine if some key exists.
*
* @param key
* The key to check.
* @return
* True if the key exists, false otherwise.
*/
def exists(key: String): F[Boolean]
/** Get the value associated with some key.
*
* @param key
* The key to fetch.
* @return
* The value stored for the key, or `None` if no such value exists.
*/
def get(key: String): F[Option[String]]
object KeyValueProvider:
def apply[F[_]](
using
kvp: KeyValueProvider[F]
): KeyValueProvider[F] = kvp
/** @return
* New instance of the no-op [[KeyValueProvider]] implementation.
*/
def noop[F[_]: Applicative]: KeyValueProvider[F] = new Noop[F]
/** No-op implementation that never contains data.
*/
final class Noop[F[_]: Applicative] extends KeyValueProvider[F]:
/** @inheritDocs
*/
override def exists(key: String): F[Boolean] = Applicative[F].pure(false)
/** @inheritDocs
*/
override def get(key: String): F[Option[String]] = Applicative[F].pure(None)
end KeyValueProvider

View file

@ -1,24 +0,0 @@
package gs.predicate.v0.kv
import cats.effect.Sync
import cats.effect.std.MapRef
import cats.syntax.all.*
/** Provides keys and values, given some in-memory map.
*
* @param map
* The underlying map.
*/
final class MemoryMapKeyValueProvider[F[_]: Sync](
private val map: MapRef[F, String, Option[String]]
) extends KeyValueProvider[F]:
/** @inheritDocs
*/
override def exists(key: String): F[Boolean] =
map.apply(key).get.map(_.isDefined)
/** @inheritDocs
*/
override def get(key: String): F[Option[String]] =
map.apply(key).get

View file

@ -1,76 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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 some [[KeyValueProvider]] contains the given key,
* and the string value associated with that key contains some other string.
*
* @param key
* The key that should exist.
* @param containedValue
* The substring that must be contained in the value.
*/
final class ValueContains[F[_]: Functor: KeyValueProvider](
val key: String,
val containedValue: String
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = ValueContains.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
KeyValueProvider[F].get(key).map {
case Some(value) => Predicate.Result(value.contains(containedValue))
case _ => Predicate.Result.missed()
}
object ValueContains:
final val PredicateType: String = "kv-string-contains"
def apply[F[_]: Functor: KeyValueProvider](
key: String,
containedValue: String
): ValueContains[F] =
new ValueContains[F](key, containedValue)
given valueContainsEncoder[F[_]]: Encoder[ValueContains[F]] =
Encoder.instance[ValueContains[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.value, Json.fromString(p.containedValue))
)
}
given valueContainsDecoder[F[_]: Functor: KeyValueProvider]
: Decoder[ValueContains[F]] =
Decoder.instance[ValueContains[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
value <- cursor.downField(JsonKeys.value).as[String]
yield new ValueContains(key, value)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end ValueContains

View file

@ -1,79 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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 some (string-valued) [[KeyValueProvider]] contains
* the given key, and the string value associated with that key ends with some
* other string.
*
* @param id
* The unique identifier of this [[Predicate]].
* @param key
* The key that should exist.
* @param suffix
* The substring that must be the suffix of the value.
*/
final class ValueEndsWith[F[_]: Functor: KeyValueProvider](
val key: String,
val suffix: String
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = ValueEndsWith.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
KeyValueProvider[F].get(key).map {
case Some(value) => Predicate.Result(value.endsWith(suffix))
case _ => Predicate.Result.missed()
}
object ValueEndsWith:
final val PredicateType: String = "kv-string-ends-with"
def apply[F[_]: Functor: KeyValueProvider](
key: String,
suffix: String
): ValueEndsWith[F] =
new ValueEndsWith[F](key, suffix)
given valueEndsWithEncoder[F[_]]: Encoder[ValueEndsWith[F]] =
Encoder.instance[ValueEndsWith[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.value, Json.fromString(p.suffix))
)
}
given valueEndsWithDecoder[F[_]: Functor: KeyValueProvider]
: Decoder[ValueEndsWith[F]] =
Decoder.instance[ValueEndsWith[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
value <- cursor.downField(JsonKeys.value).as[String]
yield new ValueEndsWith(key, value)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end ValueEndsWith

View file

@ -1,76 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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 some [[KeyValueProvider]] contains the given key,
* and the value associated with that key equals the given value.
*
* @param key
* The key that should exist.
* @param value
* The value that must be associated with the key.
*/
final class ValueEquals[F[_]: Functor: KeyValueProvider](
val key: String,
val value: String
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = ValueEquals.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
KeyValueProvider[F].get(key).map {
case Some(value) => Predicate.Result(this.value == value)
case _ => Predicate.Result.missed()
}
object ValueEquals:
final val PredicateType: String = "kv-value-equals"
def apply[F[_]: Functor: KeyValueProvider](
key: String,
value: String
): ValueEquals[F] =
new ValueEquals[F](key, value)
given valueEqualsEncoder[F[_]]: Encoder[ValueEquals[F]] =
Encoder.instance[ValueEquals[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.value, Json.fromString(p.value))
)
}
given valueEqualsDecoder[F[_]: Functor: KeyValueProvider]
: Decoder[ValueEquals[F]] =
Decoder.instance[ValueEquals[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
value <- cursor.downField(JsonKeys.value).as[String]
yield new ValueEquals(key, value)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end ValueEquals

View file

@ -1,76 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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 some [[KeyValueProvider]] contains the given key,
* and the value associated with that key is contained in the set of given
* values.
*
* @param key
* The key that should exist.
* @param values
* The list of values, such that one value should match the input.
*/
final class ValueIn[F[_]: Functor: KeyValueProvider](
val key: String,
val values: Set[String]
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = ValueIn.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
KeyValueProvider[F].get(key).map {
case Some(value) => Predicate.Result(values.contains(value))
case _ => Predicate.Result.missed()
}
object ValueIn:
final val PredicateType: String = "kv-value-in"
def apply[F[_]: Functor: KeyValueProvider](
key: String,
values: Set[String]
): ValueIn[F] =
new ValueIn[F](key, values)
given valueInEncoder[F[_]]: Encoder[ValueIn[F]] =
Encoder.instance[ValueIn[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.values, Json.fromValues(p.values.map(Json.fromString)))
)
}
given valueInDecoder[F[_]: Functor: KeyValueProvider]: Decoder[ValueIn[F]] =
Decoder.instance[ValueIn[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
values <- cursor.downField(JsonKeys.value).as[Set[String]]
yield new ValueIn(key, values)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end ValueIn

View file

@ -1,78 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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 some [[KeyValueProvider]] contains the given key,
* and the value associated with that key is not equal to the given value.
*
* @param id
* The unique identifier of this [[Predicate]].
* @param key
* The key that should exist.
* @param value
* The value that must not be associated with the key.
*/
final class ValueNotEquals[F[_]: Functor: KeyValueProvider](
val key: String,
val value: String
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = ValueNotEquals.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
KeyValueProvider[F].get(key).map {
case Some(v) => Predicate.Result(v != value)
case _ => Predicate.Result.missed()
}
object ValueNotEquals:
final val PredicateType: String = "kv-value-not-equals"
def apply[F[_]: Functor: KeyValueProvider](
key: String,
value: String
): ValueNotEquals[F] =
new ValueNotEquals[F](key, value)
given valueNotEqualsEncoder[F[_]]: Encoder[ValueNotEquals[F]] =
Encoder.instance[ValueNotEquals[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.value, Json.fromString(p.value))
)
}
given valueNotEqualsDecoder[F[_]: Functor: KeyValueProvider]
: Decoder[ValueNotEquals[F]] =
Decoder.instance[ValueNotEquals[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
value <- cursor.downField(JsonKeys.value).as[String]
yield new ValueNotEquals(key, value)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end ValueNotEquals

View file

@ -1,76 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
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 some [[KeyValueProvider]] contains the given key,
* and the string value associated with that key starts with some other string.
*
* @param key
* The key that should exist.
* @param prefix
* The substring that must be the prefix of the value.
*/
final class ValueStartsWith[F[_]: Functor: KeyValueProvider](
val key: String,
val prefix: String
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = ValueStartsWith.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
KeyValueProvider[F].get(key).map {
case Some(value) => Predicate.Result(value.startsWith(prefix))
case _ => Predicate.Result.missed()
}
object ValueStartsWith:
final val PredicateType: String = "kv-string-starts-with"
def apply[F[_]: Functor: KeyValueProvider](
key: String,
prefix: String
): ValueStartsWith[F] =
new ValueStartsWith[F](key, prefix)
given valueStartsWithEncoder[F[_]]: Encoder[ValueStartsWith[F]] =
Encoder.instance[ValueStartsWith[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.key, Json.fromString(p.key)),
(JsonKeys.value, Json.fromString(p.prefix))
)
}
given valueStartsWithDecoder[F[_]: Functor: KeyValueProvider]
: Decoder[ValueStartsWith[F]] =
Decoder.instance[ValueStartsWith[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for
key <- cursor.downField(JsonKeys.key).as[String]
value <- cursor.downField(JsonKeys.value).as[String]
yield new ValueStartsWith(key, value)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end ValueStartsWith

View file

@ -12,10 +12,6 @@ object JsonKeys:
*/
val predicates: String = "predicates"
/** Used to represent some "key" (e.g. a key/value or JSON key).
*/
val key: String = "key"
/** Used to represent any value.
*/
val value: String = "value"
@ -44,4 +40,8 @@ object JsonKeys:
*/
val comparison: String = "comparison"
/** Used to collect comparisons for composites such as AND and OR.
*/
val comparisons: String = "comparisons"
end JsonKeys

View file

@ -1,6 +1,5 @@
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
@ -8,15 +7,9 @@ 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
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 gs.predicate.v0.json.JsonComparisonPredicate
import gs.predicate.v0.string.StringComparison
import gs.predicate.v0.string.StringComparisonPredicate
import io.circe.*
import io.circe.syntax.*
@ -27,12 +20,24 @@ import io.circe.syntax.*
* @return
* The JSON blob representing the [[And]].
*/
def encodeAnd[F[_]](and: And[F]): Json =
def encodeAnd(and: And): Json =
Json.obj(
(JsonKeys.predicateType, Json.fromString(And.PredicateType)),
(JsonKeys.predicates, and.ps.asJson)
)
def encodeJsonComparisonAnd(and: JsonComparison.And): Json =
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.And.Name)),
(JsonKeys.comparisons, and.cs.asJson)
)
def encodeStringComparisonAnd(and: StringComparison.And): Json =
Json.obj(
(JsonKeys.name, Json.fromString(StringComparison.And.Name)),
(JsonKeys.comparisons, and.cs.asJson)
)
/** Given some [[Or]] predicate, encode it as a JSON blob.
*
* @param and
@ -40,27 +45,36 @@ def encodeAnd[F[_]](and: And[F]): Json =
* @return
* The JSON blob representing the [[Or]].
*/
def encodeOr[F[_]](and: Or[F]): Json =
def encodeOr(and: Or): Json =
Json.obj(
(JsonKeys.predicateType, Json.fromString(Or.PredicateType)),
(JsonKeys.predicates, and.ps.asJson)
)
def encodeJsonComparisonOr(or: JsonComparison.Or): Json =
Json.obj(
(JsonKeys.name, Json.fromString(JsonComparison.Or.Name)),
(JsonKeys.comparisons, or.cs.asJson)
)
def encodeStringComparisonOr(or: StringComparison.Or): Json =
Json.obj(
(JsonKeys.name, Json.fromString(StringComparison.Or.Name)),
(JsonKeys.comparisons, or.cs.asJson)
)
/** @return
* Generic encoder for any [[Predicate]].
*/
given predicateEncoder[F[_]]: Encoder[Predicate[F]] =
given predicateEncoder: Encoder[Predicate] =
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: JsonQueryComparison[F] => Encoder[JsonQueryComparison[F]].apply(p)
case p: True.type => Encoder[True.type].apply(p)
case p: False.type => Encoder[False.type].apply(p)
case p: And => encodeAnd(p)
case p: Or => encodeOr(p)
case p: StringComparisonPredicate =>
Encoder[StringComparisonPredicate].apply(p)
case p: JsonComparisonPredicate => Encoder[JsonComparisonPredicate].apply(p)
case p =>
throw new IllegalArgumentException(
s"Unsupported predicate type: '${p.predicateType}'"
@ -69,6 +83,12 @@ given predicateEncoder[F[_]]: Encoder[Predicate[F]] =
given jsonComparisonEncoder: Encoder[JsonComparison] =
Encoder.instance[JsonComparison] {
case jc: JsonComparison.And => encodeJsonComparisonAnd(jc)
case jc: JsonComparison.Or => encodeJsonComparisonOr(jc)
case jc: JsonComparison.True.type =>
Encoder[JsonComparison.True.type].apply(jc)
case jc: JsonComparison.False.type =>
Encoder[JsonComparison.False.type].apply(jc)
case jc: JsonComparison.Eq => Encoder[JsonComparison.Eq].apply(jc)
case jc: JsonComparison.Neq => Encoder[JsonComparison.Neq].apply(jc)
case jc: JsonComparison.StringContains =>
@ -104,6 +124,50 @@ given jsonComparisonEncoder: Encoder[JsonComparison] =
)
}
given stringComparisonEncoder: Encoder[StringComparison] =
Encoder.instance[StringComparison] {
case jc: StringComparison.And => encodeStringComparisonAnd(jc)
case jc: StringComparison.Or => encodeStringComparisonOr(jc)
case jc: StringComparison.True.type =>
Encoder[StringComparison.True.type].apply(jc)
case jc: StringComparison.False.type =>
Encoder[StringComparison.False.type].apply(jc)
case jc: StringComparison.Eq => Encoder[StringComparison.Eq].apply(jc)
case jc: StringComparison.Neq => Encoder[StringComparison.Neq].apply(jc)
case jc: StringComparison.StringContains =>
Encoder[StringComparison.StringContains].apply(jc)
case jc: StringComparison.StringPrefix =>
Encoder[StringComparison.StringPrefix].apply(jc)
case jc: StringComparison.StringSuffix =>
Encoder[StringComparison.StringSuffix].apply(jc)
case jc: StringComparison.IntLessThan =>
Encoder[StringComparison.IntLessThan].apply(jc)
case jc: StringComparison.IntLessThanOrEqualTo =>
Encoder[StringComparison.IntLessThanOrEqualTo].apply(jc)
case jc: StringComparison.IntGreaterThan =>
Encoder[StringComparison.IntGreaterThan].apply(jc)
case jc: StringComparison.IntGreaterThanOrEqualTo =>
Encoder[StringComparison.IntGreaterThanOrEqualTo].apply(jc)
case jc: StringComparison.IntBetweenInclusive =>
Encoder[StringComparison.IntBetweenInclusive].apply(jc)
case jc: StringComparison.IntBetweenExclusive =>
Encoder[StringComparison.IntBetweenExclusive].apply(jc)
case jc: StringComparison.DateEq =>
Encoder[StringComparison.DateEq].apply(jc)
case jc: StringComparison.DateBefore =>
Encoder[StringComparison.DateBefore].apply(jc)
case jc: StringComparison.DateAfter =>
Encoder[StringComparison.DateAfter].apply(jc)
case jc: StringComparison.DateBetweenInclusive =>
Encoder[StringComparison.DateBetweenInclusive].apply(jc)
case jc: StringComparison.DateBetweenExclusive =>
Encoder[StringComparison.DateBetweenExclusive].apply(jc)
case jc =>
throw new IllegalArgumentException(
s"Unsupported string comparison: '${jc.name}'"
)
}
/** Given some JSON cursor, decode a logical [[And]] predicate.
*
* @param cursor
@ -111,12 +175,24 @@ given jsonComparisonEncoder: Encoder[JsonComparison] =
* @return
* The [[And]] predicate or a decoding failure.
*/
def decodeAnd[F[_]: Applicative: KeyValueProvider: JsonProvider](
def decodeAnd(
cursor: HCursor
): Either[DecodingFailure, And[F]] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
): Either[DecodingFailure, And] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate]]
yield new And(ps)
def decodeJsonComparisonAnd(
cursor: HCursor
): Either[DecodingFailure, JsonComparison.And] =
for cs <- cursor.downField(JsonKeys.comparisons).as[List[JsonComparison]]
yield new JsonComparison.And(cs)
def decodeStringComparisonAnd(
cursor: HCursor
): Either[DecodingFailure, StringComparison.And] =
for cs <- cursor.downField(JsonKeys.comparisons).as[List[StringComparison]]
yield new StringComparison.And(cs)
/** Given some JSON cursor, decode a logical [[Or]] predicate.
*
* @param cursor
@ -124,11 +200,22 @@ def decodeAnd[F[_]: Applicative: KeyValueProvider: JsonProvider](
* @return
* The [[Or]] predicate or a decoding failure.
*/
def decodeOr[F[_]: Applicative: KeyValueProvider: JsonProvider](cursor: HCursor)
: Either[DecodingFailure, Or[F]] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
def decodeOr(cursor: HCursor): Either[DecodingFailure, Or] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate]]
yield new Or(ps)
def decodeJsonComparisonOr(
cursor: HCursor
): Either[DecodingFailure, JsonComparison.Or] =
for cs <- cursor.downField(JsonKeys.comparisons).as[List[JsonComparison]]
yield new JsonComparison.Or(cs)
def decodeStringComparisonOr(
cursor: HCursor
): Either[DecodingFailure, StringComparison.Or] =
for cs <- cursor.downField(JsonKeys.comparisons).as[List[StringComparison]]
yield new StringComparison.Or(cs)
/** Given some JSON cursor, decode any known [[Predicate]].
*
* @param cursor
@ -136,28 +223,20 @@ def decodeOr[F[_]: Applicative: KeyValueProvider: JsonProvider](cursor: HCursor)
* @return
* The decoded [[Predicate]] or some decoding failure.
*/
def decodePredicate[F[_]: Applicative: KeyValueProvider: JsonProvider](
def decodePredicate(
cursor: HCursor
): Either[DecodingFailure, Predicate[F]] =
): Either[DecodingFailure, Predicate] =
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 ValueNotEquals.PredicateType =>
Decoder[ValueNotEquals[F]].apply(cursor)
case ValueContains.PredicateType =>
Decoder[ValueContains[F]].apply(cursor)
case ValueStartsWith.PredicateType =>
Decoder[ValueStartsWith[F]].apply(cursor)
case ValueEndsWith.PredicateType =>
Decoder[ValueEndsWith[F]].apply(cursor)
case JsonQueryComparison.PredicateType =>
Decoder[JsonQueryComparison[F]].apply(cursor)
case True.PredicateType => Right(True)
case False.PredicateType => Right(False)
case And.PredicateType => decodeAnd(cursor)
case Or.PredicateType => decodeOr(cursor)
case StringComparisonPredicate.PredicateType =>
Decoder[StringComparisonPredicate].apply(cursor)
case JsonComparisonPredicate.PredicateType =>
Decoder[JsonComparisonPredicate].apply(cursor)
case _ =>
Left(
DecodingFailure(
@ -170,9 +249,8 @@ def decodePredicate[F[_]: Applicative: KeyValueProvider: JsonProvider](
/** @return
* Generic decoder for any [[Predicate]].
*/
given predicateDecoder[F[_]: Applicative: KeyValueProvider: JsonProvider]
: Decoder[Predicate[F]] =
Decoder.instance[Predicate[F]](cursor => decodePredicate[F](cursor))
given predicateDecoder: Decoder[Predicate] =
Decoder.instance[Predicate](cursor => decodePredicate(cursor))
/** Given some JSON cursor, decode any known JSON comparison.
*
@ -186,6 +264,12 @@ def decodeJsonComparison(cursor: HCursor)
for
name <- cursor.downField(JsonKeys.name).as[String]
comparison <- name match
case JsonComparison.And.Name => decodeJsonComparisonAnd(cursor)
case JsonComparison.Or.Name => decodeJsonComparisonOr(cursor)
case JsonComparison.True.Name =>
Decoder[JsonComparison.True.type].apply(cursor)
case JsonComparison.False.Name =>
Decoder[JsonComparison.False.type].apply(cursor)
case JsonComparison.Eq.Name => Decoder[JsonComparison.Eq].apply(cursor)
case JsonComparison.Neq.Name => Decoder[JsonComparison.Neq].apply(cursor)
case JsonComparison.StringContains.Name =>
@ -226,3 +310,57 @@ def decodeJsonComparison(cursor: HCursor)
*/
given jsonComparisonDecoder: Decoder[JsonComparison] =
Decoder.instance[JsonComparison](decodeJsonComparison)
def decodeStringComparison(cursor: HCursor)
: Either[DecodingFailure, StringComparison] =
for
name <- cursor.downField(JsonKeys.name).as[String]
comparison <- name match
case StringComparison.And.Name => decodeStringComparisonAnd(cursor)
case StringComparison.Or.Name => decodeStringComparisonOr(cursor)
case StringComparison.True.Name =>
Decoder[StringComparison.True.type].apply(cursor)
case StringComparison.False.Name =>
Decoder[StringComparison.False.type].apply(cursor)
case StringComparison.Eq.Name =>
Decoder[StringComparison.Eq].apply(cursor)
case StringComparison.Neq.Name =>
Decoder[StringComparison.Neq].apply(cursor)
case StringComparison.StringContains.Name =>
Decoder[StringComparison.StringContains].apply(cursor)
case StringComparison.StringPrefix.Name =>
Decoder[StringComparison.StringPrefix].apply(cursor)
case StringComparison.StringSuffix.Name =>
Decoder[StringComparison.StringSuffix].apply(cursor)
case StringComparison.IntLessThan.Name =>
Decoder[StringComparison.IntLessThan].apply(cursor)
case StringComparison.IntLessThanOrEqualTo.Name =>
Decoder[StringComparison.IntLessThanOrEqualTo].apply(cursor)
case StringComparison.IntGreaterThan.Name =>
Decoder[StringComparison.IntGreaterThan].apply(cursor)
case StringComparison.IntGreaterThanOrEqualTo.Name =>
Decoder[StringComparison.IntGreaterThanOrEqualTo].apply(cursor)
case StringComparison.IntBetweenInclusive.Name =>
Decoder[StringComparison.IntBetweenInclusive].apply(cursor)
case StringComparison.IntBetweenExclusive.Name =>
Decoder[StringComparison.IntBetweenExclusive].apply(cursor)
case StringComparison.DateEq.Name =>
Decoder[StringComparison.DateEq].apply(cursor)
case StringComparison.DateBefore.Name =>
Decoder[StringComparison.DateBefore].apply(cursor)
case StringComparison.DateAfter.Name =>
Decoder[StringComparison.DateAfter].apply(cursor)
case StringComparison.DateBetweenInclusive.Name =>
Decoder[StringComparison.DateBetweenInclusive].apply(cursor)
case StringComparison.DateBetweenExclusive.Name =>
Decoder[StringComparison.DateBetweenExclusive].apply(cursor)
case _ =>
Left(
DecodingFailure(Messages.unrecognizedStringComparison(name), Nil)
)
yield comparison
/** Generic decoder for any [[StringComparison]].
*/
given stringComparisonDecoder: Decoder[StringComparison] =
Decoder.instance[StringComparison](decodeStringComparison)

View file

@ -0,0 +1,642 @@
package gs.predicate.v0.string
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
import scala.util.Try
/** Serializable comparisons against strings.
*
* @param name
* The name of the comparison - used for serialization.
*/
abstract class StringComparison(val name: String):
def compare(input: String): Boolean
object StringComparison:
object True extends StringComparison("true"):
final val Name: String = "true"
def compare(input: String): Boolean = true
given Encoder[True.type] = Encoder.instance[True.type] { jc =>
Json.obj(
JsonKeys.name -> Json.fromString(Name)
)
}
given Decoder[True.type] = Decoder.instance[True.type] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name => Right(True)
case candidate =>
Left(
DecodingFailure(
Messages.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
object False extends StringComparison("false"):
final val Name: String = "false"
def compare(input: String): Boolean = false
given Encoder[False.type] = Encoder.instance[False.type] { jc =>
Json.obj(
JsonKeys.name -> Json.fromString(Name)
)
}
given Decoder[False.type] = Decoder.instance[False.type] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name => Right(False)
case candidate =>
Left(
DecodingFailure(
Messages.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class And(cs: List[StringComparison]) extends StringComparison(And.Name):
def compare(input: String): Boolean =
cs.map(_.compare(input)).foldLeft(true) {
(
acc,
item
) => acc && item
}
object And:
final val Name: String = "and"
case class Or(cs: List[StringComparison]) extends StringComparison(Or.Name):
def compare(input: String): Boolean =
cs.map(_.compare(input)).foldLeft(false) {
(
acc,
item
) => acc || item
}
object Or:
final val Name: String = "or"
case class Eq(target: String) extends StringComparison(Eq.Name):
def compare(input: String): 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 -> Json.fromString(jc.target)
)
}
given Decoder[Eq] = Decoder.instance[Eq] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[String]
yield Eq(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class Neq(target: String) extends StringComparison(Neq.Name):
def compare(input: String): 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 -> Json.fromString(jc.target)
)
}
given Decoder[Neq] = Decoder.instance[Neq] { cursor =>
cursor.downField(JsonKeys.name).as[String].flatMap {
case Name =>
for target <- cursor.downField(JsonKeys.value).as[String]
yield Neq(target)
case candidate =>
Left(
DecodingFailure(
Messages.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class StringContains(target: String)
extends StringComparison(StringContains.Name):
def compare(input: String): Boolean =
input.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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class StringPrefix(target: String)
extends StringComparison(StringPrefix.Name):
def compare(input: String): Boolean =
input.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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class StringSuffix(target: String)
extends StringComparison(StringSuffix.Name):
def compare(input: String): Boolean =
input.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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class IntLessThan(target: Int)
extends StringComparison(IntLessThan.Name):
def compare(input: String): Boolean =
asInt(input).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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class IntLessThanOrEqualTo(target: Int)
extends StringComparison(IntLessThanOrEqualTo.Name):
def compare(input: String): Boolean =
asInt(input).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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class IntGreaterThan(target: Int)
extends StringComparison(IntGreaterThan.Name):
def compare(input: String): Boolean =
asInt(input).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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class IntGreaterThanOrEqualTo(target: Int)
extends StringComparison(IntGreaterThanOrEqualTo.Name):
def compare(input: String): Boolean =
asInt(input).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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class IntBetweenInclusive(
lower: Int,
upper: Int
) extends StringComparison(IntBetweenInclusive.Name):
def compare(input: String): Boolean =
asInt(input).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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class IntBetweenExclusive(
lower: Int,
upper: Int
) extends StringComparison(IntBetweenExclusive.Name):
def compare(input: String): Boolean =
asInt(input).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)
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class DateEq(target: LocalDate) extends StringComparison(DateEq.Name):
def compare(input: String): Boolean =
asDate(input).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())
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class DateBefore(target: LocalDate)
extends StringComparison(DateBefore.Name):
def compare(input: String): Boolean =
asDate(input).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())
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class DateAfter(target: LocalDate)
extends StringComparison(DateAfter.Name):
def compare(input: String): Boolean =
asDate(input).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())
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class DateBetweenInclusive(
lower: LocalDate,
upper: LocalDate
) extends StringComparison(DateBetweenInclusive.Name):
def compare(input: String): Boolean =
asDate(input)
.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())
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
case class DateBetweenExclusive(
lower: LocalDate,
upper: LocalDate
) extends StringComparison(DateBetweenExclusive.Name):
def compare(input: String): Boolean =
asDate(input)
.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())
)
}
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.invalidStringComparison(candidate, Name),
Nil
)
)
}
}
private def asInt(input: String): Option[Int] =
input.toIntOption
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 StringComparison

View file

@ -0,0 +1,68 @@
package gs.predicate.v0.string
import gs.predicate.v0.api.Messages
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.serde.json.JsonKeys
import gs.predicate.v0.serde.json.stringComparisonDecoder
import gs.predicate.v0.serde.json.stringComparisonEncoder
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 given string input matches the comparison
* function.
*
* @param comparison
* The string comparison that must match the input value.
*/
final class StringComparisonPredicate(
val comparison: StringComparison
) extends StringPredicate:
/** @inheritDocs
*/
final override val predicateType: String =
StringComparisonPredicate.PredicateType
/** @inheritDocs
*/
override def evalString(input: String): Predicate.Result =
Predicate.Result(comparison.compare(input))
object StringComparisonPredicate:
final val PredicateType: String = "string-comparison"
def apply(
comparison: StringComparison
): StringComparisonPredicate = new StringComparisonPredicate(comparison)
given jsonQueryComparisonEncoder: Encoder[StringComparisonPredicate] =
Encoder.instance[StringComparisonPredicate] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.comparison, p.comparison.asJson)
)
}
given jsonQueryComparisonDecoder: Decoder[StringComparisonPredicate] =
Decoder.instance[StringComparisonPredicate] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for comparison <- cursor
.downField(JsonKeys.comparison)
.as[StringComparison]
yield new StringComparisonPredicate(comparison)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end StringComparisonPredicate

View file

@ -0,0 +1,12 @@
package gs.predicate.v0.string
import gs.predicate.v0.api.Predicate
abstract class StringPredicate extends Predicate:
override def eval(input: Any): Predicate.Result =
input match
case str: String => evalString(str)
case _ => Predicate.Result.missed()
def evalString(input: String): Predicate.Result

View file

@ -1,19 +0,0 @@
package support
import cats.effect.IO
import cats.effect.unsafe.IORuntime
import munit.FunSuite
import munit.Location
abstract class IOSuite extends FunSuite:
implicit val runtime: IORuntime = IORuntime.global
def iotest(
name: String
)(
body: => IO[Any]
)(
implicit
loc: Location
): Unit =
test(name)(body.unsafeRunSync())

View file

@ -1,58 +1,47 @@
package gs.predicate.v0.api
import cats.effect.IO
import support.IOSuite
class AndTests extends munit.FunSuite:
class AndTests extends IOSuite:
test("should return true if all are true") {
val and = And(True, True, True)
val and2 = And(True, True, True)
iotest("should return true if all are true") {
val and = And(True[IO], True[IO], True[IO])
val and2 = And(True[IO], True[IO], True[IO])
for
result <- and.eval()
result2 <- and2.eval()
yield
val result = and.eval(())
val result2 = and2.eval(())
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
}
iotest("should return false if any are false") {
val and = And(True[IO], False[IO], True[IO])
val and2 = And(True[IO], False[IO], True[IO])
test("should return false if any are false") {
val and = And(True, False, True)
val and2 = And(True, False, True)
for
result <- and.eval()
result2 <- and2.eval()
yield
val result = and.eval(())
val result2 = and2.eval(())
assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false)
}
iotest("should return false for an empty list") {
val and = And.empty[IO]
val and2 = And[IO]()
val and3 = And[IO]()
test("should return false for an empty list") {
val and = And.empty
val and2 = And()
val and3 = And()
for
result <- and.eval()
result2 <- and2.eval()
result3 <- and3.eval()
yield
val result = and.eval(())
val result2 = and2.eval(())
val result3 = and3.eval(())
assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false)
assertEquals(result3.unwrap(), false)
}
iotest("should return the underlying predicate for a singular entry") {
val p = True[IO]
test("should return the underlying predicate for a singular entry") {
val p = True
val and = And(p)
val and2 = And(p)
for
result <- and.eval()
result2 <- and2.eval()
yield
val result = and.eval(())
val result2 = and2.eval(())
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
assertEquals(p, and)

View file

@ -1,13 +1,9 @@
package gs.predicate.v0.api
import cats.effect.IO
import support.IOSuite
class FalseTests extends munit.FunSuite:
class FalseTests extends IOSuite:
iotest("should return false") {
val predicate = False[IO]
for result <- predicate.eval()
yield assertEquals(result.unwrap(), false)
test("should return false") {
val predicate = False
val result = predicate.eval(())
assertEquals(result.unwrap(), false)
}

View file

@ -1,58 +1,47 @@
package gs.predicate.v0.api
import cats.effect.IO
import support.IOSuite
class OrTests extends munit.FunSuite:
class OrTests extends IOSuite:
test("should return true if any are true") {
val or = Or(False, True, False)
val or2 = Or(False, True, False)
iotest("should return true if any are true") {
val or = Or(False[IO], True[IO], False[IO])
val or2 = Or(False[IO], True[IO], False[IO])
for
result <- or.eval()
result2 <- or2.eval()
yield
val result = or.eval(())
val result2 = or2.eval(())
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
}
iotest("should return false if all are false") {
val or = Or(False[IO], False[IO], False[IO])
val or2 = Or(False[IO], False[IO], False[IO])
test("should return false if all are false") {
val or = Or(False, False, False)
val or2 = Or(False, False, False)
for
result <- or.eval()
result2 <- or2.eval()
yield
val result = or.eval(())
val result2 = or2.eval(())
assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false)
}
iotest("should return false for an empty list") {
val or = Or.empty[IO]
val or2 = Or[IO]()
val or3 = Or[IO]()
test("should return false for an empty list") {
val or = Or.empty
val or2 = Or()
val or3 = Or()
for
result <- or.eval()
result2 <- or2.eval()
result3 <- or3.eval()
yield
val result = or.eval(())
val result2 = or2.eval(())
val result3 = or3.eval(())
assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false)
assertEquals(result3.unwrap(), false)
}
iotest("should return the underlying predicate for a singular entry") {
val p = True[IO]
test("should return the underlying predicate for a singular entry") {
val p = True
val or = Or(p)
val or2 = Or(p)
for
result <- or.eval()
result2 <- or2.eval()
yield
val result = or.eval(())
val result2 = or2.eval(())
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
assertEquals(p, or)

View file

@ -1,13 +1,9 @@
package gs.predicate.v0.api
import cats.effect.IO
import support.IOSuite
class TrueTests extends munit.FunSuite:
class TrueTests extends IOSuite:
iotest("should return true") {
val predicate = True[IO]
for result <- predicate.eval()
yield assertEquals(result.unwrap(), true)
test("should return true") {
val predicate = True
val result = predicate.eval(())
assertEquals(result.unwrap(), true)
}

View file

@ -0,0 +1,53 @@
package gs.predicate.v0.json
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
class JsonComparisonPredicateTests extends munit.FunSuite:
import JsonComparisonPredicateTests.Data
test("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)
val p = JsonComparisonPredicate(JsonQuery.compile(query), jc)
val result = p.eval(blob)
assertEquals(result, Predicate.Result.matched())
}
test("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)
val p = JsonComparisonPredicate(JsonQuery.compile(query), jc)
val result = p.eval(blob)
assertEquals(result, Predicate.Result.missed())
}
object JsonComparisonPredicateTests:
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
end JsonComparisonPredicateTests

View file

@ -10,6 +10,41 @@ import munit.FunSuite
class JsonComparisonTests extends FunSuite:
import JsonComparisonTests.*
test("should support True") {
val jc = JsonComparison.True
assert(jc.compare(Json.obj()))
}
test("should support False") {
val jc = JsonComparison.False
assert(!jc.compare(Json.obj()))
}
test("should support And") {
val jc1 = JsonComparison.And(
List(JsonComparison.True, JsonComparison.True, JsonComparison.False)
)
val jc2 = JsonComparison.And(List(JsonComparison.True, JsonComparison.True))
val jc3 = JsonComparison.And(List())
val json = Json.obj()
assert(!jc1.compare(json))
assert(jc2.compare(json))
assert(jc3.compare(json))
}
test("should support Or") {
val jc1 = JsonComparison.Or(
List(JsonComparison.True, JsonComparison.True, JsonComparison.False)
)
val jc2 =
JsonComparison.Or(List(JsonComparison.False, JsonComparison.False))
val jc3 = JsonComparison.Or(List())
val json = Json.obj()
assert(jc1.compare(json))
assert(!jc2.compare(json))
assert(!jc3.compare(json))
}
test("should support Eq") {
val key = keyGen.gen()
val value = jsonStringGen.gen()

View file

@ -1,65 +0,0 @@
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

@ -1,128 +0,0 @@
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 JsonQueryEqualsTests extends IOSuite:
import JsonQueryEqualsTests.Data
import JsonQueryEqualsTests.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
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryEquals[IO](key, JsonQuery.compile(query), value)
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
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryEquals[IO](key, JsonQuery.compile(query), searchValue)
p.eval().map(result => assertEquals(result, Predicate.Result.missed()))
}
}
iotest("should find an exact match against some integer value") {
val key = Data.keyGen.gen()
val value = Data.intValGen.gen()
val query = key
val blob = Json.obj(
key -> value
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryEquals[IO](key, JsonQuery.compile(query), value)
p.eval().map(result => assertEquals(result, Predicate.Result.matched()))
}
}
iotest("should fail to match against some non-equal integer value") {
val key = Data.keyGen.gen()
val value = Data.intValGen.gen()
val searchValue = Data.intValGen.gen()
val query = key
val blob = Json.obj(
key -> value
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryEquals[IO](key, JsonQuery.compile(query), searchValue)
p.eval().map(result => assertEquals(result, Predicate.Result.missed()))
}
}
iotest("should find an exact match against some boolean value") {
val key = Data.keyGen.gen()
val value = Data.boolValGen.gen()
val query = key
val blob = Json.obj(
key -> value
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryEquals[IO](key, JsonQuery.compile(query), value)
p.eval().map(result => assertEquals(result, Predicate.Result.matched()))
}
}
iotest("should fail to match against some non-equal boolean value") {
val key = Data.keyGen.gen()
val value = Json.fromBoolean(true)
val searchValue = Json.fromBoolean(false)
val query = key
val blob = Json.obj(
key -> value
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryEquals[IO](key, JsonQuery.compile(query), searchValue)
p.eval().map(result => assertEquals(result, Predicate.Result.missed()))
}
}
object JsonQueryEqualsTests:
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)
val intValGen: Gen[Json] = Gen.integer.inRange(0, 1000).map(Json.fromInt)
val boolValGen: Gen[Json] = Gen.boolean().map(Json.fromBoolean)
end Data
def newProvider(data: Map[String, Json]): IO[JsonProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, Json](data)
yield new MemoryMapJsonProvider(map)
end JsonQueryEqualsTests

View file

@ -1,62 +0,0 @@
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

@ -1,68 +0,0 @@
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 JsonQueryInTests extends IOSuite:
import JsonQueryInTests.Data
import JsonQueryInTests.newProvider
iotest("should find an exact match against some set of values") {
val key = Data.keyGen.gen()
val value = Data.strValGen.gen()
val values = Set(value, Data.strValGen.gen(), Data.intValGen.gen())
val query = key
val blob = Json.obj(
key -> value
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryIn[IO](key, JsonQuery.compile(query), values)
p.eval().map(result => assertEquals(result, Predicate.Result.matched()))
}
}
iotest("should fail to match anything within some set of values") {
val key = Data.keyGen.gen()
val value = Data.strValGen.gen()
val searchValues = Set(Data.strValGen.gen())
val query = key
val blob = Json.obj(
key -> value
)
newProvider(Map(key -> blob)).flatMap { provider =>
given JsonProvider[IO] = provider
val p = JsonQueryIn[IO](key, JsonQuery.compile(query), searchValues)
p.eval().map(result => assertEquals(result, Predicate.Result.missed()))
}
}
end JsonQueryInTests
object JsonQueryInTests:
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)
val intValGen: Gen[Json] = Gen.integer.inRange(0, 1000).map(Json.fromInt)
end Data
def newProvider(data: Map[String, Json]): IO[JsonProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, Json](data)
yield new MemoryMapJsonProvider(map)
end JsonQueryInTests

View file

@ -1,6 +1,5 @@
package gs.predicate.v0.json.query
import cats.Eq
import io.circe.Json
class CompiledQueryEvalTests extends munit.FunSuite:
@ -9,7 +8,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle a single, top-level key (matching case)") {
val query = compile("key")
val expectedValue = Json.fromString("value")
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, true)
}
@ -17,7 +16,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle a single, top-level key (non-matching case)") {
val query = compile("missing")
val expectedValue = Json.fromString("value")
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, false)
}
@ -27,7 +26,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
) {
val query = compile("foo.bar[any].baz[all].x")
val expectedValue = Json.fromInt(10)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, true)
}
@ -37,7 +36,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
) {
val query = compile("foo.bar[any].baz[all].z")
val expectedValue = Json.fromInt(0)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, false)
}
@ -47,7 +46,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
) {
val query = compile("foo.bar[all].baz[any].y")
val expectedValue = Json.fromString("a")
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, true)
}
@ -57,7 +56,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
) {
val query = compile("foo.bar[all].baz[any].z")
val expectedValue = Json.fromInt(0)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, false)
}
@ -65,7 +64,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle nested index (matching case)") {
val query = compile("foo.bar[0].baz[2].z")
val expectedValue = Json.fromInt(2)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, true)
}
@ -73,7 +72,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle nested index (non-matching case)") {
val query = compile("foo.bar[0].baz[1].z")
val expectedValue = Json.fromInt(2)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, false)
}
@ -81,7 +80,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle index after all (matching case)") {
val query = compile("foo.bar[all].baz[0].x")
val expectedValue = Json.fromInt(10)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, true)
}
@ -89,7 +88,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle index after all (non-matching case)") {
val query = compile("foo.bar[all].baz[0].z")
val expectedValue = Json.fromInt(0)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, false)
}
@ -97,7 +96,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle [any] raw array values (matching case)") {
val query = compile("rawValuesAny[any]")
val expectedValue = Json.fromInt(1)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, true)
}
@ -105,7 +104,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle [any] raw array values (non-matching case)") {
val query = compile("rawValuesAny[any]")
val expectedValue = Json.fromInt(6)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, false)
}
@ -113,7 +112,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle [all] raw array values (matching case)") {
val query = compile("rawValuesAll[all]")
val expectedValue = Json.fromInt(1)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, true)
}
@ -121,7 +120,7 @@ class CompiledQueryEvalTests extends munit.FunSuite:
test("should handle [all] raw array values (non-matching case)") {
val query = compile("rawValuesAll[all]")
val expectedValue = Json.fromInt(6)
val p = (json: Json) => Eq[Json].eqv(json, expectedValue)
val p = (json: Json) => json.equals(expectedValue)
val result = query.eval(Data.parsedJson, p)
assertEquals(result, false)
}

View file

@ -1,49 +0,0 @@
package gs.predicate.v0.kv
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 support.IOSuite
class KeyExistsTests extends IOSuite:
import KeyExistsTests.Data
iotest("should find a key that exists within some provider") {
KeyExistsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = KeyExists[IO](Data.ExistingKey)
for result <- p.eval()
yield
// TODO: assertPredicateMatched(result)
assertEquals(result, Predicate.Result.matched())
}
}
iotest("should not find a key that does not exist within some provider") {
KeyExistsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = KeyExists[IO](Data.NotExistingKey)
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
object KeyExistsTests:
object Data:
val ExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(8)).gen()
val NotExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(6)).gen()
val ExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(10)).gen()
val KeyValues: Map[String, String] = Map(ExistingKey -> ExistingValue)
end Data
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapKeyValueProvider(map)
end KeyExistsTests

View file

@ -1,11 +0,0 @@
package gs.predicate.v0.kv
import cats.Id
class KeyValueProviderTests extends munit.FunSuite:
test("should provide a no-op implementation") {
val kvp = KeyValueProvider.noop[Id]
assertEquals(kvp.exists("something"), false)
assertEquals(kvp.get("something"), None)
}

View file

@ -1,90 +0,0 @@
package gs.predicate.v0.kv
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.kv.StringContainsTests.Data.Substring1
import support.IOSuite
class StringContainsTests extends IOSuite:
import StringContainsTests.Data
iotest("should find a contained string in any position") {
StringContainsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 = ValueContains[IO](Data.PassingKey, Data.Substring1)
val p2 = ValueContains[IO](Data.PassingKey, Data.Substring2)
val p3 = ValueContains[IO](Data.PassingKey, Data.Substring3)
for
r1 <- p1.eval()
r2 <- p2.eval()
r3 <- p3.eval()
yield
assertEquals(r1, Predicate.Result.matched())
assertEquals(r2, Predicate.Result.matched())
assertEquals(r3, Predicate.Result.matched())
}
}
iotest("should not find a key that does not exist within some provider") {
StringContainsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = ValueContains[IO](Data.NotExistingKey, "")
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
iotest("should match if an empty substring is provided") {
StringContainsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 = ValueContains[IO](Data.EmptyStringKey, "")
val p2 = ValueContains[IO](Data.PassingKey, "")
for
r1 <- p1.eval()
r2 <- p2.eval()
yield
assertEquals(r1, Predicate.Result.matched())
assertEquals(r2, Predicate.Result.matched())
}
}
iotest("should not match if the target value is not contained in the input") {
StringContainsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = ValueContains[IO](Data.PassingKey, Substring1.reverse)
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
object StringContainsTests:
object Data:
val PassingKey: String = Gen.string.alphaNumeric(Size.Fixed(8)).gen()
val PassingValue: String = "abcdefhij"
val Substring1: String = "abc"
val Substring2: String = "def"
val Substring3: String = "hij"
val NotExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(6)).gen()
val NotExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(4)).gen()
val EmptyStringKey: String = "empty"
val KeyValues: Map[String, String] = Map(
PassingKey -> PassingValue,
EmptyStringKey -> ""
)
end Data
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapKeyValueProvider(map)
end StringContainsTests

View file

@ -1,100 +0,0 @@
package gs.predicate.v0.kv
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 support.IOSuite
class ValueEndsWithTests extends IOSuite:
import ValueEndsWithTests.Data
iotest("should find a string as a prefix of the input") {
ValueEndsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 = ValueEndsWith[IO](Data.PassingKey, Data.Substring1)
val p2 = ValueEndsWith[IO](Data.PassingKey, Data.Substring2)
val p3 = ValueEndsWith[IO](Data.PassingKey, Data.Substring3)
for
r1 <- p1.eval()
r2 <- p2.eval()
r3 <- p3.eval()
yield
assertEquals(r1, Predicate.Result.matched())
assertEquals(r2, Predicate.Result.matched())
assertEquals(r3, Predicate.Result.matched())
}
}
iotest("should not find a key that does not exist within some provider") {
ValueEndsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = ValueEndsWith[IO](Data.NotExistingKey, "")
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
iotest("should match if an empty substring is provided") {
ValueEndsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 = ValueEndsWith[IO](Data.EmptyStringKey, "")
val p2 = ValueEndsWith[IO](Data.PassingKey, "")
for
r1 <- p1.eval()
r2 <- p2.eval()
yield
assertEquals(r1, Predicate.Result.matched())
assertEquals(r2, Predicate.Result.matched())
}
}
iotest(
"should not match if the target value is not the suffix of the input"
) {
ValueEndsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 = ValueEndsWith[IO](Data.PassingKey, Data.Substring3 + "z")
val p2 =
ValueEndsWith[IO](Data.PassingKey, Data.Substring3.reverse)
val p3 =
ValueEndsWith[IO](Data.PassingKey, Data.Substring2.reverse)
for
r1 <- p1.eval()
r2 <- p2.eval()
r3 <- p3.eval()
yield
assertEquals(r1, Predicate.Result.missed())
assertEquals(r2, Predicate.Result.missed())
assertEquals(r3, Predicate.Result.missed())
}
}
object ValueEndsWithTests:
object Data:
val PassingKey: String = Gen.string.alphaNumeric(Size.Fixed(8)).gen()
val PassingValue: String = "abcdefghi"
val Substring1: String = "i"
val Substring2: String = "hi"
val Substring3: String = "abcdefghi"
val NotExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(6)).gen()
val EmptyStringKey: String = "empty"
val KeyValues: Map[String, String] = Map(
PassingKey -> PassingValue,
EmptyStringKey -> ""
)
end Data
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapKeyValueProvider(map)
end ValueEndsWithTests

View file

@ -1,101 +0,0 @@
package gs.predicate.v0.kv
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 support.IOSuite
class ValueStartsWithTests extends IOSuite:
import ValueStartsWithTests.Data
iotest("should find a string as a prefix of the input") {
ValueStartsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 = ValueStartsWith[IO](Data.PassingKey, Data.Substring1)
val p2 = ValueStartsWith[IO](Data.PassingKey, Data.Substring2)
val p3 = ValueStartsWith[IO](Data.PassingKey, Data.Substring3)
for
r1 <- p1.eval()
r2 <- p2.eval()
r3 <- p3.eval()
yield
assertEquals(r1, Predicate.Result.matched())
assertEquals(r2, Predicate.Result.matched())
assertEquals(r3, Predicate.Result.matched())
}
}
iotest("should not find a key that does not exist within some provider") {
ValueStartsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = ValueStartsWith[IO](Data.NotExistingKey, "")
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
iotest("should match if an empty substring is provided") {
ValueStartsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 = ValueStartsWith[IO](Data.EmptyStringKey, "")
val p2 = ValueStartsWith[IO](Data.PassingKey, "")
for
r1 <- p1.eval()
r2 <- p2.eval()
yield
assertEquals(r1, Predicate.Result.matched())
assertEquals(r2, Predicate.Result.matched())
}
}
iotest(
"should not match if the target value is not the prefix of the input"
) {
ValueStartsWithTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p1 =
ValueStartsWith[IO](Data.PassingKey, Data.Substring3 + "k")
val p2 =
ValueStartsWith[IO](Data.PassingKey, Data.Substring3.reverse)
val p3 =
ValueStartsWith[IO](Data.PassingKey, Data.Substring2.reverse)
for
r1 <- p1.eval()
r2 <- p2.eval()
r3 <- p3.eval()
yield
assertEquals(r1, Predicate.Result.missed())
assertEquals(r2, Predicate.Result.missed())
assertEquals(r3, Predicate.Result.missed())
}
}
object ValueStartsWithTests:
object Data:
val PassingKey: String = Gen.string.alphaNumeric(Size.Fixed(8)).gen()
val PassingValue: String = "abcdefghi"
val Substring1: String = "a"
val Substring2: String = "ab"
val Substring3: String = "abcdefghi"
val NotExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(6)).gen()
val EmptyStringKey: String = "empty"
val KeyValues: Map[String, String] = Map(
PassingKey -> PassingValue,
EmptyStringKey -> ""
)
end Data
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapKeyValueProvider(map)
end ValueStartsWithTests

View file

@ -1,59 +0,0 @@
package gs.predicate.v0.kv
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 support.IOSuite
class ValueEqualsTests extends IOSuite:
import ValueEqualsTests.Data
iotest("should find an exact match against some value") {
ValueEqualsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p =
ValueEquals[IO](Data.ExistingKey, Data.ExistingValue)
for result <- p.eval()
yield assertEquals(result, Predicate.Result.matched())
}
}
iotest("should not find a value if it is not associated to a key") {
ValueEqualsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p =
ValueEquals[IO](Data.ExistingKey, Data.NotExistingValue)
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
iotest("should not find a key that does not exist within some provider") {
ValueEqualsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = ValueEquals[IO](Data.NotExistingKey, "")
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
object ValueEqualsTests:
object Data:
val ExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(8)).gen()
val NotExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(6)).gen()
val ExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(10)).gen()
val NotExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(4)).gen()
val KeyValues: Map[String, String] = Map(ExistingKey -> ExistingValue)
end Data
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapKeyValueProvider(map)
end ValueEqualsTests

View file

@ -1,66 +0,0 @@
package gs.predicate.v0.kv
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 support.IOSuite
class ValueInTests extends IOSuite:
import ValueInTests.Data
iotest("should find an exact match against some value") {
ValueInTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p =
ValueIn[IO](
Data.ExistingKey,
Set(
Data.ExistingValue,
"",
Gen.string.alphaNumeric(Size.Fixed(8)).gen()
)
)
for result <- p.eval()
yield assertEquals(result, Predicate.Result.matched())
}
}
iotest("should not find a value if it is not associated to a key") {
ValueInTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p =
ValueIn[IO](Data.ExistingKey, Set(Data.NotExistingValue))
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
iotest("should not find a key that does not exist within some provider") {
ValueInTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = ValueIn[IO](Data.NotExistingKey, Set(""))
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
object ValueInTests:
object Data:
val ExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(8)).gen()
val NotExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(6)).gen()
val ExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(10)).gen()
val NotExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(4)).gen()
val KeyValues: Map[String, String] = Map(ExistingKey -> ExistingValue)
end Data
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapKeyValueProvider(map)
end ValueInTests

View file

@ -1,62 +0,0 @@
package gs.predicate.v0.kv
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 support.IOSuite
class ValueNotEqualsTests extends IOSuite:
import ValueNotEqualsTests.Data
iotest("should NOT find an exact match against some value") {
ValueNotEqualsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p =
ValueNotEquals[IO](Data.ExistingKey, Data.ExistingValue)
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
iotest("should match a value if that value is not equal to the target") {
ValueNotEqualsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p =
ValueNotEquals[IO](
Data.ExistingKey,
Data.NotExistingValue
)
for result <- p.eval()
yield assertEquals(result, Predicate.Result.matched())
}
}
iotest("should not find a key that does not exist within some provider") {
ValueNotEqualsTests.newProvider(Data.KeyValues).flatMap { provider =>
given KeyValueProvider[IO] = provider
val p = ValueNotEquals[IO](Data.NotExistingKey, "")
for result <- p.eval()
yield assertEquals(result, Predicate.Result.missed())
}
}
object ValueNotEqualsTests:
object Data:
val ExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(8)).gen()
val NotExistingKey: String = Gen.string.alphaNumeric(Size.Fixed(6)).gen()
val ExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(10)).gen()
val NotExistingValue: String = Gen.string.alphaNumeric(Size.Fixed(4)).gen()
val KeyValues: Map[String, String] = Map(ExistingKey -> ExistingValue)
end Data
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapKeyValueProvider(map)
end ValueNotEqualsTests

View file

@ -1,14 +1,11 @@
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
import gs.predicate.v0.json.JsonProvider
import gs.predicate.v0.kv.KeyValueProvider
import io.circe.Decoder
import io.circe.Encoder
import io.circe.Json
@ -17,15 +14,12 @@ 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)
val decoded = Decoder[Predicate].decodeJson(json)
assertEquals(decoded.isLeft, true)
assertEquals(
decoded.left.toOption.map(_.message),
@ -34,9 +28,9 @@ class ApiCodecTests extends FunSuite:
}
test("should serialize and deserialize a True predicate") {
val p = True[IO]
val encoded = Encoder[True[IO]].apply(p)
val decoded = Decoder[True[IO]].decodeJson(encoded)
val p = True
val encoded = Encoder[True.type].apply(p)
val decoded = Decoder[True.type].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
@ -47,9 +41,9 @@ class ApiCodecTests extends FunSuite:
}
test("should serialize and deserialize a False predicate") {
val p = False[IO]
val encoded = Encoder[False[IO]].apply(p)
val decoded = Decoder[False[IO]].decodeJson(encoded)
val p = False
val encoded = Encoder[False.type].apply(p)
val decoded = Decoder[False.type].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
@ -60,16 +54,13 @@ class ApiCodecTests extends FunSuite:
}
test("should serialize and deserialize an And predicate") {
given JsonProvider[IO] = JsonProvider.noop[IO]
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
val t = True[IO]
val f = False[IO]
val trueJson = Encoder[True[IO]].apply(t)
val falseJson = Encoder[False[IO]].apply(f)
val p = And[IO](t, t, f)
val encoded = Encoder[And[IO]].apply(p.asInstanceOf[And[IO]])
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
val t = True
val f = False
val trueJson = Encoder[True.type].apply(t)
val falseJson = Encoder[False.type].apply(f)
val p = And(t, t, f)
val encoded = Encoder[And].apply(p.asInstanceOf[And])
val decoded = Decoder[Predicate].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
@ -81,16 +72,13 @@ class ApiCodecTests extends FunSuite:
}
test("should serialize and deserialize an Or predicate") {
given JsonProvider[IO] = JsonProvider.noop[IO]
given KeyValueProvider[IO] = KeyValueProvider.noop[IO]
val t = True[IO]
val f = False[IO]
val trueJson = Encoder[True[IO]].apply(t)
val falseJson = Encoder[False[IO]].apply(f)
val p = Or[IO](t, t, f)
val encoded = Encoder[Or[IO]].apply(p.asInstanceOf[Or[IO]])
val decoded = Decoder[Predicate[IO]].decodeJson(encoded)
val t = True
val f = False
val trueJson = Encoder[True.type].apply(t)
val falseJson = Encoder[False.type].apply(f)
val p = Or(t, t, f)
val encoded = Encoder[Or].apply(p.asInstanceOf[Or])
val decoded = Decoder[Predicate].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(

View file

@ -1,12 +1,9 @@
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.JsonComparisonPredicate
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
@ -15,36 +12,34 @@ 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]
test(
"should serialize and deserialize a predicate: JsonComparisonPredicate"
) {
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)
val p = JsonComparisonPredicate(query, comparison)
val encoded = Encoder[JsonComparisonPredicate].apply(p)
val decoded = Decoder[Predicate].decodeJson(encoded)
assertEquals(
encoded,
Json.obj(
(
JsonKeys.predicateType,
Json.fromString(JsonQueryComparison.PredicateType)
Json.fromString(JsonComparisonPredicate.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 d: JsonComparisonPredicate =>
(p.query.raw, p.comparison.asJson)
case _ => fail("Decoded an unexpected predicate.")
},
Right((key, query.raw, comparison.asJson))
Right((query.raw, comparison.asJson))
)
}

View file

@ -1,183 +0,0 @@
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)
)
}

View file

@ -0,0 +1,224 @@
package gs.predicate.v0.string
import gs.datagen.v0.Gen
import gs.datagen.v0.generators.MinMax
import gs.datagen.v0.generators.Size
import java.time.LocalDate
import munit.FunSuite
class StringComparisonTests extends FunSuite:
import StringComparisonTests.*
test("should support True") {
val jc = StringComparison.True
assert(jc.compare(""))
}
test("should support False") {
val jc = StringComparison.False
assert(!jc.compare(""))
}
test("should support And") {
val jc1 = StringComparison.And(
List(StringComparison.True, StringComparison.True, StringComparison.False)
)
val jc2 =
StringComparison.And(List(StringComparison.True, StringComparison.True))
val jc3 = StringComparison.And(List())
val str = ""
assert(!jc1.compare(str))
assert(jc2.compare(str))
assert(jc3.compare(str))
}
test("should support Or") {
val jc1 = StringComparison.Or(
List(StringComparison.True, StringComparison.True, StringComparison.False)
)
val jc2 =
StringComparison.Or(List(StringComparison.False, StringComparison.False))
val jc3 = StringComparison.Or(List())
val str = ""
assert(jc1.compare(str))
assert(!jc2.compare(str))
assert(!jc3.compare(str))
}
test("should support Eq") {
val value = strGen.gen()
val jc = StringComparison.Eq(value)
assert(jc.compare(value))
assert(!jc.compare(value + value))
}
test("should support Neq") {
val value = strGen.gen()
val jc = StringComparison.Neq(value)
assert(!jc.compare(value))
assert(jc.compare(value + value))
}
test("should support StringContains") {
val str = strGen.gen()
val substr = str.substring(3, 7)
val jc1 = StringComparison.StringContains(substr)
val jc2 = StringComparison.StringContains(str)
val jc3 = StringComparison.StringContains(longStrGen.gen())
assert(jc1.compare(str))
assert(jc2.compare(str))
assert(!jc3.compare(str))
}
test("should support StringPrefix") {
val str = strGen.gen()
val substr = str.take(4)
val jc1 = StringComparison.StringPrefix(substr)
val jc2 = StringComparison.StringPrefix(str)
val jc3 = StringComparison.StringPrefix(longStrGen.gen())
assert(jc1.compare(str))
assert(jc2.compare(str))
assert(!jc3.compare(str))
}
test("should support StringSuffix") {
val str = strGen.gen()
val substr = str.takeRight(4)
val jc1 = StringComparison.StringSuffix(substr)
val jc2 = StringComparison.StringSuffix(str)
val jc3 = StringComparison.StringSuffix(longStrGen.gen())
assert(jc1.compare(str))
assert(jc2.compare(str))
assert(!jc3.compare(str))
}
test("should support IntLessThan") {
val int = intGen.gen()
val target = 101
val jc1 = StringComparison.IntLessThan(target)
val jc3 = StringComparison.IntLessThan(-1)
assert(jc1.compare(int.toString()))
assert(!jc3.compare(int.toString()))
}
test("should support IntLessThanOrEqualTo") {
val int = intGen.gen()
val target = 101
val jc1 = StringComparison.IntLessThanOrEqualTo(target)
val jc2 = StringComparison.IntLessThanOrEqualTo(int)
val jc3 = StringComparison.IntLessThan(-1)
assert(jc1.compare(int.toString()))
assert(jc2.compare(int.toString()))
assert(!jc3.compare(int.toString()))
}
test("should support IntGreaterThan") {
val int = intGen.gen()
val target = -1
val jc1 = StringComparison.IntGreaterThan(target)
val jc3 = StringComparison.IntGreaterThan(101)
assert(jc1.compare(int.toString()))
assert(!jc3.compare(int.toString()))
}
test("should support IntGreaterThanOrEqualTo") {
val int = intGen.gen()
val target = -1
val jc1 = StringComparison.IntGreaterThanOrEqualTo(target)
val jc2 = StringComparison.IntGreaterThanOrEqualTo(int)
val jc3 = StringComparison.IntGreaterThan(101)
assert(jc1.compare(int.toString()))
assert(jc2.compare(int.toString()))
assert(!jc3.compare(int.toString()))
}
test("should support IntBetweenInclusive") {
val int = intGen.gen()
val lower = 0
val upper = 100
val jc1 = StringComparison.IntBetweenInclusive(lower, upper)
val jc2 = StringComparison.IntBetweenInclusive(int, int)
val jc3 = StringComparison.IntBetweenInclusive(1000, 10000)
assert(jc1.compare(int.toString()))
assert(jc2.compare(int.toString()))
assert(!jc3.compare(int.toString()))
}
test("should support IntBetweenExclusive") {
val int = intGen.gen()
val lower = -1
val upper = 101
val jc1 = StringComparison.IntBetweenExclusive(lower, upper)
val jc2 = StringComparison.IntBetweenExclusive(int, int)
val jc3 = StringComparison.IntBetweenExclusive(1000, 10000)
assert(jc1.compare(int.toString()))
assert(!jc2.compare(int.toString()))
assert(!jc3.compare(int.toString()))
}
test("should support DateEq") {
val date = dateGen.gen()
val target = date
val jc1 = StringComparison.DateEq(target)
val jc2 = StringComparison.DateEq(target.plusDays(1L))
assert(jc1.compare(date.toString()))
assert(!jc2.compare(date.toString()))
}
test("should support DateBefore") {
val date = dateGen.gen()
val target = date.plusDays(1L)
val jc1 = StringComparison.DateBefore(target)
val jc2 = StringComparison.DateBefore(date)
assert(jc1.compare(date.toString()))
assert(!jc2.compare(date.toString()))
}
test("should support DateAfter") {
val date = dateGen.gen()
val target = date.minusDays(1L)
val jc1 = StringComparison.DateAfter(target)
val jc2 = StringComparison.DateAfter(date)
assert(jc1.compare(date.toString()))
assert(!jc2.compare(date.toString()))
}
test("should support DateBetweenInclusive") {
val date = dateGen.gen()
val lower = date.minusDays(1L)
val upper = date.plusDays(1L)
val jc1 = StringComparison.DateBetweenInclusive(lower, upper)
val jc2 = StringComparison.DateBetweenInclusive(date, date)
val jc3 = StringComparison.DateBetweenInclusive(
date.plusDays(1L),
date.plusDays(1000L)
)
assert(jc1.compare(date.toString()))
assert(jc2.compare(date.toString()))
assert(!jc3.compare(date.toString()))
}
test("should support DateBetweenExclusive") {
val date = dateGen.gen()
val lower = date.minusDays(1L)
val upper = date.plusDays(1L)
val jc1 = StringComparison.DateBetweenExclusive(lower, upper)
val jc2 = StringComparison.DateBetweenExclusive(date, date)
val jc3 = StringComparison.DateBetweenExclusive(
date.plusDays(1L),
date.plusDays(1000L)
)
assert(jc1.compare(date.toString()))
assert(!jc2.compare(date.toString()))
assert(!jc3.compare(date.toString()))
}
object StringComparisonTests:
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 intGen: Gen[Int] = Gen.integer.inRange(0, 100)
val dateGen: Gen[LocalDate] = Gen.date.aroundToday(MinMax(1, 3))
end StringComparisonTests