Major refactor, lots more content, lots of cleanup.

This commit is contained in:
Pat Garrity 2025-11-22 10:11:20 -06:00
parent 910f14a3c6
commit fae37780e9
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
61 changed files with 1624 additions and 1020 deletions

View file

@ -13,10 +13,6 @@ ThisBuild / licenses := Seq(
"MIT" -> url("https://git.garrity.co/garrity-software/gs-predicate/LICENSE")
)
val noPublishSettings = Seq(
publish := {}
)
val sharedSettings = Seq(
scalaVersion := scala3,
version := semVerSelected.value,
@ -32,13 +28,13 @@ val Deps = new {
val Circe = new {
val Core: ModuleID = "io.circe" %% "circe-core" % "0.14.15"
val Parser: ModuleID = "io.circe" %% "circe-parser" % "0.14.15"
val Generic: ModuleID = "io.circe" %% "circe-generic" % "0.14.15"
val Optics: ModuleID = "io.circe" %% "circe-optics" % "0.15.1"
val Parser: ModuleID = "io.circe" %% "circe-parser" % "0.14.15"
}
val Gs = new {
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.3.3"
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.4.1"
}
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.1.1"
@ -53,86 +49,16 @@ lazy val testSettings = Seq(
lazy val `gs-predicate` = project
.in(file("."))
.aggregate(
`test-support`,
api,
keyValue,
json
)
.settings(noPublishSettings)
.settings(sharedSettings)
.settings(testSettings)
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
/** Internal project used for unit tests.
*/
lazy val `test-support` = project
.in(file("modules/test-support"))
.settings(sharedSettings)
.settings(testSettings)
.settings(noPublishSettings)
.settings(
name := s"${gsProjectName.value}-test-support"
)
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Core,
Deps.Cats.Effect
)
)
/** Core API - Defines fundamental predicates.
*/
lazy val api = project
.in(file("modules/api"))
.dependsOn(`test-support` % "test->test")
.settings(sharedSettings)
.settings(testSettings)
.settings(
name := s"${gsProjectName.value}-api-v${semVerMajor.value}"
)
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Core,
Deps.Gs.Uuid
)
)
/** Key-Value - Defines predicates that can match on string keys and values.
*/
lazy val keyValue = project
.in(file("modules/kv"))
.dependsOn(`test-support` % "test->test")
.dependsOn(api)
.settings(sharedSettings)
.settings(testSettings)
.settings(
name := s"${gsProjectName.value}-kv-v${semVerMajor.value}"
)
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Core,
Deps.Cats.Effect,
Deps.Gs.Uuid
)
)
/** JSON - Defines predicates that can match on JSON values.
*/
lazy val json = project
.in(file("modules/json"))
.dependsOn(`test-support` % "test->test")
.dependsOn(api)
.settings(sharedSettings)
.settings(testSettings)
.settings(
name := s"${gsProjectName.value}-json-v${semVerMajor.value}"
)
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Core,
Deps.Cats.Effect,
Deps.Circe.Core,
Deps.Circe.Parser,
Deps.Circe.Generic,
Deps.Circe.Optics,
Deps.Gs.Uuid
Deps.Circe.Parser
)
)

View file

@ -1,41 +0,0 @@
package gs.predicate.v0.api
import cats.Applicative
import cats.syntax.all.*
import gs.uuid.v0.UUID
/** Implements logical AND.
*
* @param ps
* The predicates to evaluate.
*/
final class And[F[_]: Applicative, -A](
val id: UUID,
private val ps: List[Predicate[F, A]]
) extends Predicate[F, A]:
/** @inheritDocs
*/
override def eval(input: A): F[Predicate.Result] =
ps.map(_.eval(input)).sequence.map(_.allMatch())
object And:
def empty[F[_]: Applicative]: Predicate[F, Any] = False[F]
def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Predicate[F, A] =
ps.toList match
case Nil => False[F]
case p :: Nil => p
case list => new And(UUID.v7(), list)
def apply[F[_]: Applicative, A](
id: UUID,
ps: Predicate[F, A]*
): Predicate[F, A] =
ps.toList match
case Nil => False[F]
case p :: Nil => p
case list => new And(id, list)
end And

View file

