Baseline project from which to work.
All checks were successful
/ Build and Release Application (push) Successful in 2m28s

This commit is contained in:
Pat Garrity 2026-05-11 21:28:06 -05:00
commit 87a6fd5398
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
22 changed files with 1876 additions and 0 deletions

View file

@ -0,0 +1,40 @@
on:
pull_request:
types: [opened, synchronize, reopened]
defaults:
run:
shell: bash
jobs:
library_snapshot:
runs-on: docker
container:
image: registry.garrity.co:8443/gs/ci-scala:latest
name: 'Build and Test Application Snapshot'
env:
GS_MAVEN_USER: ${{ vars.GS_MAVEN_USER }}
GS_MAVEN_TOKEN: ${{ secrets.GS_MAVEN_TOKEN }}
steps:
- uses: actions/checkout@v4
name: 'Checkout Repository'
with:
fetch-depth: 0
- name: 'Pre-Commit'
run: |
pre-commit install
pre-commit run --all-files
- name: 'Prepare Versioned Build'
run: |
latest_git_tag="$(git describe --tags --abbrev=0 || echo 'No Tags')"
latest_commit_message="$(git show -s --format=%s HEAD)"
echo "Previous Git Tag: $latest_git_tag"
echo "Latest Commit: $latest_commit_message (SNAPSHOT)"
sbtn -Dsnapshot=true "api/calVerInfo"
- name: 'Unit Tests and Code Coverage'
run: |
sbtn clean
sbtn coverage
sbtn test
sbtn coverageReport
sbtn coverageAggregate

View file

@ -0,0 +1,54 @@
on:
push:
branches:
- main
defaults:
run:
shell: bash
jobs:
application_release:
runs-on: docker
container:
image: registry.garrity.co:8443/gs/ci-scala:latest
name: 'Build and Release Application'
env:
GS_MAVEN_USER: ${{ vars.GS_MAVEN_USER }}
GS_MAVEN_TOKEN: ${{ secrets.GS_MAVEN_TOKEN }}
steps:
- uses: actions/checkout@v4
name: 'Checkout Repository'
with:
fetch-depth: 0
- name: 'Pre-Commit'
run: |
pre-commit install
pre-commit run --all-files
- name: 'Prepare Versioned Build'
run: |
latest_git_tag="$(git describe --tags --abbrev=0 || echo 'No Tags')"
latest_commit_message="$(git show -s --format=%s HEAD)"
echo "Previous Git Tag: $latest_git_tag"
echo "Latest Commit: $latest_commit_message"
sbtn -Drelease=true api/calVerInfo
- name: 'Unit Tests and Code Coverage'
run: |
sbtn clean
sbtn coverage
sbtn test
sbtn coverageReport
sbtn coverageAggregate
- name: 'Publish Release'
run: |
sbtn coverageOff
sbtn clean
sbtn api/calVerWriteVersionToFile
selected_version="$(cat .version)"
echo "PRODUCING A RELEASE IS CURRENTLY TURNED OFF -- $selected_version"
- name: 'Create Git Tag'
run: |
selected_version="$(cat .version)"
echo "TAGGING IS CURRENTLY TURNED OFF -- $selected_version"
#git tag "$selected_version"
#git push origin "$selected_version"

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target/
project/target/
project/project/
.version

16
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,16 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: fix-byte-order-marker
- id: mixed-line-ending
args: ['--fix=lf']
description: Enforces using only 'LF' line endings.
- id: trailing-whitespace
- repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala
rev: v1.0.1
hooks:
- id: scalafmt

72
.scalafmt.conf Normal file
View file

@ -0,0 +1,72 @@
// See: https://github.com/scalameta/scalafmt/tags for the latest tags.
version = 3.8.1
runner.dialect = scala3
maxColumn = 80
rewrite {
rules = [RedundantBraces, RedundantParens, Imports, SortModifiers]
imports.expand = true
imports.sort = scalastyle
redundantBraces.ifElseExpressions = true
redundantBraces.stringInterpolation = true
}
indent {
main = 2
callSite = 2
defnSite = 2
extendSite = 4
withSiteRelativeToExtends = 2
commaSiteRelativeToExtends = 2
}
align {
preset = more
openParenCallSite = false
openParenDefnSite = false
}
newlines {
implicitParamListModifierForce = [before,after]
topLevelStatementBlankLines = [
{
blanks = 1
}
]
afterCurlyLambdaParams = squash
}
danglingParentheses {
defnSite = true
callSite = true
ctrlSite = true
exclude = []
}
verticalMultiline {
atDefnSite = true
arityThreshold = 2
newlineAfterOpenParen = true
}
comments {
wrap = standalone
}
docstrings {
style = "SpaceAsterisk"
oneline = unfold
wrap = yes
forceBlankLineBefore = true
}
project {
excludePaths = [
"glob:**target/**",
"glob:**.metals/**",
"glob:**.bloop/**",
"glob:**.bsp/**",
"glob:**metals.sbt",
"glob:**.git/**"
]
}

68
README.md Normal file
View file

