Baseline project from which to work.
All checks were successful
/ Build and Release Application (push) Successful in 2m28s
All checks were successful
/ Build and Release Application (push) Successful in 2m28s
This commit is contained in:
commit
87a6fd5398
22 changed files with 1876 additions and 0 deletions
40
.forgejo/workflows/pull_request.yaml
Normal file
40
.forgejo/workflows/pull_request.yaml
Normal 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
|
||||
54
.forgejo/workflows/release.yaml
Normal file
54
.forgejo/workflows/release.yaml
Normal 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
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
target/
|
||||
project/target/
|
||||
project/project/
|
||||
.version
|
||||
16
.pre-commit-config.yaml
Normal file
16
.pre-commit-config.yaml
Normal 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
72
.scalafmt.conf
Normal 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
68
README.md
Normal 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
111
build.sbt
Normal 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
|
||||
)
|
||||
)
|
||||
17
modules/api/src/main/resources/logback.xml
Normal file
17
modules/api/src/main/resources/logback.xml
Normal 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>
|
||||
34
modules/api/src/main/scala/gs/respite/api/RespiteApi.scala
Normal file
34
modules/api/src/main/scala/gs/respite/api/RespiteApi.scala
Normal 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)
|
||||
98
modules/db/src/main/scala/gs/respite/db/KeySpaceDb.scala
Normal file
98
modules/db/src/main/scala/gs/respite/db/KeySpaceDb.scala
Normal 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
|
||||
209
modules/db/src/main/scala/gs/respite/db/MemoryDb.scala
Normal file
209
modules/db/src/main/scala/gs/respite/db/MemoryDb.scala
Normal 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
|
||||
|
|
@ -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
|
||||
192
modules/db/src/main/scala/gs/respite/db/RespiteDb.scala
Normal file
192
modules/db/src/main/scala/gs/respite/db/RespiteDb.scala
Normal 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
|
||||
299
modules/model/src/main/scala/gs/respite/model/Key.scala
Normal file
299
modules/model/src/main/scala/gs/respite/model/Key.scala
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
50
modules/model/src/main/scala/gs/respite/model/KeyType.scala
Normal file
50
modules/model/src/main/scala/gs/respite/model/KeyType.scala
Normal 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
|
||||
|
|
@ -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
|
||||
349
modules/model/src/main/scala/gs/respite/model/Value.scala
Normal file
349
modules/model/src/main/scala/gs/respite/model/Value.scala
Normal 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
|
||||
|
|
@ -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
1
project/build.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
sbt.version=1.12.11
|
||||
33
project/plugins.sbt
Normal file
33
project/plugins.sbt
Normal 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")
|
||||
Loading…
Add table
Reference in a new issue