@ -1,23 +0,0 @@
package gs.predicate.v0.api
import cats.Applicative
import gs.uuid.v0.UUID
/** Always returns a miss.
*/
final class False[F[_]: Applicative](
val id: UUID
) extends Predicate[F, Any]:
/** @inheritDocs
*/
override def eval(input: Any): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.missed())
object False:
def apply[F[_]: Applicative]: False[F] = new False[F](UUID.v7())
def apply[F[_]: Applicative](id: UUID): False[F] = new False[F](id)
end False

View file

@ -1,41 +0,0 @@
package gs.predicate.v0.api
import cats.Applicative
import cats.syntax.all.*
import gs.uuid.v0.UUID
/** Implements logical OR.
*
* @param ps
* The predicates to evaluate.
*/
final class Or[F[_]: Applicative, -A](
val id: UUID,
private val ps: List[Predicate[F, A]]
) extends Predicate[F, A]:
/** @inheritDocs
*/
override def eval(input: A): F[Predicate.Result] =
ps.map(_.eval(input)).sequence.map(_.anyMatch())
object Or:
def empty[F[_]: Applicative]: Predicate[F, Any] = False[F]
def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Predicate[F, A] =
ps.toList match
case Nil => False[F]
case p :: Nil => p
case list => new Or(UUID.v7(), list)
def apply[F[_]: Applicative, A](
id: UUID,
ps: Predicate[F, A]*
): Predicate[F, A] =
ps.toList match
case Nil => False[F]
case p :: Nil => p
case list => new Or(id, list)
end Or

View file

@ -1,23 +0,0 @@
package gs.predicate.v0.api
import cats.Applicative
import gs.uuid.v0.UUID
/** Always returns a match.
*/
final class True[F[_]: Applicative](
val id: UUID
) extends Predicate[F, Any]:
/** @inheritDocs
*/
override def eval(input: Any): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.matched())
object True:
def apply[F[_]: Applicative]: True[F] = new True[F](UUID.v7())
def apply[F[_]: Applicative](id: UUID): True[F] = new True[F](id)
end True

View file

@ -1,35 +0,0 @@
package gs.predicate.v0.api
import cats.effect.IO
import gs.uuid.v0.UUID
import support.IOSuite
class FalseTests extends IOSuite:
iotest("should return false") {
val predicate = False[IO]
for result <- predicate.eval(())
yield assertEquals(result.unwrap(), false)
}
iotest("should provide a unique identifier") {
val id = UUID.v7()
val predicate = False[IO](id)
for result <- predicate.eval(())
yield
assertEquals(result.unwrap(), false)
assertEquals(predicate.id, id)
assertEquals(predicate.hashCode(), id.hashCode())
assertEquals(predicate.toString(), s"predicate:${id.withoutDashes()}")
}
test("should support equality") {
val predicate = False[IO]
val p2 = False[IO](predicate.id)
val p3 = False[IO]
assertEquals(predicate, predicate)
assertEquals(predicate, p2)
assertNotEquals(predicate, p3)
}

View file

@ -1,17 +0,0 @@
package gs.predicate.v0.api
import munit.FunSuite
class PredicateTests extends FunSuite:
test("should not equal non-predicate objects") {
val x = "other"
val p = Predicate.alwaysTrue[cats.Id]
assertEquals(p.equals(x), false)
assertNotEquals(p, null)
}
test("should provide a default toString implementation") {
val p = Predicate.alwaysFalse[cats.Id]
assertEquals(p.toString(), s"predicate:${p.id.withoutDashes()}")
}

View file

@ -1,35 +0,0 @@
package gs.predicate.v0.api
import cats.effect.IO
import gs.uuid.v0.UUID
import support.IOSuite
class TrueTests extends IOSuite:
iotest("should return true") {
val predicate = True[IO]
for result <- predicate.eval(())
yield assertEquals(result.unwrap(), true)
}
iotest("should provide a unique identifier") {
val id = UUID.v7()
val predicate = True[IO](id)
for result <- predicate.eval(())
yield
assertEquals(result.unwrap(), true)
assertEquals(predicate.id, id)
assertEquals(predicate.hashCode(), id.hashCode())
assertEquals(predicate.toString(), s"predicate:${id.withoutDashes()}")
}
test("should support equality") {
val predicate = True[IO]
val p2 = True[IO](predicate.id)
val p3 = True[IO]
assertEquals(predicate, predicate)
assertEquals(predicate, p2)
assertNotEquals(predicate, p3)
}

View file

