Full coverage for key-value.

This commit is contained in:
Pat Garrity 2025-11-05 21:56:13 -06:00
parent ec880797f1
commit 163957ad27
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
9 changed files with 395 additions and 11 deletions

View file

@ -1,6 +1,6 @@
package gs.predicate.v0.kv package gs.predicate.v0.kv
import cats.effect.Sync import cats.Functor
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
@ -16,7 +16,7 @@ import gs.uuid.v0.UUID
* @param containedValue * @param containedValue
* The substring that must be contained in the value. * The substring that must be contained in the value.
*/ */
final class StringContains[F[_]: Sync, K]( final class StringContains[F[_]: Functor, K](
val id: UUID, val id: UUID,
val key: K, val key: K,
val containedValue: String val containedValue: String
@ -30,3 +30,13 @@ final class StringContains[F[_]: Sync, K](
case Some(value) => Predicate.Result(value.contains(containedValue)) case Some(value) => Predicate.Result(value.contains(containedValue))
case _ => Predicate.Result.missed() 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,6 +1,6 @@
package gs.predicate.v0.kv package gs.predicate.v0.kv
import cats.effect.Sync import cats.Functor
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
@ -16,7 +16,7 @@ import gs.uuid.v0.UUID
* @param suffix * @param suffix
* The substring that must be the suffix of the value. * The substring that must be the suffix of the value.
*/ */
final class StringEndsWith[F[_]: Sync, K]( final class StringEndsWith[F[_]: Functor, K](
val id: UUID, val id: UUID,
val key: K, val key: K,
val suffix: String val suffix: String
@ -30,3 +30,13 @@ final class StringEndsWith[F[_]: Sync, K](
case Some(value) => Predicate.Result(value.endsWith(suffix)) case Some(value) => Predicate.Result(value.endsWith(suffix))
case _ => Predicate.Result.missed() 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,6 +1,6 @@
package gs.predicate.v0.kv package gs.predicate.v0.kv
import cats.effect.Sync import cats.Functor
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
@ -16,7 +16,7 @@ import gs.uuid.v0.UUID
* @param prefix * @param prefix
* The substring that must be the prefix of the value. * The substring that must be the prefix of the value.
*/ */
final class StringStartsWith[F[_]: Sync, K]( final class StringStartsWith[F[_]: Functor, K](
val id: UUID, val id: UUID,
val key: K, val key: K,
val prefix: String val prefix: String
@ -30,3 +30,13 @@ final class StringStartsWith[F[_]: Sync, K](
case Some(value) => Predicate.Result(value.startsWith(prefix)) case Some(value) => Predicate.Result(value.startsWith(prefix))
case _ => Predicate.Result.missed() 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,6 +1,6 @@
package gs.predicate.v0.kv package gs.predicate.v0.kv
import cats.effect.Sync import cats.Functor
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Predicate import gs.predicate.v0.api.Predicate
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
@ -15,10 +15,10 @@ import gs.uuid.v0.UUID
* @param value * @param value
* The value that must not be associated with the key. * The value that must not be associated with the key.
*/ */
final class ValueNotEquals[F[_]: Sync, K, V]( final class ValueNotEquals[F[_]: Functor, K, V](
val id: UUID, val id: UUID,
val key: K, val key: K,
val expectedValue: V val value: V
)( )(
using using
CanEqual[V, V] CanEqual[V, V]
@ -28,6 +28,19 @@ final class ValueNotEquals[F[_]: Sync, K, V](
*/ */
override def eval(input: KeyValueProvider[F, K, V]): F[Predicate.Result] = override def eval(input: KeyValueProvider[F, K, V]): F[Predicate.Result] =
input.get(key).map { input.get(key).map {
case Some(value) => Predicate.Result(value != expectedValue) case Some(v) => Predicate.Result(v != value)
case _ => Predicate.Result.missed() 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

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

View file

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

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

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

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