@ -0,0 +1,68 @@
# respite
> _respite_
> An interruption in the intensity or amount of something.
This project is a way to disrupt the stress. It is also an in-memory key/value
store.
## At a Glance
- Single instance (no clustered mode)
- Concurrency support and atomic operations
- No Schema
- No Pub/Sub
- Supports specific types
- Supports key and value constraints
- HTTP API
### Supported Key Types
The sizes below omit the size of heap references (64 bits).
| Type | Size in Bytes | Description |
| -------- | ------------- | ---------------------- |
| `int64` | `8` | Signed 64-bit integer. |
| `string` | Variable | UTF-16 |
| `uuid` | `16` | N/A |
| `date` | `8` | Year, month, day. |
### Supported Value Types
The sizes below omit the size of heap references (64 bits). Booleans do not have
precisely defined sizes on the JVM, but 1 byte is typical.
| Type | Size in Bytes | Description |
| -------- | ------------- | ---------------------- |
| `int64` | `8` | Signed 64-bit integer. |
| `string` | Variable | UTF-16 |
| `uuid` | `16` | N/A |
| `date` | `8` | Year, month, day. |
| `bool` | `1` | True/false. |
| `byte` | Variable | Any byte array. |
### Supported Operations
- Get
- Set
- Delete
- Increment
- Decrement
- Compare and Swap
- Insert
- Contains
### Supported Constraints
Any of the following constraints may be applied to a single key space. If any
operation on the key space violates a constraint, that operation fails.
| Constraint | Description |
| ------------------- | -------------------------------------------------------------------------- |
| `single_key_type` | Only the given type is accepted for keys. |
| `single_value_type` | Only the given type is accepted for values. |
| `write_once` | Each key may be written exactly once. Deletes and reassignments will fail. |
## HTTP API
Respite provides an HTTP API that uses JSON to transmit data.

111
build.sbt Normal file
View file

@ -0,0 +1,111 @@
val scala3: String = "3.8.3"
ThisBuild / scalaVersion := scala3
ThisBuild / gsProjectName := "respite"
ThisBuild / externalResolvers := Seq(
"Garrity Software Mirror" at "https://maven.garrity.co/releases",
"Garrity Software Releases" at "https://maven.garrity.co/gs"
)
lazy val sharedSettings = Seq(
scalaVersion := scala3,
version := calVer.value,
publish / skip := true,
publishLocal / skip := true,
publishArtifact := false
)
val Deps = new {
val Cats = new {
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.7.0"
}
val Fs2 = new {
private val Version: String = "3.13.0"
val Core: ModuleID = "co.fs2" %% "fs2-core" % Version
val IO: ModuleID = "co.fs2" %% "fs2-io" % Version
}
val Gs = new {
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.4.2"
val Std: ModuleID = "gs" %% "gs-std-core-v0" % "0.1.2"
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
}
val Http4s = new {
private val Version: String = "1.0.0-M45"
val Core: ModuleID = "org.http4s" %% "http4s-core" % Version
val Dsl: ModuleID = "org.http4s" %% "http4s-dsl" % Version
val Circe: ModuleID = "org.http4s" %% "http4s-circe" % Version
val EmberServer: ModuleID = "org.http4s" %% "http4s-ember-server" % Version
}
val Log4Cats = new {
val Slf4j: ModuleID = "org.typelevel" %% "log4cats-slf4j" % "2.8.0"
}
val LogbackClassic: ModuleID = "ch.qos.logback" % "logback-classic" % "1.5.32"
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.3.0"
}
lazy val testSettings = Seq(
libraryDependencies ++= Seq(
Deps.MUnit % Test,
Deps.Gs.Datagen % Test
)
)
lazy val respite = project
.in(file("."))
.aggregate(model, db, api)
.settings(sharedSettings)
.settings(name := s"${gsProjectName.value}")
lazy val model = project
.in(file("modules/model"))
.settings(sharedSettings)
.settings(testSettings)
.settings(name := s"${gsProjectName.value}-model")
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Core,
Deps.Gs.Uuid,
Deps.Gs.Std
)
)
lazy val db = project
.in(file("modules/db"))
.dependsOn(model)
.settings(sharedSettings)
.settings(testSettings)
.settings(name := s"${gsProjectName.value}-db")
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Core,
Deps.Cats.Effect
)
)
lazy val api = project
.in(file("modules/api"))
.dependsOn(model, db)
.settings(sharedSettings)
.settings(testSettings)
.settings(name := s"${gsProjectName.value}-api")
.settings(fork := true)
.settings(
libraryDependencies ++= Seq(
Deps.Http4s.Core,
Deps.Http4s.Dsl,
Deps.Http4s.Circe,
Deps.Http4s.EmberServer,
Deps.Log4Cats.Slf4j,
Deps.LogbackClassic
)
)

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<appender name="STDOUT" class="ConsoleAppender">
<encoder class="PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View file

@ -0,0 +1,34 @@
package gs.respite.api
import cats.effect.ExitCode
import cats.effect.IO
import cats.effect.IOApp
import com.comcast.ip4s._
import org.http4s.HttpRoutes
import org.http4s.Method
import org.http4s.dsl.io.*
import org.http4s.ember.server.EmberServerBuilder
import org.typelevel.log4cats.LoggerFactory
import org.typelevel.log4cats.slf4j.Slf4jFactory
object RespiteApi extends IOApp:
given CanEqual[Method, Method] = CanEqual.derived
given CanEqual[org.http4s.Uri.Path, org.http4s.Uri.Path] = CanEqual.derived
implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO]
private val RespiteService = HttpRoutes
.of[IO] { case GET -> Root =>
Ok("Hello, Respite!")
}
.orNotFound
override def run(args: List[String]): cats.effect.IO[ExitCode] =
EmberServerBuilder
.default[IO]
.withHost(ipv4"0.0.0.0")
.withPort(port"8080")
.withHttpApp(RespiteService)
.build
.use(_ => IO.never)
.as(ExitCode.Success)

