From ed83e305180b0767ace57a9cdf660c1a73d6d228 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Mon, 3 Nov 2025 22:15:10 -0600 Subject: [PATCH] Working on the key/value module --- build.sbt | 23 +++++++++- .../scala/gs/predicate/v0/kv/KeyExists.scala | 23 ++++++++++ .../gs/predicate/v0/kv/KeyValueProvider.scala | 44 +++++++++++++++++++ .../v0/kv/KeyValueStringProvider.scala | 6 +++ .../v0/kv/MemoryMapStringProvider.scala | 24 ++++++++++ .../gs/predicate/v0/kv/StringContains.scala | 32 ++++++++++++++ .../gs/predicate/v0/kv/StringEndsWith.scala | 32 ++++++++++++++ .../gs/predicate/v0/kv/StringStartsWith.scala | 32 ++++++++++++++ .../gs/predicate/v0/kv/ValueEquals.scala | 33 ++++++++++++++ .../gs/predicate/v0/kv/ValueNotEquals.scala | 33 ++++++++++++++ 10 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/KeyExists.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueStringProvider.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/MemoryMapStringProvider.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/StringContains.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/StringEndsWith.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/StringStartsWith.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/ValueEquals.scala create mode 100644 modules/kv/src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala diff --git a/build.sbt b/build.sbt index 1f0536c..541e739 100644 --- a/build.sbt +++ b/build.sbt @@ -49,7 +49,8 @@ lazy val `gs-predicate` = project .in(file(".")) .aggregate( `test-support`, - api + api, + keyValue ) .settings(noPublishSettings) .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") @@ -71,7 +72,7 @@ lazy val `test-support` = project ) ) -/** Core API - the only dependency needed to write tests. +/** Core API - Defines fundamental predicates. */ lazy val api = project .in(file("modules/api")) @@ -81,6 +82,24 @@ lazy val api = project .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, diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyExists.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyExists.scala new file mode 100644 index 0000000..bd58b3f --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyExists.scala @@ -0,0 +1,23 @@ +package gs.predicate.v0.kv + +import cats.effect.Sync +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[_]: Sync, 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) diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala new file mode 100644 index 0000000..ad3d11d --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueProvider.scala @@ -0,0 +1,44 @@ +package gs.predicate.v0.kv + +import cats.Applicative + +/** Interface for anything that can fetch values for stored keys. + */ +trait KeyValueProvider[F[_], -K, V]: + /** Determine if some key exists. + * + * @param key + * The key to check. + * @return + * True if the key exists, false otherwise. + */ + def exists(key: K): F[Boolean] + + /** Get the value associated with some key. + * + * @param key + * The key to fetch. + * @return + * The value stored for the key, or `None` if no such value exists. + */ + def get(key: K): F[Option[V]] + +object KeyValueProvider: + + /** @return + * New instance of the no-op [[KeyValueProvider]] implementation. + */ + def noop[F[_]: Applicative]: KeyValueProvider[F, Any, Any] = new Noop[F] + + /** No-op implementation that never contains data. + */ + final class Noop[F[_]: Applicative] extends KeyValueProvider[F, Any, Any]: + /** @inheritDocs + */ + override def exists(key: Any): F[Boolean] = Applicative[F].pure(false) + + /** @inheritDocs + */ + override def get(key: Any): F[Option[Any]] = Applicative[F].pure(None) + +end KeyValueProvider diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueStringProvider.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueStringProvider.scala new file mode 100644 index 0000000..7f7a7b2 --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/KeyValueStringProvider.scala @@ -0,0 +1,6 @@ +package gs.predicate.v0.kv + +/** Type alias for a [[KeyValueProvider]] that associates string values with + * string keys. + */ +type KeyValueStringProvider[F[_]] = KeyValueProvider[F, String, String] diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/MemoryMapStringProvider.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/MemoryMapStringProvider.scala new file mode 100644 index 0000000..929a44f --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/MemoryMapStringProvider.scala @@ -0,0 +1,24 @@ +package gs.predicate.v0.kv + +import cats.effect.Sync +import cats.effect.std.MapRef +import cats.syntax.all.* + +/** Provides keys and values, given some in-memory map. + * + * @param map + * The underlying map. + */ +final class MemoryMapStringProvider[F[_]: Sync]( + private val map: MapRef[F, String, Option[String]] +) extends KeyValueStringProvider[F]: + + /** @inheritDocs + */ + override def exists(key: String): F[Boolean] = + map.apply(key).get.map(_.isDefined) + + /** @inheritDocs + */ + override def get(key: String): F[Option[String]] = + map.apply(key).get diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/StringContains.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/StringContains.scala new file mode 100644 index 0000000..b001fce --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/StringContains.scala @@ -0,0 +1,32 @@ +package gs.predicate.v0.kv + +import cats.effect.Sync +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[_]: Sync, 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() + } diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/StringEndsWith.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/StringEndsWith.scala new file mode 100644 index 0000000..4ca24c1 --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/StringEndsWith.scala @@ -0,0 +1,32 @@ +package gs.predicate.v0.kv + +import cats.effect.Sync +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[_]: Sync, 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() + } diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/StringStartsWith.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/StringStartsWith.scala new file mode 100644 index 0000000..3ef1f6a --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/StringStartsWith.scala @@ -0,0 +1,32 @@ +package gs.predicate.v0.kv + +import cats.effect.Sync +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[_]: Sync, 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() + } diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/ValueEquals.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/ValueEquals.scala new file mode 100644 index 0000000..bc31119 --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/ValueEquals.scala @@ -0,0 +1,33 @@ +package gs.predicate.v0.kv + +import cats.effect.Sync +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[_]: Sync, 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() + } diff --git a/modules/kv/src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala b/modules/kv/src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala new file mode 100644 index 0000000..78030bb --- /dev/null +++ b/modules/kv/src/main/scala/gs/predicate/v0/kv/ValueNotEquals.scala @@ -0,0 +1,33 @@ +package gs.predicate.v0.kv + +import cats.effect.Sync +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[_]: Sync, 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() + }