diff --git a/src/main/scala/gs/predicate/v0/json/JsonQueryIn.scala b/src/main/scala/gs/predicate/v0/json/JsonQueryIn.scala new file mode 100644 index 0000000..46d93fd --- /dev/null +++ b/src/main/scala/gs/predicate/v0/json/JsonQueryIn.scala @@ -0,0 +1,49 @@ +package gs.predicate.v0.json + +import cats.Applicative +import cats.syntax.all.* +import gs.predicate.v0.api.Predicate +import gs.predicate.v0.json.query.JsonQuery +import io.circe.Json + +/** Predicate that matches if the JSON provider contains a JSON blob with the + * given key, and that blob contains the given query, and the result of the + * query matches 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) + +end JsonQueryIn diff --git a/src/main/scala/gs/predicate/v0/json/MemoryMapJsonProvider.scala b/src/main/scala/gs/predicate/v0/json/MemoryMapJsonProvider.scala new file mode 100644 index 0000000..92a3aae --- /dev/null +++ b/src/main/scala/gs/predicate/v0/json/MemoryMapJsonProvider.scala @@ -0,0 +1,20 @@ +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/test/scala/gs/predicate/v0/json/JsonQueryEqualsTests.scala b/src/test/scala/gs/predicate/v0/json/JsonQueryEqualsTests.scala new file mode 100644 index 0000000..aece2b9 --- /dev/null +++ b/src/test/scala/gs/predicate/v0/json/JsonQueryEqualsTests.scala @@ -0,0 +1,128 @@ +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/JsonQueryInTests.scala b/src/test/scala/gs/predicate/v0/json/JsonQueryInTests.scala new file mode 100644 index 0000000..8c886f3 --- /dev/null +++ b/src/test/scala/gs/predicate/v0/json/JsonQueryInTests.scala @@ -0,0 +1,68 @@ +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/kv/ValueInTests.scala b/src/test/scala/gs/predicate/v0/kv/ValueInTests.scala new file mode 100644 index 0000000..538df2f --- /dev/null +++ b/src/test/scala/gs/predicate/v0/kv/ValueInTests.scala @@ -0,0 +1,66 @@ +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