View file

@ -0,0 +1,98 @@
package gs.respite.db
import cats.Applicative
import cats.effect.Async
import gs.respite.model.KeySpaceName
/** Database for key spaces -- each key space is a [[RespiteDb]]. Keys may not
* be duplicated _within_ a key space, but _may_ be duplicated across key
* spaces.
*
* Each key space is a named, isolated, set of data.
*/
trait KeySpaceDb[F[_]]:
/** @return
* The default key space.
*/
def default: RespiteDb[F]
/** Get the key space with the given name.
*
* @param name
* The name of the key space.
* @return
* The [[RespiteDb]] that stores the key space, or `None` if the name does
* not exist.
*/
def get(name: KeySpaceName): F[Option[RespiteDb[F]]]
/** Create a new key space with the given name. Fails if the name already
* exists.
*
* @param name
* The unique name of the key space.
* @param db
* The database implementation for this key space.
* @return
* True if creation succeeds, false otherwise.
*/
def create(
name: KeySpaceName,
db: RespiteDb[F]
): F[Boolean]
/** Delete the key space with the given name. Implicitly removes all data
* stored within the key space.
*
* @param name
* The name of the key space to delete.
* @return
* Side-effect describing the operation.
*/
def delete(name: KeySpaceName): F[Unit]
end KeySpaceDb
object KeySpaceDb:
/** @return
* An instance of [[KeySpaceDb]] that does nothing.
*/
def noop[F[_]: Applicative]: KeySpaceDb[F] = new Noop[F]
/** @return
* An instance of [[MemoryKeySpaceDb]] that stores data in memory and
* supports concurrent access.
*/
def memory[F[_]: Async]: F[KeySpaceDb[F]] =
MemoryKeySpaceDb.initialize[F]
/** An implementation of [[KeySpaceDb]] that does nothing.
*/
final class Noop[F[_]: Applicative] extends KeySpaceDb[F]:
private val defaultDb = RespiteDb.noop[F](KeySpaceName.Default)
/** @inheritDocs
*/
override def default: RespiteDb[F] = defaultDb
/** @inheritDocs
*/
override def get(name: KeySpaceName): F[Option[RespiteDb[F]]] =
Applicative[F].pure(None)
/** @inheritDocs
*/
override def create(
name: KeySpaceName,
db: RespiteDb[F]
): F[Boolean] =
Applicative[F].pure(false)
/** @inheritDocs
*/
override def delete(name: KeySpaceName): F[Unit] =
Applicative[F].unit
end KeySpaceDb

View file