@ -1,31 +0,0 @@
package gs.predicate.v0.json
import cats.Applicative
import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID
import io.circe.Json
/** Predicate that matches if JSON blob is an object that contains the given
* key.
*
* @param id
* The unique identifier of this [[Predicate]].
* @param key
* The key that should exist.
*/
final class JsonKeyExists[F[_]: Applicative](
val id: UUID,
val queryString: String
) extends Predicate[F, Json]:
/** @inheritDocs
*/
override def eval(input: Json): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result(false))
object JsonKeyExists:
def apply[F[_]: Applicative](key: String): JsonKeyExists[F] =
new JsonKeyExists[F](UUID.v7(), key)
end JsonKeyExists

View file

@ -1,30 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID
/** 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, K, V](
val id: UUID,
val key: K
) extends Predicate[F, KeyValueProvider[F, K, V]]:
/** @inheritDocs
*/
override def eval(input: KeyValueProvider[F, K, V]): F[Predicate.Result] =
input.exists(key).map(Predicate.Result.apply)
object KeyExists:
def apply[F[_]: Functor, K, V](key: K): KeyExists[F, K, V] =
new KeyExists[F, K, V](UUID.v7(), key)
end KeyExists

View file

@ -1,6 +0,0 @@
package gs.predicate.v0.kv
/** Type alias for a [[KeyValueProvider]] that associates string values with
* string keys.
*/
type KeyValueStringProvider[F[_]] = KeyValueProvider[F, String, String]

View file

@ -1,42 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID
/** Predicate that matches if some (string-valued) [[KeyValueProvider]] contains
* the given key, and the string value associated with that key contains some
* other string.
*
* @param id
* The unique identifier of this [[Predicate]].
* @param key
* The key that should exist.
* @param containedValue
* The substring that must be contained in the value.
*/
final class StringContains[F[_]: Functor, K](
val id: UUID,
val key: K,
val containedValue: String
) extends Predicate[F, KeyValueProvider[F, K, String]]:
/** @inheritDocs
*/
override def eval(input: KeyValueProvider[F, K, String])
: F[Predicate.Result] =
input.get(key).map {
case Some(value) => Predicate.Result(value.contains(containedValue))
case _ => Predicate.Result.missed()
}
object StringContains:
def apply[F[_]: Functor, K](
key: K,
containedValue: String
): StringContains[F, K] =
new StringContains[F, K](UUID.v7(), key, containedValue)
end StringContains

View file

@ -1,42 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID
/** 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 StringEndsWith[F[_]: Functor, K](
val id: UUID,
val key: K,
val suffix: String
) extends Predicate[F, KeyValueProvider[F, K, String]]:
/** @inheritDocs
*/
override def eval(input: KeyValueProvider[F, K, String])
: F[Predicate.Result] =
input.get(key).map {
case Some(value) => Predicate.Result(value.endsWith(suffix))
case _ => Predicate.Result.missed()
}
object StringEndsWith:
def apply[F[_]: Functor, K](
key: K,
suffix: String
): StringEndsWith[F, K] =
new StringEndsWith[F, K](UUID.v7(), key, suffix)
end StringEndsWith

View file

@ -1,42 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID
/** Predicate that matches if some (string-valued) [[KeyValueProvider]] contains
* the given key, and the string value associated with that key starts with
* some other string.
*
* @param id
* The unique identifier of this [[Predicate]].
* @param key
* The key that should exist.
* @param prefix
* The substring that must be the prefix of the value.
*/
final class StringStartsWith[F[_]: Functor, K](
val id: UUID,
val key: K,
val prefix: String
) extends Predicate[F, KeyValueProvider[F, K, String]]:
/** @inheritDocs
*/
override def eval(input: KeyValueProvider[F, K, String])
: F[Predicate.Result] =
input.get(key).map {
case Some(value) => Predicate.Result(value.startsWith(prefix))
case _ => Predicate.Result.missed()
}
object StringStartsWith:
def apply[F[_]: Functor, K](
key: K,
prefix: String
): StringStartsWith[F, K] =
new StringStartsWith[F, K](UUID.v7(), key, prefix)
end StringStartsWith

View file

