From 71d5d72d35b7de01fa155693e5f46094a227c869 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Wed, 26 Nov 2025 22:46:06 -0600 Subject: [PATCH] refactored entire library --- build.sbt | 7 - src/main/scala/gs/predicate/v0/api/And.scala | 40 +- .../scala/gs/predicate/v0/api/False.scala | 23 +- .../scala/gs/predicate/v0/api/Messages.scala | 10 + src/main/scala/gs/predicate/v0/api/Or.scala | 34 +- .../scala/gs/predicate/v0/api/Predicate.scala | 10 +- src/main/scala/gs/predicate/v0/api/True.scala | 23 +- .../gs/predicate/v0/json/JsonComparison.scala | 73 ++ .../v0/json/JsonComparisonPredicate.scala | 76 +++ .../gs/predicate/v0/json/JsonExists.scala | 63 -- .../gs/predicate/v0/json/JsonPredicate.scala | 10 +- .../gs/predicate/v0/json/JsonProvider.scala | 37 - .../v0/json/JsonQueryComparison.scala | 91 --- .../predicate/v0/json/JsonQueryEquals.scala | 84 --- .../predicate/v0/json/JsonQueryExists.scala | 41 -- .../gs/predicate/v0/json/JsonQueryIn.scala | 84 --- .../v0/json/JsonQueryNotEquals.scala | 85 --- .../v0/json/MemoryMapJsonProvider.scala | 20 - .../scala/gs/predicate/v0/kv/KeyExists.scala | 65 -- .../predicate/v0/kv/KeyValuePredicate.scala | 8 - .../gs/predicate/v0/kv/KeyValueProvider.scala | 49 -- .../v0/kv/MemoryMapKeyValueProvider.scala | 24 - .../gs/predicate/v0/kv/ValueContains.scala | 76 --- .../gs/predicate/v0/kv/ValueEndsWith.scala | 79 --- .../gs/predicate/v0/kv/ValueEquals.scala | 76 --- .../scala/gs/predicate/v0/kv/ValueIn.scala | 76 --- .../gs/predicate/v0/kv/ValueNotEquals.scala | 78 --- .../gs/predicate/v0/kv/ValueStartsWith.scala | 76 --- .../gs/predicate/v0/serde/json/JsonKeys.scala | 8 +- .../gs/predicate/v0/serde/json/codecs.scala | 240 +++++-- .../v0/string/StringComparison.scala | 642 ++++++++++++++++++ .../v0/string/StringComparisonPredicate.scala | 68 ++ .../predicate/v0/string/StringPredicate.scala | 12 + src/test/scala/gs/predicate/v0/IOSuite.scala | 19 - .../scala/gs/predicate/v0/api/AndTests.scala | 77 +-- .../gs/predicate/v0/api/FalseTests.scala | 14 +- .../scala/gs/predicate/v0/api/OrTests.scala | 77 +-- .../scala/gs/predicate/v0/api/TrueTests.scala | 14 +- .../json/JsonComparisonPredicateTests.scala | 53 ++ .../v0/json/JsonComparisonTests.scala | 35 + .../v0/json/JsonQueryComparisonTests.scala | 65 -- .../v0/json/JsonQueryEqualsTests.scala | 128 ---- .../v0/json/JsonQueryExistsTests.scala | 62 -- .../predicate/v0/json/JsonQueryInTests.scala | 68 -- .../json/query/CompiledQueryEvalTests.scala | 29 +- .../gs/predicate/v0/kv/KeyExistsTests.scala | 49 -- .../v0/kv/KeyValueProviderTests.scala | 11 - .../predicate/v0/kv/StringContainsTests.scala | 90 --- .../predicate/v0/kv/StringEndsWithTests.scala | 100 --- .../v0/kv/StringStartsWithTests.scala | 101 --- .../gs/predicate/v0/kv/ValueEqualsTests.scala | 59 -- .../gs/predicate/v0/kv/ValueInTests.scala | 66 -- .../predicate/v0/kv/ValueNotEqualsTests.scala | 62 -- .../v0/serde/json/ApiCodecTests.scala | 54 +- .../v0/serde/json/JsonCodecTests.scala | 27 +- .../v0/serde/json/KeyValueCodecTests.scala | 183 ----- .../v0/string/StringComparisonTests.scala | 224 ++++++ 57 files changed, 1571 insertions(+), 2484 deletions(-) create mode 100644 src/main/scala/gs/predicate/v0/json/JsonComparisonPredicate.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/JsonExists.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/JsonProvider.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/JsonQueryComparison.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/JsonQueryEquals.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/JsonQueryExists.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/JsonQueryIn.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/JsonQueryNotEquals.scala delete mode 100644 src/main/scala/gs/predicate/v0/json/MemoryMapJsonProvider.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/KeyExists.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/KeyValuePredicate.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/MemoryMapKeyValueProvider.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/ValueContains.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/ValueEndsWith.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/ValueEquals.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/ValueIn.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala delete mode 100644 src/main/scala/gs/predicate/v0/kv/ValueStartsWith.scala create mode 100644 src/main/scala/gs/predicate/v0/string/StringComparison.scala create mode 100644 src/main/scala/gs/predicate/v0/string/StringComparisonPredicate.scala create mode 100644 src/main/scala/gs/predicate/v0/string/StringPredicate.scala delete mode 100644 src/test/scala/gs/predicate/v0/IOSuite.scala create mode 100644 src/test/scala/gs/predicate/v0/json/JsonComparisonPredicateTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/json/JsonQueryComparisonTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/json/JsonQueryEqualsTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/json/JsonQueryExistsTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/json/JsonQueryInTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/KeyExistsTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/KeyValueProviderTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/StringContainsTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/StringEndsWithTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/StringStartsWithTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/ValueEqualsTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/ValueInTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/kv/ValueNotEqualsTests.scala delete mode 100644 src/test/scala/gs/predicate/v0/serde/json/KeyValueCodecTests.scala create mode 100644 src/test/scala/gs/predicate/v0/string/StringComparisonTests.scala diff --git a/build.sbt b/build.sbt index f2b1602..5ddfe5a 100644 --- a/build.sbt +++ b/build.sbt @@ -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, diff --git a/src/main/scala/gs/predicate/v0/api/And.scala b/src/main/scala/gs/predicate/v0/api/And.scala index ff20cd9..cc226fd 100644 --- a/src/main/scala/gs/predicate/v0/api/And.scala +++ b/src/main/scala/gs/predicate/v0/api/And.scala @@ -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 => diff --git a/src/main/scala/gs/predicate/v0/api/False.scala b/src/main/scala/gs/predicate/v0/api/False.scala index 92733b4..fbafe5a 100644 --- a/src/main/scala/gs/predicate/v0/api/False.scala +++ b/src/main/scala/gs/predicate/v0/api/False.scala @@ -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( diff --git a/src/main/scala/gs/predicate/v0/api/Messages.scala b/src/main/scala/gs/predicate/v0/api/Messages.scala index 75e83ca..2fa34bb 100644 --- a/src/main/scala/gs/predicate/v0/api/Messages.scala +++ b/src/main/scala/gs/predicate/v0/api/Messages.scala @@ -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'" diff --git a/src/main/scala/gs/predicate/v0/api/Or.scala b/src/main/scala/gs/predicate/v0/api/Or.scala index fadab1c..a402775 100644 --- a/src/main/scala/gs/predicate/v0/api/Or.scala +++ b/src/main/scala/gs/predicate/v0/api/Or.scala @@ -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( diff --git a/src/main/scala/gs/predicate/v0/api/Predicate.scala b/src/main/scala/gs/predicate/v0/api/Predicate.scala index 50c9721..c07080c 100644 --- a/src/main/scala/gs/predicate/v0/api/Predicate.scala +++ b/src/main/scala/gs/predicate/v0/api/Predicate.scala @@ -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: * diff --git a/src/main/scala/gs/predicate/v0/api/True.scala b/src/main/scala/gs/predicate/v0/api/True.scala index f848981..0c563d1 100644 --- a/src/main/scala/gs/predicate/v0/api/True.scala +++ b/src/main/scala/gs/predicate/v0/api/True.scala @@ -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( diff --git a/src/main/scala/gs/predicate/v0/json/JsonComparison.scala b/src/main/scala/gs/predicate/v0/json/JsonComparison.scala index 87f55c5..db91a34 100644 --- a/src/main/scala/gs/predicate/v0/json/JsonComparison.scala +++ b/src/main/scala/gs/predicate/v0/json/JsonComparison.scala @@ -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) diff --git a/src/main/scala/gs/predicate/v0/json/JsonComparisonPredicate.scala b/src/main/scala/gs/predicate/v0/json/JsonComparisonPredicate.scala new file mode 100644 index 0000000..9eb5798 --- /dev/null +++ b/src/main/scala/gs/predicate/v0/json/JsonComparisonPredicate.scala @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonExists.scala b/src/main/scala/gs/predicate/v0/json/JsonExists.scala deleted file mode 100644 index f68f9ae..0000000 --- a/src/main/scala/gs/predicate/v0/json/JsonExists.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonPredicate.scala b/src/main/scala/gs/predicate/v0/json/JsonPredicate.scala index b223c21..1c6ea2e 100644 --- a/src/main/scala/gs/predicate/v0/json/JsonPredicate.scala +++ b/src/main/scala/gs/predicate/v0/json/JsonPredicate.scala @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonProvider.scala b/src/main/scala/gs/predicate/v0/json/JsonProvider.scala deleted file mode 100644 index 8256fc8..0000000 --- a/src/main/scala/gs/predicate/v0/json/JsonProvider.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonQueryComparison.scala b/src/main/scala/gs/predicate/v0/json/JsonQueryComparison.scala deleted file mode 100644 index f8a563e..0000000 --- a/src/main/scala/gs/predicate/v0/json/JsonQueryComparison.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonQueryEquals.scala b/src/main/scala/gs/predicate/v0/json/JsonQueryEquals.scala deleted file mode 100644 index eaaa9c2..0000000 --- a/src/main/scala/gs/predicate/v0/json/JsonQueryEquals.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonQueryExists.scala b/src/main/scala/gs/predicate/v0/json/JsonQueryExists.scala deleted file mode 100644 index 861b795..0000000 --- a/src/main/scala/gs/predicate/v0/json/JsonQueryExists.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonQueryIn.scala b/src/main/scala/gs/predicate/v0/json/JsonQueryIn.scala deleted file mode 100644 index 28c61af..0000000 --- a/src/main/scala/gs/predicate/v0/json/JsonQueryIn.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/JsonQueryNotEquals.scala b/src/main/scala/gs/predicate/v0/json/JsonQueryNotEquals.scala deleted file mode 100644 index 93dff41..0000000 --- a/src/main/scala/gs/predicate/v0/json/JsonQueryNotEquals.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/json/MemoryMapJsonProvider.scala b/src/main/scala/gs/predicate/v0/json/MemoryMapJsonProvider.scala deleted file mode 100644 index 92a3aae..0000000 --- a/src/main/scala/gs/predicate/v0/json/MemoryMapJsonProvider.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/KeyExists.scala b/src/main/scala/gs/predicate/v0/kv/KeyExists.scala deleted file mode 100644 index 380c52a..0000000 --- a/src/main/scala/gs/predicate/v0/kv/KeyExists.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/KeyValuePredicate.scala b/src/main/scala/gs/predicate/v0/kv/KeyValuePredicate.scala deleted file mode 100644 index abbbb8c..0000000 --- a/src/main/scala/gs/predicate/v0/kv/KeyValuePredicate.scala +++ /dev/null @@ -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) diff --git a/src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala b/src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala deleted file mode 100644 index 1b88075..0000000 --- a/src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/MemoryMapKeyValueProvider.scala b/src/main/scala/gs/predicate/v0/kv/MemoryMapKeyValueProvider.scala deleted file mode 100644 index 6fa09e8..0000000 --- a/src/main/scala/gs/predicate/v0/kv/MemoryMapKeyValueProvider.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/ValueContains.scala b/src/main/scala/gs/predicate/v0/kv/ValueContains.scala deleted file mode 100644 index bace9e8..0000000 --- a/src/main/scala/gs/predicate/v0/kv/ValueContains.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/ValueEndsWith.scala b/src/main/scala/gs/predicate/v0/kv/ValueEndsWith.scala deleted file mode 100644 index 702a4f5..0000000 --- a/src/main/scala/gs/predicate/v0/kv/ValueEndsWith.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/ValueEquals.scala b/src/main/scala/gs/predicate/v0/kv/ValueEquals.scala deleted file mode 100644 index 2ed87a0..0000000 --- a/src/main/scala/gs/predicate/v0/kv/ValueEquals.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/ValueIn.scala b/src/main/scala/gs/predicate/v0/kv/ValueIn.scala deleted file mode 100644 index 12fd24a..0000000 --- a/src/main/scala/gs/predicate/v0/kv/ValueIn.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala b/src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala deleted file mode 100644 index 7cc6f0f..0000000 --- a/src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/kv/ValueStartsWith.scala b/src/main/scala/gs/predicate/v0/kv/ValueStartsWith.scala deleted file mode 100644 index de134c0..0000000 --- a/src/main/scala/gs/predicate/v0/kv/ValueStartsWith.scala +++ /dev/null @@ -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 diff --git a/src/main/scala/gs/predicate/v0/serde/json/JsonKeys.scala b/src/main/scala/gs/predicate/v0/serde/json/JsonKeys.scala index 3cbc83a..8c8feeb 100644 --- a/src/main/scala/gs/predicate/v0/serde/json/JsonKeys.scala +++ b/src/main/scala/gs/predicate/v0/serde/json/JsonKeys.scala @@ -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 diff --git a/src/main/scala/gs/predicate/v0/serde/json/codecs.scala b/src/main/scala/gs/predicate/v0/serde/json/codecs.scala index 06310e4..be19c9f 100644 --- a/src/main/scala/gs/predicate/v0/serde/json/codecs.scala +++ b/src/main/scala/gs/predicate/v0/serde/json/codecs.scala @@ -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,28 +45,37 @@ 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 => + 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) diff --git a/src/main/scala/gs/predicate/v0/string/StringComparison.scala b/src/main/scala/gs/predicate/v0/string/StringComparison.scala new file mode 100644 index 0000000..cb9f801 --- /dev/null +++ b/src/main/scala/gs/predicate/v0/string/StringComparison.scala @@ -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 diff --git a/src/main/scala/gs/predicate/v0/string/StringComparisonPredicate.scala b/src/main/scala/gs/predicate/v0/string/StringComparisonPredicate.scala new file mode 100644 index 0000000..0b8f6f5 --- /dev/null +++ b/src/main/scala/gs/predicate/v0/string/StringComparisonPredicate.scala @@ -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 diff --git a/src/main/scala/gs/predicate/v0/string/StringPredicate.scala b/src/main/scala/gs/predicate/v0/string/StringPredicate.scala new file mode 100644 index 0000000..5134b83 --- /dev/null +++ b/src/main/scala/gs/predicate/v0/string/StringPredicate.scala @@ -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 diff --git a/src/test/scala/gs/predicate/v0/IOSuite.scala b/src/test/scala/gs/predicate/v0/IOSuite.scala deleted file mode 100644 index 1fc7f44..0000000 --- a/src/test/scala/gs/predicate/v0/IOSuite.scala +++ /dev/null @@ -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()) diff --git a/src/test/scala/gs/predicate/v0/api/AndTests.scala b/src/test/scala/gs/predicate/v0/api/AndTests.scala index 0dfbf68..60f50a6 100644 --- a/src/test/scala/gs/predicate/v0/api/AndTests.scala +++ b/src/test/scala/gs/predicate/v0/api/AndTests.scala @@ -1,60 +1,49 @@ 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 - assertEquals(result.unwrap(), true) - assertEquals(result2.unwrap(), true) + 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 - assertEquals(result.unwrap(), false) - assertEquals(result2.unwrap(), false) + 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 - assertEquals(result.unwrap(), false) - assertEquals(result2.unwrap(), false) - assertEquals(result3.unwrap(), false) + 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 - assertEquals(result.unwrap(), true) - assertEquals(result2.unwrap(), true) - assertEquals(p, and) - assertEquals(p, and2) + val result = and.eval(()) + val result2 = and2.eval(()) + assertEquals(result.unwrap(), true) + assertEquals(result2.unwrap(), true) + assertEquals(p, and) + assertEquals(p, and2) } diff --git a/src/test/scala/gs/predicate/v0/api/FalseTests.scala b/src/test/scala/gs/predicate/v0/api/FalseTests.scala index 5b8fdd9..9ec8508 100644 --- a/src/test/scala/gs/predicate/v0/api/FalseTests.scala +++ b/src/test/scala/gs/predicate/v0/api/FalseTests.scala @@ -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) } diff --git a/src/test/scala/gs/predicate/v0/api/OrTests.scala b/src/test/scala/gs/predicate/v0/api/OrTests.scala index ab98546..d3d3e61 100644 --- a/src/test/scala/gs/predicate/v0/api/OrTests.scala +++ b/src/test/scala/gs/predicate/v0/api/OrTests.scala @@ -1,60 +1,49 @@ 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 - assertEquals(result.unwrap(), true) - assertEquals(result2.unwrap(), true) + 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 - assertEquals(result.unwrap(), false) - assertEquals(result2.unwrap(), false) + 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 - assertEquals(result.unwrap(), false) - assertEquals(result2.unwrap(), false) - assertEquals(result3.unwrap(), false) + 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 - assertEquals(result.unwrap(), true) - assertEquals(result2.unwrap(), true) - assertEquals(p, or) - assertEquals(p, or2) + val result = or.eval(()) + val result2 = or2.eval(()) + assertEquals(result.unwrap(), true) + assertEquals(result2.unwrap(), true) + assertEquals(p, or) + assertEquals(p, or2) } diff --git a/src/test/scala/gs/predicate/v0/api/TrueTests.scala b/src/test/scala/gs/predicate/v0/api/TrueTests.scala index 8038481..c53640c 100644 --- a/src/test/scala/gs/predicate/v0/api/TrueTests.scala +++ b/src/test/scala/gs/predicate/v0/api/TrueTests.scala @@ -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) } diff --git a/src/test/scala/gs/predicate/v0/json/JsonComparisonPredicateTests.scala b/src/test/scala/gs/predicate/v0/json/JsonComparisonPredicateTests.scala new file mode 100644 index 0000000..416ea78 --- /dev/null +++ b/src/test/scala/gs/predicate/v0/json/JsonComparisonPredicateTests.scala @@ -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 diff --git a/src/test/scala/gs/predicate/v0/json/JsonComparisonTests.scala b/src/test/scala/gs/predicate/v0/json/JsonComparisonTests.scala index db1929f..3a8a14a 100644 --- a/src/test/scala/gs/predicate/v0/json/JsonComparisonTests.scala +++ b/src/test/scala/gs/predicate/v0/json/JsonComparisonTests.scala @@ -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() diff --git a/src/test/scala/gs/predicate/v0/json/JsonQueryComparisonTests.scala b/src/test/scala/gs/predicate/v0/json/JsonQueryComparisonTests.scala deleted file mode 100644 index 276ac5f..0000000 --- a/src/test/scala/gs/predicate/v0/json/JsonQueryComparisonTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/json/JsonQueryEqualsTests.scala b/src/test/scala/gs/predicate/v0/json/JsonQueryEqualsTests.scala deleted file mode 100644 index aece2b9..0000000 --- a/src/test/scala/gs/predicate/v0/json/JsonQueryEqualsTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/json/JsonQueryExistsTests.scala b/src/test/scala/gs/predicate/v0/json/JsonQueryExistsTests.scala deleted file mode 100644 index 7e4b25c..0000000 --- a/src/test/scala/gs/predicate/v0/json/JsonQueryExistsTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/json/JsonQueryInTests.scala b/src/test/scala/gs/predicate/v0/json/JsonQueryInTests.scala deleted file mode 100644 index 8c886f3..0000000 --- a/src/test/scala/gs/predicate/v0/json/JsonQueryInTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/json/query/CompiledQueryEvalTests.scala b/src/test/scala/gs/predicate/v0/json/query/CompiledQueryEvalTests.scala index 1963258..149acb3 100644 --- a/src/test/scala/gs/predicate/v0/json/query/CompiledQueryEvalTests.scala +++ b/src/test/scala/gs/predicate/v0/json/query/CompiledQueryEvalTests.scala @@ -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) } diff --git a/src/test/scala/gs/predicate/v0/kv/KeyExistsTests.scala b/src/test/scala/gs/predicate/v0/kv/KeyExistsTests.scala deleted file mode 100644 index 7327ed6..0000000 --- a/src/test/scala/gs/predicate/v0/kv/KeyExistsTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/kv/KeyValueProviderTests.scala b/src/test/scala/gs/predicate/v0/kv/KeyValueProviderTests.scala deleted file mode 100644 index 14fc2d8..0000000 --- a/src/test/scala/gs/predicate/v0/kv/KeyValueProviderTests.scala +++ /dev/null @@ -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) - } diff --git a/src/test/scala/gs/predicate/v0/kv/StringContainsTests.scala b/src/test/scala/gs/predicate/v0/kv/StringContainsTests.scala deleted file mode 100644 index f60776c..0000000 --- a/src/test/scala/gs/predicate/v0/kv/StringContainsTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/kv/StringEndsWithTests.scala b/src/test/scala/gs/predicate/v0/kv/StringEndsWithTests.scala deleted file mode 100644 index b452977..0000000 --- a/src/test/scala/gs/predicate/v0/kv/StringEndsWithTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/kv/StringStartsWithTests.scala b/src/test/scala/gs/predicate/v0/kv/StringStartsWithTests.scala deleted file mode 100644 index 91626a7..0000000 --- a/src/test/scala/gs/predicate/v0/kv/StringStartsWithTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/kv/ValueEqualsTests.scala b/src/test/scala/gs/predicate/v0/kv/ValueEqualsTests.scala deleted file mode 100644 index 9df3238..0000000 --- a/src/test/scala/gs/predicate/v0/kv/ValueEqualsTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/kv/ValueInTests.scala b/src/test/scala/gs/predicate/v0/kv/ValueInTests.scala deleted file mode 100644 index 538df2f..0000000 --- a/src/test/scala/gs/predicate/v0/kv/ValueInTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/kv/ValueNotEqualsTests.scala b/src/test/scala/gs/predicate/v0/kv/ValueNotEqualsTests.scala deleted file mode 100644 index c55c2db..0000000 --- a/src/test/scala/gs/predicate/v0/kv/ValueNotEqualsTests.scala +++ /dev/null @@ -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 diff --git a/src/test/scala/gs/predicate/v0/serde/json/ApiCodecTests.scala b/src/test/scala/gs/predicate/v0/serde/json/ApiCodecTests.scala index 940fb56..058b4e0 100644 --- a/src/test/scala/gs/predicate/v0/serde/json/ApiCodecTests.scala +++ b/src/test/scala/gs/predicate/v0/serde/json/ApiCodecTests.scala @@ -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( diff --git a/src/test/scala/gs/predicate/v0/serde/json/JsonCodecTests.scala b/src/test/scala/gs/predicate/v0/serde/json/JsonCodecTests.scala index 81fdb11..7873393 100644 --- a/src/test/scala/gs/predicate/v0/serde/json/JsonCodecTests.scala +++ b/src/test/scala/gs/predicate/v0/serde/json/JsonCodecTests.scala @@ -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)) ) } diff --git a/src/test/scala/gs/predicate/v0/serde/json/KeyValueCodecTests.scala b/src/test/scala/gs/predicate/v0/serde/json/KeyValueCodecTests.scala deleted file mode 100644 index 2621b3d..0000000 --- a/src/test/scala/gs/predicate/v0/serde/json/KeyValueCodecTests.scala +++ /dev/null @@ -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) - ) - } diff --git a/src/test/scala/gs/predicate/v0/string/StringComparisonTests.scala b/src/test/scala/gs/predicate/v0/string/StringComparisonTests.scala new file mode 100644 index 0000000..4b3a43e --- /dev/null +++ b/src/test/scala/gs/predicate/v0/string/StringComparisonTests.scala @@ -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 -- 2.43.0