@ -0,0 +1,209 @@
package gs.respite.db
import cats.effect.Async
import cats.effect.std.MapRef
import cats.syntax.all.*
import gs.respite.model.ConstraintException
import gs.respite.model.Int64Value
import gs.respite.model.Key
import gs.respite.model.KeySpaceName
import gs.respite.model.KeyType
import gs.respite.model.Value
import gs.respite.model.ValueType
/** Implementation of [[RespiteDb]] base on Cats Effect and in-memory
* concurrency primitives. Safe for concurrent use across threads.
*
* Use the `initialize` function to instantiate this class.
*
* @param kv
* The underlying key-value store.
*/
final class MemoryDb[F[_]: Async] private (
private val kv: MapRef[F, Key, Option[Value]],
val keySpace: KeySpaceName,
val singleKeyType: Option[KeyType],
val singleValueType: Option[ValueType],
val isWriteOnce: Boolean
) extends RespiteDb[F]:
val enforceSingleKeyType: (Key) => F[Key] =
singleKeyType match
case None => key => Async[F].pure(key)
case Some(kt) =>
k =>
if k.keyType == kt then Async[F].pure(k)
else
Async[F].raiseError(
ConstraintException.InvalidKeyException(k, kt, keySpace)
)
val enforceSingleValueType: (Key, Value) => F[Value] =
singleValueType match
case None =>
(
_,
value
) => Async[F].pure(value)
case Some(vt) =>
(
k,
v
) =>
if v.valueType == vt then Async[F].pure(v)
else
Async[F].raiseError(
ConstraintException.InvalidValueException(k, v, vt, keySpace)
)
/** @inheritDocs
*/
override def get(key: Key): F[Option[Value]] =
for
k <- enforceSingleKeyType(key)
result <- kv.apply(k).get
yield result
/** @inheritDocs
*/
override def set(
key: Key,
value: Value
): F[Option[Value]] =
for
k <- enforceSingleKeyType(key)
v <- enforceSingleValueType(k, value)
result <- kv.apply(k).getAndUpdate {
case None => Some(v)
case Some(oldValue) =>
// TODO: Rip out to pre-calculated function
if isWriteOnce then
throw ConstraintException.WriteOnceException(key, keySpace)
else Some(v)
}
yield result
/** @inheritDocs
*/
override def delete(key: Key): F[Option[Value]] =
if isWriteOnce then
Async[F].raiseError(ConstraintException.WriteOnceException(key, keySpace))
else
for
k <- enforceSingleKeyType(key)
result <- kv.apply(k).get.flatMap(v => kv.unsetKey(k).as(v))
yield result
/** @inheritDocs
*/
override def increment(key: Key): F[Option[Value]] =
for
k <- enforceSingleKeyType(key)
result <- kv.apply(k).updateAndGet {
case Some(value) =>
// TODO: Rip out to pre-calculated function
if isWriteOnce then
throw ConstraintException.WriteOnceException(key, keySpace)
else
value match
case lv: Int64Value => Some(lv.increment())
case _ => None
case None => None
}
yield result
/** @inheritDocs
*/
override def decrement(key: Key): F[Option[Value]] =
for
k <- enforceSingleKeyType(key)
result <- kv.apply(key).updateAndGet {
case Some(value) =>
// TODO: Rip out to pre-calculated function
if isWriteOnce then
throw ConstraintException.WriteOnceException(key, keySpace)
else
value match
case lv: Int64Value => Some(lv.decrement())
case _ => None
case None => None
}
yield result
/** @inheritDocs
*/
override def cas(
key: Key,
expectedValue: Value,
newValue: Value
): F[Boolean] =
if isWriteOnce then
Async[F].raiseError(ConstraintException.WriteOnceException(key, keySpace))
else
for
k <- enforceSingleKeyType(key)
ev <- enforceSingleValueType(k, expectedValue)
nv <- enforceSingleValueType(k, newValue)
result <- kv
.apply(k)
.updateAndGet {
case Some(oldValue) =>
if oldValue == ev then Some(nv)
else Some(oldValue)
case None => None
}
.map {
case None =>
// This operation explicitly does not work on unset keys.
false
case Some(outcome) =>
// If the outcome is the `newValue`, that means we successfully
// updated using CAS.
outcome == newValue
}
yield result
/** @inheritDocs
*/
override def insert(
key: Key,
value: Value
): F[Option[Value]] =
for
k <- enforceSingleKeyType(key)
v <- enforceSingleValueType(k, value)
result <- kv.apply(k).updateAndGet {
case None => Some(v)
case Some(alreadyThere) => Some(alreadyThere)
}
yield result
/** @inheritDocs
*/
override def contains(key: Key): F[Boolean] =
for
k <- enforceSingleKeyType(key)
result <- kv.apply(k).get.map(_.isDefined)
yield result
object MemoryDb:
/** Initialize a new in-memory database, backed by a `ConcurrentHashMap`.
*
* @return
* The new database.
*/
def initialize[F[_]: Async](keySpace: KeySpaceName): F[RespiteDb[F]] =
MapRef
.ofConcurrentHashMap[F, Key, Value](initialCapacity = 4096)
.map(kv =>
new MemoryDb[F](
kv = kv,
keySpace = keySpace,
singleKeyType = None,
singleValueType = None,
isWriteOnce = false
)
)
end MemoryDb

View file

@ -0,0 +1,59 @@
package gs.respite.db
import cats.effect.Async
import cats.effect.std.MapRef
import cats.syntax.all.*
import gs.respite.model.KeySpaceName
/** Implementation of [[KeySpace]] base on Cats Effect and in-memory concurrency
* primitives. Safe for concurrent use across threads.
*
* Use the `initialize` function to instantiate this class.
*
* @param kv
* The underlying key-value store.
*/
final class MemoryKeySpaceDb[F[_]] private (
private val defaultDb: RespiteDb[F],
private val kv: MapRef[F, KeySpaceName, Option[RespiteDb[F]]]
) extends KeySpaceDb[F]:
/** @inheritDocs
*/
override def default: RespiteDb[F] = defaultDb
/** @inheritDocs
*/
override def get(name: KeySpaceName): F[Option[RespiteDb[F]]] =
kv.apply(name).get
/** @inheritDocs
*/
override def create(
name: KeySpaceName,
db: RespiteDb[F]
): F[Boolean] =
kv.apply(name).tryUpdate {
case None => Some(db)
case Some(existing) => Some(existing)
}
/** @inheritDocs
*/
override def delete(name: KeySpaceName): F[Unit] =
kv.unsetKey(name)
object MemoryKeySpaceDb:
/** Initialize a new in-memory database, backed by a `ConcurrentHashMap`.
*
* @return
* The new database.
*/
def initialize[F[_]: Async]: F[KeySpaceDb[F]] =
for
defaultDb <- RespiteDb.memory[F](KeySpaceName.Default)
kv <- MapRef.ofConcurrentHashMap(initialCapacity = 16)
yield new MemoryKeySpaceDb(defaultDb, kv)
end MemoryKeySpaceDb

View file