@ -1,46 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID
/** Predicate that matches if some [[KeyValueProvider]] contains the given key,
* and the value associated with that key equals the given value.
*
* @param id
* The unique identifier of this [[Predicate]].
* @param key
* The key that should exist.
* @param value
* The value that must be associated with the key.
*/
final class ValueEquals[F[_]: Functor, K, V](
val id: UUID,
val key: K,
val expectedValue: V
)(
using
CanEqual[V, V]
) extends Predicate[F, KeyValueProvider[F, K, V]]:
/** @inheritDocs
*/
override def eval(input: KeyValueProvider[F, K, V]): F[Predicate.Result] =
input.get(key).map {
case Some(value) => Predicate.Result(value == expectedValue)
case _ => Predicate.Result.missed()
}
object ValueEquals:
def apply[F[_]: Functor, K, V](
key: K,
value: V
)(
using
CanEqual[V, V]
): ValueEquals[F, K, V] =
new ValueEquals[F, K, V](UUID.v7(), key, value)
end ValueEquals

View file

@ -1,46 +0,0 @@
package gs.predicate.v0.kv
import cats.Functor
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID
/** 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, K, V](
val id: UUID,
val key: K,
val value: V
)(
using
CanEqual[V, V]
) extends Predicate[F, KeyValueProvider[F, K, V]]:
/** @inheritDocs
*/
override def eval(input: KeyValueProvider[F, K, V]): F[Predicate.Result] =
input.get(key).map {
case Some(v) => Predicate.Result(v != value)
case _ => Predicate.Result.missed()
}
object ValueNotEquals:
def apply[F[_]: Functor, K, V](
key: K,
value: V
)(
using
CanEqual[V, V]
): ValueNotEquals[F, K, V] =
new ValueNotEquals[F, K, V](UUID.v7(), key, value)
end ValueNotEquals

View file

