Working on the key/value module

This commit is contained in:
Pat Garrity 2025-11-03 22:15:10 -06:00
parent af109358b5
commit ed83e30518
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
10 changed files with 280 additions and 2 deletions

View file

@ -49,7 +49,8 @@ lazy val `gs-predicate` = project
.in(file(".")) .in(file("."))
.aggregate( .aggregate(
`test-support`, `test-support`,
api api,
keyValue
) )
.settings(noPublishSettings) .settings(noPublishSettings)
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") .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 lazy val api = project
.in(file("modules/api")) .in(file("modules/api"))
@ -81,6 +82,24 @@ lazy val api = project
.settings( .settings(
name := s"${gsProjectName.value}-api-v${semVerMajor.value}" 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( .settings(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
Deps.Cats.Core, Deps.Cats.Core,

View file

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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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()
}