@ -0,0 +1,192 @@
package gs.respite.db
import cats.Applicative
import cats.effect.Async
import gs.respite.model.Key
import gs.respite.model.KeySpaceName
import gs.respite.model.Value
/** Primary database interface for Respite. Defines a single key space -- some
* isolated set of key/value data.
*/
trait RespiteDb[F[_]]:
/** @return
* The name of the key space represented by this database.
*/
def keySpace: KeySpaceName
/** Get the value assigned to some key.
*
* @param key
* The key to look up.
* @return
* The value of the key, or `None` if the key does not exist.
*/
def get(key: Key): F[Option[Value]]
/** Insert or update the value for some key.
*
* @param key
* The key.
* @param value
* The value to associate with the key.
* @return
* The previous value, if one was set.
*/
def set(
key: Key,
value: Value
): F[Option[Value]]
/** Delete the given key if it exists.
*
* @param key
* The key to delete.
* @return
* The value that was stored in the given key, if one existed.
*/
def delete(key: Key): F[Option[Value]]
/** Increment the stored value. Only valid for [[LongKey]].
*
* @param key
* The key to increment.
* @return
* The _current_ value of the key.
*/
def increment(key: Key): F[Option[Value]]
/** Decrement the stored value. Only valid for [[LongKey]].
*
* @param key
* The key to decrement.
* @return
* The _current_ value of the key.
*/
def decrement(key: Key): F[Option[Value]]
/** Compare and Swap.
*
* Compares the value stored in the key to some given value. If they are
* equal, sets the key to some new value.
*
* @param key
* The key.
* @param expectedValue
* The value to match.
* @param newValue
* The value to assign to the key, if the stored value matches the expected
* value.
* @return
* True if the value was set, false otherwise.
*/
def cas(
key: Key,
expectedValue: Value,
newValue: Value
): F[Boolean]
/** Insert a new key. If the key already exists, the value is not updated and
* the current value is returned.
*
* @param key
* The key to insert.
* @param value
* The value to associate with the key.
* @return
* The set value, or the current value if the key was already assigned.
*/
def insert(
key: Key,
value: Value
): F[Option[Value]]
/** Check if the given key exists.
*
* @param key
* They key to look up.
* @return
* True if the key exists, false otherwise.
*/
def contains(key: Key): F[Boolean]
end RespiteDb
object RespiteDb:
/** @return
* An instance of [[RespiteDb]] that does nothing.
*/
def noop[F[_]: Applicative](keySpace: KeySpaceName): RespiteDb[F] =
new Noop[F](keySpace)
/** Instantiate a new in-memory database.
*
* @param keySpace
* The name of the key space.
* @return
* The new [[RespiteDb]] instance.
*/
def memory[F[_]: Async](keySpace: KeySpaceName): F[RespiteDb[F]] =
MemoryDb.initialize(keySpace)
/** An implementation of [[RespiteDb]] that does nothing.
*
* @param keySpace
* The name of the key space.
*/
final class Noop[F[_]: Applicative](
val keySpace: KeySpaceName
) extends RespiteDb[F]:
/** @inheritDocs
*/
override def get(key: Key): F[Option[Value]] =
Applicative[F].pure(None)
/** @inheritDocs
*/
override def set(
key: Key,
value: Value
): F[Option[Value]] =
Applicative[F].pure(None)
/** @inheritDocs
*/
override def delete(key: Key): F[Option[Value]] =
Applicative[F].pure(None)
/** @inheritDocs
*/
override def increment(key: Key): F[Option[Value]] =
Applicative[F].pure(None)
/** @inheritDocs
*/
override def decrement(key: Key): F[Option[Value]] =
Applicative[F].pure(None)
/** @inheritDocs
*/
override def cas(
key: Key,
expectedValue: Value,
newValue: Value
): F[Boolean] =
Applicative[F].pure(false)
/** @inheritDocs
*/
override def insert(
key: Key,
value: Value
): F[Option[Value]] =
Applicative[F].pure(None)
/** @inheritDocs
*/
override def contains(key: Key): F[Boolean] =
Applicative[F].pure(false)
end RespiteDb

View file

