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