@ -1,84 +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") {
val p1 = StringContains[IO, String](Data.PassingKey, Data.Substring1)
val p2 = StringContains[IO, String](Data.PassingKey, Data.Substring2)
val p3 = StringContains[IO, String](Data.PassingKey, Data.Substring3)
for
provider <- StringContainsTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
r3 <- p3.eval(provider)
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") {
val p = StringContains[IO, String](Data.NotExistingKey, "")
for
provider <- StringContainsTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
yield assertEquals(result, Predicate.Result.missed())
}
iotest("should match if an empty substring is provided") {
val p1 = StringContains[IO, String](Data.EmptyStringKey, "")
val p2 = StringContains[IO, String](Data.PassingKey, "")
for
provider <- StringContainsTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
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") {
val p = StringContains[IO, String](Data.PassingKey, Substring1.reverse)
for
provider <- StringContainsTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
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[KeyValueStringProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapStringProvider(map)
end StringContainsTests

View file

@ -1,93 +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 StringEndsWithTests extends IOSuite:
import StringEndsWithTests.Data
iotest("should find a string as a prefix of the input") {
val p1 = StringEndsWith[IO, String](Data.PassingKey, Data.Substring1)
val p2 = StringEndsWith[IO, String](Data.PassingKey, Data.Substring2)
val p3 = StringEndsWith[IO, String](Data.PassingKey, Data.Substring3)
for
provider <- StringEndsWithTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
r3 <- p3.eval(provider)
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") {
val p = StringEndsWith[IO, String](Data.NotExistingKey, "")
for
provider <- StringEndsWithTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
yield assertEquals(result, Predicate.Result.missed())
}
iotest("should match if an empty substring is provided") {
val p1 = StringEndsWith[IO, String](Data.EmptyStringKey, "")
val p2 = StringEndsWith[IO, String](Data.PassingKey, "")
for
provider <- StringEndsWithTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
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"
) {
val p1 = StringEndsWith[IO, String](Data.PassingKey, Data.Substring3 + "z")
val p2 =
StringEndsWith[IO, String](Data.PassingKey, Data.Substring3.reverse)
val p3 =
StringEndsWith[IO, String](Data.PassingKey, Data.Substring2.reverse)
for
provider <- StringEndsWithTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
r3 <- p3.eval(provider)
yield
assertEquals(r1, Predicate.Result.missed())
assertEquals(r2, Predicate.Result.missed())
assertEquals(r3, Predicate.Result.missed())
}
object StringEndsWithTests:
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[KeyValueStringProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapStringProvider(map)
end StringEndsWithTests

View file

@ -1,94 +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 StringStartsWithTests extends IOSuite:
import StringStartsWithTests.Data
iotest("should find a string as a prefix of the input") {
val p1 = StringStartsWith[IO, String](Data.PassingKey, Data.Substring1)
val p2 = StringStartsWith[IO, String](Data.PassingKey, Data.Substring2)
val p3 = StringStartsWith[IO, String](Data.PassingKey, Data.Substring3)
for
provider <- StringStartsWithTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
r3 <- p3.eval(provider)
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") {
val p = StringStartsWith[IO, String](Data.NotExistingKey, "")
for
provider <- StringStartsWithTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
yield assertEquals(result, Predicate.Result.missed())
}
iotest("should match if an empty substring is provided") {
val p1 = StringStartsWith[IO, String](Data.EmptyStringKey, "")
val p2 = StringStartsWith[IO, String](Data.PassingKey, "")
for
provider <- StringStartsWithTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
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"
) {
val p1 =
StringStartsWith[IO, String](Data.PassingKey, Data.Substring3 + "k")
val p2 =
StringStartsWith[IO, String](Data.PassingKey, Data.Substring3.reverse)
val p3 =
StringStartsWith[IO, String](Data.PassingKey, Data.Substring2.reverse)
for
provider <- StringStartsWithTests.newProvider(Data.KeyValues)
r1 <- p1.eval(provider)
r2 <- p2.eval(provider)
r3 <- p3.eval(provider)
yield
assertEquals(r1, Predicate.Result.missed())
assertEquals(r2, Predicate.Result.missed())
assertEquals(r3, Predicate.Result.missed())
}
object StringStartsWithTests:
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[KeyValueStringProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapStringProvider(map)
end StringStartsWithTests

View file

@ -1,59 +0,0 @@
package gs.predicate.v0.kv
import cats.effect.IO
import cats.effect.std.MapRef
import gs.datagen.v0.Gen
import gs.datagen.v0.generators.Size
import gs.predicate.v0.api.Predicate
import support.IOSuite
class ValueNotEqualsTests extends IOSuite:
import ValueNotEqualsTests.Data
iotest("should NOT find an exact match against some value") {
val p =
ValueNotEquals[IO, String, String](Data.ExistingKey, Data.ExistingValue)
for
provider <- ValueNotEqualsTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
yield assertEquals(result, Predicate.Result.missed())
}
iotest("should match a value if that value is not equal to the target") {
val p =
ValueNotEquals[IO, String, String](
Data.ExistingKey,
Data.NotExistingValue
)
for
provider <- ValueNotEqualsTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
yield assertEquals(result, Predicate.Result.matched())
}
iotest("should not find a key that does not exist within some provider") {
val p = ValueNotEquals[IO, String, String](Data.NotExistingKey, "")
for
provider <- ValueNotEqualsTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
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[KeyValueStringProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapStringProvider(map)
end ValueNotEqualsTests

View file

@ -0,0 +1,80 @@
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
import io.circe.Encoder
import io.circe.HCursor
import io.circe.Json
import io.circe.syntax._
/** Implements logical AND.
*
* @param ps
* The predicates to evaluate.
*/
final class And[F[_]: Applicative](
val ps: List[Predicate[F]]
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = And.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
ps.map(_.eval()).sequence.map(_.allMatch())
object And:
final val PredicateType: String = "and"
def empty[F[_]: Applicative]: Predicate[F] = False[F]
def apply[F[_]: Applicative, A](ps: Predicate[F]*): Predicate[F] =
ps.toList match
case Nil => False[F]
case p :: Nil => p
case list => new And(list)
given andEncoder[F[_]](
using
Encoder[Predicate[F]]
): Encoder[And[F]] =
Encoder.instance[And[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.predicates, p.ps.asJson)
)
}
private def consumeCursor[F[_]: Applicative](
cursor: HCursor
)(
using
Decoder[Predicate[F]]
): Either[DecodingFailure, And[F]] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
yield new And(ps)
given andDecoder[F[_]: Applicative](
using
Decoder[Predicate[F]]
): Decoder[And[F]] =
Decoder.instance[And[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType => consumeCursor(cursor)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end And

View file

@ -0,0 +1,50 @@
package gs.predicate.v0.api
import cats.Applicative
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
/** Always returns a miss.
*/
final class False[F[_]: Applicative] extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = False.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.missed())
end False
object False:
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 =>
Json.obj((JsonKeys.predicateType, Json.fromString(p.predicateType)))
}
given falseDecoder[F[_]: Applicative]: Decoder[False[F]] =
Decoder.instance[False[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType => Right(False[F])
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end False

View file

@ -0,0 +1,11 @@
package gs.predicate.v0.api
object Messages:
def invalidPredicateType(
candidate: String,
expected: String
): String =
s"Received predicate type '$candidate' but expected '$expected'."
end Messages

View file

@ -0,0 +1,72 @@
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
import io.circe.Encoder
import io.circe.Json
import io.circe.syntax.*
/** Implements logical OR.
*
* @param ps
* The predicates to evaluate.
*/
final class Or[F[_]: Applicative](
val ps: List[Predicate[F]]
) extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = Or.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
ps.map(_.eval()).sequence.map(_.anyMatch())
object Or:
final val PredicateType: String = "or"
def empty[F[_]: Applicative]: Predicate[F] = False[F]
def apply[F[_]: Applicative, A](ps: Predicate[F]*): Predicate[F] =
ps.toList match
case Nil => False[F]
case p :: Nil => p
case list => new Or(list)
given orEncoder[F[_]](
using
Encoder[Predicate[F]]
): Encoder[Or[F]] =
Encoder.instance[Or[F]] { p =>
Json.obj(
(JsonKeys.predicateType, Json.fromString(PredicateType)),
(JsonKeys.predicates, p.ps.asJson)
)
}
given orDecoder[F[_]: Applicative](
using
Decoder[Predicate[F]]
): Decoder[Or[F]] =
Decoder.instance[Or[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType =>
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
yield new Or(ps)
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end Or

View file

@ -1,59 +1,34 @@
package gs.predicate.v0.api
import cats.Applicative
import gs.uuid.v0.UUID
/** A _Predicate_ is some function that accepts any input and emits some
* [[Predicate.Result]] (whether the predicate matched).
*
* Predicates evaluate input to see if the predicate matches that input.
* Predicates evaluate input extracted from context to see if the predicate
* matches that input.
*/
trait Predicate[F[_], -A]:
trait Predicate[F[_]]:
/** @return
* The unique identifier of this Predicate.
* The serializable predicate type.
*/
def id: UUID
def predicateType: String
/** Evaluate this predicate against the given input.
*
* @param input
* The input to evaluate this predicate against.
* @return
* Some [[Predicate.Result]] that describes whether the input matched the
* predicate.
*/
def eval(input: A): F[Predicate.Result]
def eval(): F[Predicate.Result]
/** Predicate equality is based on the _unique identifier_. Two predicates
* with the same ID are considered equal.
*
* @param that
* The other object.
* @return
* True if the other object is a predicate with the same ID, false
* otherwise.
*/
override def equals(that: Any): Boolean =
that match
case p: Predicate[?, ?] => p.id == id
case _ => false
/** @return
* The hash code of the unique identifier.
*/
override def hashCode(): Int = id.hashCode()
/** @inheritDocs
*/
override def toString(): String = s"predicate:${id.withoutDashes()}"
end Predicate
object Predicate:
given CanEqual[Predicate[?, ?], Predicate[?, ?]] = CanEqual.derived
def alwaysTrue[F[_]: Applicative]: Predicate[F] = True[F]
def alwaysTrue[F[_]: Applicative]: Predicate[F, Any] = True[F]
def alwaysFalse[F[_]: Applicative]: Predicate[F, Any] = False[F]
def alwaysFalse[F[_]: Applicative]: Predicate[F] = False[F]
/** The result of evaluating a [[Predicate]] is a Boolean value where:
*
@ -132,3 +107,5 @@ object Predicate:
def isMiss: Boolean = !result
end Result
end Predicate

View file

@ -0,0 +1,50 @@
package gs.predicate.v0.api
import cats.Applicative
import gs.predicate.v0.serde.json.JsonKeys
import io.circe.Decoder
import io.circe.DecodingFailure
import io.circe.Encoder
import io.circe.Json
/** Always returns a match.
*/
final class True[F[_]: Applicative] extends Predicate[F]:
/** @inheritDocs
*/
override def predicateType: String = True.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.matched())
end True
object True:
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 =>
Json.obj((JsonKeys.predicateType, Json.fromString(p.predicateType)))
}
given trueDecoder[F[_]: Applicative]: Decoder[True[F]] =
Decoder.instance[True[F]] { cursor =>
cursor.downField(JsonKeys.predicateType).as[String].flatMap {
case PredicateType => Right(True[F])
case candidate =>
Left(
DecodingFailure(
Messages.invalidPredicateType(candidate, PredicateType),
Nil
)
)
}
}
end True

View file

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

View file

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

View file

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

View file

@ -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 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)
end JsonQueryEquals

View file

@ -0,0 +1,41 @@
package gs.predicate.v0.json
import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.json.query.JsonQuery
/** Predicate that matches if the JSON provider contains a JSON blob with the
* given key, and that blob contains the given query.
*
* @param key
* The name of the JSON value that must satisfy the given query.
*/
final class JsonQueryExists[F[_]: Applicative: JsonProvider](
val key: String,
val query: JsonQuery
) extends Predicate[F]:
/** @inheritDocs
*/
final override val predicateType: String = JsonQueryExists.PredicateType
/** @inheritDocs
*/
override def eval(): F[Predicate.Result] =
JsonProvider[F].get(key).map {
case None => Predicate.Result.missed()
case Some(json) => Predicate.Result(query.eval(json, _ => true))
}
object JsonQueryExists:
final val PredicateType: String = "json-exists"
def apply[F[_]: Applicative: JsonProvider](
key: String,
query: JsonQuery
): JsonQueryExists[F] =
new JsonQueryExists[F](key, query)
end JsonQueryExists

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import cats.Applicative
/** Interface for anything that can fetch values for stored keys.
*/
trait KeyValueProvider[F[_], -K, V]:
trait KeyValueProvider[F[_]]:
/** Determine if some key exists.
*
* @param key
@ -12,7 +12,7 @@ trait KeyValueProvider[F[_], -K, V]:
* @return
* True if the key exists, false otherwise.
*/
def exists(key: K): F[Boolean]
def exists(key: String): F[Boolean]
/** Get the value associated with some key.
*
@ -21,24 +21,29 @@ trait KeyValueProvider[F[_], -K, V]:
* @return
* The value stored for the key, or `None` if no such value exists.
*/
def get(key: K): F[Option[V]]
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, Any, Any] = new Noop[F]
def noop[F[_]: Applicative]: KeyValueProvider[F] = new Noop[F]
/** No-op implementation that never contains data.
*/
final class Noop[F[_]: Applicative] extends KeyValueProvider[F, Any, Any]:
final class Noop[F[_]: Applicative] extends KeyValueProvider[F]:
/** @inheritDocs
*/
override def exists(key: Any): F[Boolean] = Applicative[F].pure(false)
override def exists(key: String): F[Boolean] = Applicative[F].pure(false)
/** @inheritDocs
*/
override def get(key: Any): F[Option[Any]] = Applicative[F].pure(None)
override def get(key: String): F[Option[String]] = Applicative[F].pure(None)
end KeyValueProvider

View file

@ -9,9 +9,9 @@ import cats.syntax.all.*
* @param map
* The underlying map.
*/
final class MemoryMapStringProvider[F[_]: Sync](
final class MemoryMapKeyValueProvider[F[_]: Sync](
private val map: MapRef[F, String, Option[String]]
) extends KeyValueStringProvider[F]:
) extends KeyValueProvider[F]:
/** @inheritDocs
*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,27 @@
package gs.predicate.v0.serde.json
/** Standard keys for JSON serialization and deserialization.
*/
object JsonKeys:
/** Designates the type of predicate.
*/
val predicateType: String = "predicateType"
/** Used to collect contained predicates for composites such as AND and OR.
*/
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"
/** Used to represent any collection of values.
*/
val values: String = "values"
end JsonKeys

View file

@ -0,0 +1,127 @@
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.Or
import gs.predicate.v0.api.Predicate
import gs.predicate.v0.api.True
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.ValueStartsWith
import io.circe.*
import io.circe.syntax.*
/** Given some [[And]] predicate, encode it as a JSON blob.
*
* @param and
* The [[And]] predicate.
* @return
* The JSON blob representing the [[And]].
*/
def encodeAnd[F[_]](and: And[F]): Json =
Json.obj(
(JsonKeys.predicateType, Json.fromString(And.PredicateType)),
(JsonKeys.predicates, and.ps.asJson)
)
/** Given some [[Or]] predicate, encode it as a JSON blob.
*
* @param and
* The [[Or]] predicate.
* @return
* The JSON blob representing the [[Or]].
*/
def encodeOr[F[_]](and: Or[F]): Json =
Json.obj(
(JsonKeys.predicateType, Json.fromString(Or.PredicateType)),
(JsonKeys.predicates, and.ps.asJson)
)
/** @return
* Generic encoder for any [[Predicate]].
*/
given predicateEncoder[F[_]]: Encoder[Predicate[F]] =
Encoder.instance {
case p: True[F] => Encoder[True[F]].apply(p)
case p: False[F] => Encoder[False[F]].apply(p)
case p: And[F] => encodeAnd[F](p)
case p: Or[F] => encodeOr[F](p)
case p: KeyExists[F] => Encoder[KeyExists[F]].apply(p)
case p: ValueEquals[F] => Encoder[ValueEquals[F]].apply(p)
case p: ValueContains[F] => Encoder[ValueContains[F]].apply(p)
case p: ValueStartsWith[F] => Encoder[ValueStartsWith[F]].apply(p)
case p: ValueEndsWith[F] => Encoder[ValueEndsWith[F]].apply(p)
case p =>
throw new IllegalArgumentException(
s"Unsupported predicate type: ${p.predicateType}"
)
}
/** Given some JSON cursor, decode a logical [[And]] predicate.
*
* TODO: Recursive depth limitations.
*
* @param cursor
* The cursor that points to some JSON value.
* @return
* The [[And]] predicate or a decoding failure.
*/
def decodeAnd[F[_]: Applicative: KeyValueProvider](cursor: HCursor)
: Either[DecodingFailure, And[F]] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
yield new And(ps)
/** Given some JSON cursor, decode a logical [[Or]] predicate.
*
* TODO: Recursive depth limitations.
*
* @param cursor
* The cursor that points to some JSON value.
* @return
* The [[Or]] predicate or a decoding failure.
*/
def decodeOr[F[_]: Applicative: KeyValueProvider](cursor: HCursor)
: Either[DecodingFailure, Or[F]] =
for ps <- cursor.downField(JsonKeys.predicates).as[List[Predicate[F]]]
yield new Or(ps)
/** Given some JSON cursor, decode any known [[Predicate]].
*
* @param cursor
* The cursor that points to some JSON value.
* @return
* The decoded [[Predicate]] or some decoding failure.
*/
def decodePredicate[F[_]: Applicative: KeyValueProvider](cursor: HCursor)
: Either[DecodingFailure, Predicate[F]] =
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 ValueContains.PredicateType =>
Decoder[ValueContains[F]].apply(cursor)
case ValueStartsWith.PredicateType =>
Decoder[ValueStartsWith[F]].apply(cursor)
case ValueEndsWith.PredicateType =>
Decoder[ValueEndsWith[F]].apply(cursor)
case predicateType =>
Left(
DecodingFailure(s"Unrecognized predicate type: '$predicateType'", Nil)
)
yield predicate
/** @return
* Generic decoder for any [[Predicate]].
*/
given predicateDecoder[F[_]: Applicative: KeyValueProvider]
: Decoder[Predicate[F]] =
Decoder.instance[Predicate[F]](cursor => decodePredicate[F](cursor))

View file

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

View file

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

View file

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

View file

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

View file

@ -12,22 +12,24 @@ class KeyExistsTests extends IOSuite:
import KeyExistsTests.Data
iotest("should find a key that exists within some provider") {
val p = KeyExists[IO, String, String](Data.ExistingKey)
for
provider <- KeyExistsTests.newProvider(Data.KeyValues)
result <- p.eval(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") {
val p = KeyExists[IO, String, String](Data.NotExistingKey)
for
provider <- KeyExistsTests.newProvider(Data.KeyValues)
result <- p.eval(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:
@ -40,8 +42,8 @@ object KeyExistsTests:
end Data
def newProvider(data: Map[String, String]): IO[KeyValueStringProvider[IO]] =
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapStringProvider(map)
yield new MemoryMapKeyValueProvider(map)
end KeyExistsTests

View file

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

View file

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

View file

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

View file

@ -12,30 +12,33 @@ 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, String, String](Data.ExistingKey, Data.ExistingValue)
for
provider <- ValueEqualsTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
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, String, String](Data.ExistingKey, Data.NotExistingValue)
for
provider <- ValueEqualsTests.newProvider(Data.KeyValues)
result <- p.eval(provider)
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") {
val p = ValueEquals[IO, String, String](Data.NotExistingKey, "")
for
provider <- ValueEqualsTests.newProvider(Data.KeyValues)
result <- p.eval(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:
@ -49,8 +52,8 @@ object ValueEqualsTests:
end Data
def newProvider(data: Map[String, String]): IO[KeyValueStringProvider[IO]] =
def newProvider(data: Map[String, String]): IO[KeyValueProvider[IO]] =
for map <- MapRef.ofSingleImmutableMap[IO, String, String](data)
yield new MemoryMapStringProvider(map)
yield new MemoryMapKeyValueProvider(map)
end ValueEqualsTests

View file

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