@ -0,0 +1,299 @@
package gs.respite.model
import gs.uuid.v0.UUID
import java.time.LocalDate
/** Represents valid ways to provide an address to some [[Value]].
*
* All valid types are recorded (and serialized) using [[KeyType]].
*/
sealed trait Key:
/** @return
* The named type for this [[Key]].
*/
def keyType: KeyType
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case sv: StringKey =>
this match
case sv1: StringKey => sv1.value == sv.value
case _ => false
case uv: UUIDKey =>
this match
case uv1: UUIDKey => uv1.value == uv.value
case _ => false
case lv: Int64Key =>
this match
case lv1: Int64Key => lv1.value == lv.value
case _ => false
case dv: DateKey =>
this match
case dv1: DateKey => dv1.value.isEqual(dv.value)
case _ => false
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int =
this match
case v: StringKey => v.hashCode()
case v: UUIDKey => v.hashCode()
case v: Int64Key => v.hashCode()
case v: DateKey => v.hashCode()
end Key
/** Represents valid keys to address values.
*/
object Key:
given CanEqual[Key, Key] = CanEqual.derived
/** Instantiate a new [[Key]] backed by a `String`.
*
* @param value
* The underlying value.
* @return
* The new [[Key]].
*/
def string(value: String): Key = StringKey(value)
/** Instantiate a new [[Key]] backed by a `UUID`.
*
* @param value
* The underlying value.
* @return
* The new [[Key]].
*/
def uuid(value: UUID): Key = UUIDKey(value)
/** Instantiate a new [[Key]] backed by a generated `UUID` using the v7
* algorithm.
*
* @return
* The new [[Key]].
*/
def uuidV7(): Key = UUIDKey.v7()
/** Instantiate a new [[Key]] backed by a random `UUID`.
*
* @return
* The new [[Key]].
*/
def uuidRandom(): Key = UUIDKey.random()
/** Instantiate a new [[Key]] backed by a `Long`.
*
* @param value
* The underlying value.
* @return
* The new [[Key]].
*/
def long(value: Long): Key = Int64Key(value)
/** Instantiate a new [[Key]] backed by a `LocalDate`.
*
* @param value
* The underlying value.
* @return
* The new [[Key]].
*/
def date(value: LocalDate): Key = DateKey(value)
end Key
/** `String`-based [[Key]].
*
* @param value
* The underlying `String`.
*/
final class StringKey private (val value: String) extends Key:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: StringKey => other.value == value
case other: String => other == value
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value
/** @inheritDocs
*/
override def keyType: KeyType = KeyType.string
end StringKey
/** `String`-based [[Key]].
*/
object StringKey:
given CanEqual[StringKey, StringKey] = CanEqual.derived
/** Instantiate a [[Key]] from the given `String`.
*
* @param value
* The `String` to express as a [[Key]].
* @return
* The new [[Key]] instance.
*/
def apply(value: String): Key =
new StringKey(value)
end StringKey
/** `UUID`-based [[Key]].
*
* @param value
* The underlying `UUID`.
*/
final class UUIDKey private (val value: UUID) extends Key:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: UUIDKey => other.value == value
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value.withDashes()
/** @inheritDocs
*/
override def keyType: KeyType = KeyType.uuid
end UUIDKey
/** `UUID`-based [[Key]].
*/
object UUIDKey:
given CanEqual[UUIDKey, UUIDKey] = CanEqual.derived
/** Instantiate a [[Key]] from the given `UUID`.
*
* @param value
* The `UUID` to express as a [[Key]].
* @return
* The new [[Key]] instance.
*/
def apply(value: UUID): UUIDKey = new UUIDKey(value)
/** Generate a new UUIDv7 expressed as a [[Key]].
*
* @return
* The new [[Key]] instance.
*/
def v7(): UUIDKey = new UUIDKey(UUID.v7())
/** Generate a new UUIDv4 expressed as a [[Key]].
*
* @return
* The new [[Key]] instance.
*/
def random(): UUIDKey = new UUIDKey(UUID.v4())
end UUIDKey
/** `Long`-based [[Key]].
*
* @param value
* The underlying `Long`.
*/
final class Int64Key private (val value: Long) extends Key:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: Int64Key => other.value == value
case other: Long => other == value
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value.toString()
/** @inheritDocs
*/
override def keyType: KeyType = KeyType.int64
end Int64Key
object Int64Key:
given CanEqual[Int64Key, Int64Key] = CanEqual.derived
/** Instantiate a [[Key]] from the given `Long`.
*
* @param value
* The `Long` to express as a [[Key]].
* @return
* The new [[Key]] instance.
*/
def apply(value: Long): Int64Key = new Int64Key(value)
end Int64Key
/** `LocalDate`-based [[Key]].
*
* @param value
* The underlying `Date`.
*/
final class DateKey private (val value: LocalDate) extends Key:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: DateKey => other.value.isEqual(value)
case other: LocalDate => other.isEqual(value)
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value.toString()
/** @inheritDocs
*/
override def keyType: KeyType = KeyType.date
end DateKey
object DateKey:
given CanEqual[DateKey, DateKey] = CanEqual.derived
/** Instantiate a [[Key]] from the given `Date`.
*
* @param value
* The `Date` to express as a [[Key]].
* @return
* The new [[Key]] instance.
*/
def apply(value: LocalDate): DateKey = new DateKey(value)
end DateKey

View file

@ -0,0 +1,36 @@
package gs.respite.model
/** Enumeration for constraints that can be applied to key spaces.
*
* @param name
* The formal name of the constraint.
*/
sealed abstract class KeySpaceConstraint(val name: String)
object KeySpaceConstraint:
/** Only the given type is accepted for keys. Calls that provide other key
* types will fail.
*
* @param keyType
* The selected [[KeyType]].
*/
case class SingleKeyType(
keyType: KeyType
) extends KeySpaceConstraint("single_key_type")
/** Only the given type is accepted for values. Calls that provide other value
* types will fail.
*
* @param valueType
* The selected [[ValueType]].
*/
case class SingleValueType(
valueType: ValueType
) extends KeySpaceConstraint("single_value_type")
/** Each key may be written exactly once. Deletes and reassignments will fail.
*/
case object WriteOnce extends KeySpaceConstraint("write_once")
end KeySpaceConstraint

View file

@ -0,0 +1,41 @@
package gs.respite.model
/** Opaque type for a string that represents the unique name of some key space.
*
* Each key space is a named, isolated, set of data.
*/
opaque type KeySpaceName = String
/** Opaque type for a string that represents the unique name of some key space.
*
* Each key space is a named, isolated, set of data.
*/
object KeySpaceName:
/** Instantiate a new [[KeySpaceName]].
*
* @param value
* The string value.
* @return
* The new name.
*/
def apply(value: String): KeySpaceName = value
/** The default key space -- the empty string.
*/
final val Default: KeySpaceName = ""
given CanEqual[KeySpaceName, KeySpaceName] = CanEqual.derived
extension (name: KeySpaceName)
/** @return
* The underlying string.
*/
def unwrap(): String = name
/** @return
* True if this is the default key space, false otherwise.
*/
def isDefault(): Boolean = name == Default
end KeySpaceName

View file

@ -0,0 +1,50 @@
package gs.respite.model
/** Type that describes all supported type for [[Key]].
*
* @param name
* The name of the type.
*/
sealed abstract class KeyType(val name: String):
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: KeyType => other.name == name
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = name.hashCode()
/** @inheritDocs
*/
override def toString(): String = name
object KeyType:
given CanEqual[KeyType, KeyType] = CanEqual.derived
/** The type for [[StringKey]]
*/
case object string extends KeyType("string")
/** The type for [[Int64Key]]
*/
case object int64 extends KeyType("int64")
/** The type for [[UUIDKey]]
*/
case object uuid extends KeyType("uuid")
/** The type for [[DateKey]]
*/
case object date extends KeyType("date")
val All: Set[KeyType] = Set(string, int64, uuid, date)
def parse(candidate: String): Option[KeyType] =
All.find(_.name.equalsIgnoreCase(candidate))
end KeyType

View file

@ -0,0 +1,35 @@
package gs.respite.model
sealed abstract class RespiteException(message: String)
extends Throwable(message)
sealed abstract class ConstraintException(message: String)
extends RespiteException(message)
object ConstraintException:
final class InvalidKeyException(
val key: Key,
val limit: KeyType,
val keySpace: KeySpaceName
) extends ConstraintException(
s"Received key of type '$key', but key space '$keySpace' is restricted to key type '$limit'."
)
final class InvalidValueException(
val key: Key,
val value: Value,
val limit: ValueType,
val keySpace: KeySpaceName
) extends ConstraintException(
s"Received value of type '$value' for key '$key', but key space '$keySpace' is restricted to value type '$limit'."
)
final class WriteOnceException(
val key: Key,
val keySpace: KeySpaceName
) extends ConstraintException(
s"Attempted to write key '$key' in key space '$keySpace', but the key already exists and this key space is immutable after write."
)
end ConstraintException

View file

@ -0,0 +1,349 @@
package gs.respite.model
import gs.std.v0.core.Base64Encoder
import gs.uuid.v0.UUID
import java.time.LocalDate
/** Represents valid ways to represent data referenced by [[Key]].
*
* All valid types are recorded (and serialized) using [[ValueType]].
*/
sealed trait Value:
/** @return
* The named type for this [[Value]].
*/
def valueType: ValueType
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case sv: StringValue =>
this match
case sv1: StringValue => sv1.value == sv.value
case _ => false
case uv: UUIDValue =>
this match
case uv1: UUIDValue => uv1.value == uv.value
case _ => false
case lv: Int64Value =>
this match
case lv1: Int64Value => lv1.value == lv.value
case _ => false
case bv: BooleanValue =>
this match
case bv1: BooleanValue => bv1.value == bv.value
case _ => false
case dv: DateValue =>
this match
case dv1: DateValue => dv1.value.isEqual(dv.value)
case _ => false
case bv: ByteValue =>
this match
case bv1: ByteValue => bv1.value.sameElements(bv.value)
case _ => false
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int =
this match
case v: StringValue => v.hashCode()
case v: UUIDValue => v.hashCode()
case v: Int64Value => v.hashCode()
case v: BooleanValue => v.hashCode()
case v: DateValue => v.hashCode()
case v: ByteValue => v.hashCode()
end Value
object Value:
given CanEqual[Value, Value] = CanEqual.derived
end Value
/** `String`-based [[Value]].
*
* @param value
* The underlying `String`.
*/
final class StringValue private (val value: String) extends Value:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: StringValue => other.value == value
case other: String => other == value
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value
/** @inheritDocs
*/
override def valueType: ValueType = ValueType.string
end StringValue
/** `String`-based [[Value]].
*/
object StringValue:
given CanEqual[StringValue, StringValue] = CanEqual.derived
/** Instantiate a [[Value]] from the given `String`.
*
* @param value
* The `String` to express as a [[Value]].
* @return
* The new [[Value]] instance.
*/
def apply(value: String): Value =
new StringValue(value)
end StringValue
/** `UUID`-based [[Value]].
*
* @param value
* The underlying `UUID`.
*/
final class UUIDValue private (val value: UUID) extends Value:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: UUIDValue => other.value == value
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value.withDashes()
/** @inheritDocs
*/
override def valueType: ValueType = ValueType.uuid
end UUIDValue
/** `UUID`-based [[Value]].
*/
object UUIDValue:
given CanEqual[UUIDValue, UUIDValue] = CanEqual.derived
/** Instantiate a [[Value]] from the given `UUID`.
*
* @param value
* The `UUID` to express as a [[Value]].
* @return
* The new [[Value]] instance.
*/
def apply(value: UUID): UUIDValue = new UUIDValue(value)
/** Generate a new UUIDv7 expressed as a [[Value]].
*
* @return
* The new [[Value]] instance.
*/
def v7(): UUIDValue = new UUIDValue(UUID.v7())
/** Generate a new UUIDv4 expressed as a [[Value]].
*
* @return
* The new [[Value]] instance.
*/
def random(): UUIDValue = new UUIDValue(UUID.v4())
end UUIDValue
/** `Long`-based [[Value]].
*
* @param value
* The underlying `Long`.
*/
final class Int64Value private (val value: Long) extends Value:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: Int64Value => other.value == value
case other: Long => other == value
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value.toString()
def increment(): Int64Value = new Int64Value(value + 1L)
def decrement(): Int64Value = new Int64Value(value - 1L)
/** @inheritDocs
*/
override def valueType: ValueType = ValueType.int64
end Int64Value
object Int64Value:
given CanEqual[Int64Value, Int64Value] = CanEqual.derived
/** Instantiate a [[Value]] from the given `Long`.
*
* @param value
* The `Long` to express as a [[Value]].
* @return
* The new [[Value]] instance.
*/
def apply(value: Long): Int64Value = new Int64Value(value)
end Int64Value
/** `Boolean`-based [[Value]].
*
* @param value
* The underlying `Boolean`.
*/
final class BooleanValue private (val value: Boolean) extends Value:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: BooleanValue => other.value == value
case other: Boolean => other == value
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value.toString()
/** @inheritDocs
*/
override def valueType: ValueType = ValueType.bool
end BooleanValue
object BooleanValue:
given CanEqual[BooleanValue, BooleanValue] = CanEqual.derived
/** Instantiate a [[Value]] from the given `Boolean`.
*
* @param value
* The `Boolean` to express as a [[Value]].
* @return
* The new [[Value]] instance.
*/
def apply(value: Boolean): BooleanValue = new BooleanValue(value)
end BooleanValue
/** `Date`-based [[Value]].
*
* @param value
* The underlying `Date`.
*/
final class DateValue private (val value: LocalDate) extends Value:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: DateValue => other.value.isEqual(value)
case other: LocalDate => other.isEqual(value)
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = value.toString()
/** @inheritDocs
*/
override def valueType: ValueType = ValueType.date
end DateValue
object DateValue:
given CanEqual[DateValue, DateValue] = CanEqual.derived
/** Instantiate a [[Value]] from the given `Date`.
*
* @param value
* The `Date` to express as a [[Value]].
* @return
* The new [[Value]] instance.
*/
def apply(value: LocalDate): DateValue = new DateValue(value)
end DateValue
/** `Byte`-based [[Value]].
*
* @param value
* The underlying `Byte`.
*/
final class ByteValue private (val value: Vector[Byte]) extends Value:
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: ByteValue => other.value.sameElements(value)
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = value.hashCode()
/** @inheritDocs
*/
override def toString(): String = Base64Encoder.encode(value.toArray).data
/** @inheritDocs
*/
override def valueType: ValueType = ValueType.date
end ByteValue
object ByteValue:
given CanEqual[ByteValue, ByteValue] = CanEqual.derived
/** Instantiate a [[Value]] from the given `Byte`.
*
* @param value
* The `Byte` to express as a [[Value]].
* @return
* The new [[Value]] instance.
*/
def apply(value: Vector[Byte]): ByteValue = new ByteValue(value)
end ByteValue

View file

@ -0,0 +1,58 @@
package gs.respite.model
/** Type that describes all supported type for [[Value]].
*
* @param name
* The name of the type.
*/
sealed abstract class ValueType(val name: String):
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: ValueType => other.name == name
case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = name.hashCode()
/** @inheritDocs
*/
override def toString(): String = name
object ValueType:
given CanEqual[ValueType, ValueType] = CanEqual.derived
/** The type for [[StringValue]]
*/
case object string extends ValueType("string")
/** The type for [[Int64Value]]
*/
case object int64 extends ValueType("int64")
/** The type for [[UUIDValue]]
*/
case object uuid extends ValueType("uuid")
/** The type for [[DateValue]]
*/
case object date extends ValueType("date")
/** The type for [[BooleanValue]]
*/
case object bool extends ValueType("bool")
/** The type for [[ByteValue]]
*/
case object byte extends ValueType("byte")
val All: Set[ValueType] = Set(string, int64, uuid, date, bool, byte)
def parse(candidate: String): Option[ValueType] =
All.find(_.name.equalsIgnoreCase(candidate))
end ValueType

1
project/build.properties Normal file
View file

@ -0,0 +1 @@
sbt.version=1.12.11

33
project/plugins.sbt Normal file
View file

@ -0,0 +1,33 @@
def selectCredentials(): Credentials =
if ((Path.userHome / ".sbt" / ".credentials").exists())
Credentials(Path.userHome / ".sbt" / ".credentials")
else
Credentials.apply(
realm = "Reposilite",
host = "maven.garrity.co",
userName = sys.env
.get("GS_MAVEN_USER")
.getOrElse(
throw new RuntimeException(
"You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_USER environment variable."
)
),
passwd = sys.env
.get("GS_MAVEN_TOKEN")
.getOrElse(
throw new RuntimeException(
"You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_TOKEN environment variable."
)
)
)
credentials += selectCredentials()
externalResolvers := Seq(
"Garrity Software Mirror" at "https://maven.garrity.co/releases",
"Garrity Software Releases" at "https://maven.garrity.co/gs"
)
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4")
addSbtPlugin("gs" % "sbt-garrity-software" % "0.7.0")
addSbtPlugin("gs" % "sbt-gs-calver" % "0.2.0")