Compare commits

..

No commits in common. "main" and "0.2.0" have entirely different histories.
main ... 0.2.0

13 changed files with 49 additions and 394 deletions

View file

@ -0,0 +1,8 @@
on: [push]
jobs:
test:
runs-on: docker
container:
image: alpine:3.19
steps:
- run: echo Verifying builds

View file

@ -1,67 +0,0 @@
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 Library 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)"
if [[ "$latest_commit_message" == *"(major)"* ]]; then
export GS_RELEASE_TYPE="major"
elif [[ "$latest_commit_message" == *"(minor)"* ]]; then
export GS_RELEASE_TYPE="minor"
elif [[ "$latest_commit_message" == *"(patch)"* ]]; then
export GS_RELEASE_TYPE="patch"
elif [[ "$latest_commit_message" == *"(docs)"* ]]; then
export GS_RELEASE_TYPE="norelease"
elif [[ "$latest_commit_message" == *"(norelease)"* ]]; then
export GS_RELEASE_TYPE="norelease"
else
export GS_RELEASE_TYPE="norelease"
fi
echo "GS_RELEASE_TYPE=$GS_RELEASE_TYPE" >> $GITHUB_ENV
echo "Previous Git Tag: $latest_git_tag"
echo "Latest Commit: $latest_commit_message ($GS_RELEASE_TYPE) (SNAPSHOT)"
if [ "$GS_RELEASE_TYPE" = "norelease" ]; then
sbtn -Dsnapshot=true -Drelease="patch" semVerInfo
else
sbtn -Dsnapshot=true -Drelease="$GS_RELEASE_TYPE" semVerInfo
fi
- name: 'Unit Tests and Code Coverage'
run: |
sbtn clean
sbtn coverage
sbtn test
sbtn coverageReport
- name: 'Publish Snapshot'
run: |
echo "Testing env var propagation = ${{ env.GS_RELEASE_TYPE }}"
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
echo "Skipping publish due to GS_RELEASE_TYPE=norelease"
else
sbtn coverageOff
sbtn clean
sbtn publish
fi

View file

@ -1,84 +0,0 @@
on:
push:
branches:
- main
defaults:
run:
shell: bash
jobs:
library_release:
runs-on: docker
container:
image: registry.garrity.co:8443/gs/ci-scala:latest
name: 'Build and Release Library'
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)"
if [[ "$latest_commit_message" == *"(major)"* ]]; then
export GS_RELEASE_TYPE="major"
elif [[ "$latest_commit_message" == *"(minor)"* ]]; then
export GS_RELEASE_TYPE="minor"
elif [[ "$latest_commit_message" == *"(patch)"* ]]; then
export GS_RELEASE_TYPE="patch"
elif [[ "$latest_commit_message" == *"(docs)"* ]]; then
export GS_RELEASE_TYPE="norelease"
elif [[ "$latest_commit_message" == *"(norelease)"* ]]; then
export GS_RELEASE_TYPE="norelease"
else
export GS_RELEASE_TYPE="norelease"
fi
echo "GS_RELEASE_TYPE=$GS_RELEASE_TYPE" >> $GITHUB_ENV
echo "Previous Git Tag: $latest_git_tag"
echo "Latest Commit: $latest_commit_message"
echo "Selected Release Type: '$GS_RELEASE_TYPE'"
if [ "$GS_RELEASE_TYPE" = "norelease" ]; then
echo "Skipping all versioning for 'norelease' commit."
else
sbtn -Drelease="$GS_RELEASE_TYPE" semVerInfo
fi
- name: 'Unit Tests and Code Coverage'
run: |
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
echo "Skipping build/test for 'norelease' commit."
else
sbtn clean
sbtn coverage
sbtn test
sbtn coverageReport
fi
- name: 'Publish Release'
run: |
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
echo "Skipping publish for 'norelease' commit."
else
sbtn coverageOff
sbtn clean
sbtn semVerWriteVersionToFile
sbtn publish
fi
- name: 'Create Git Tag'
run: |
if [ "${{ env.GS_RELEASE_TYPE }}" = "norelease" ]; then
echo "Skipping Git tag for 'norelease' commit."
else
selected_version="$(cat .version)"
git tag "$selected_version"
git push origin "$selected_version"
fi

1
.gitignore vendored
View file

@ -2,4 +2,3 @@ target/
project/target/ project/target/
project/project/ project/project/
modules/core/target/ modules/core/target/
.version

View file

@ -5,13 +5,7 @@ repos:
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - 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
- id: check-yaml
- repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala - repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala
rev: v1.0.0 rev: v0.1.3
hooks: hooks:
- id: scalafmt - id: scalafmt

View file

@ -1,5 +1,5 @@
// See: https://github.com/scalameta/scalafmt/tags for the latest tags. // See: https://github.com/scalameta/scalafmt/tags for the latest tags.
version = 3.9.4 version = 3.7.11
runner.dialect = scala3 runner.dialect = scala3
maxColumn = 80 maxColumn = 80

View file

@ -1,23 +1,15 @@
# gs-uuid # gs-uuid
[GS Open Source](https://garrity.co/oss.html) |
[License (Apache 2.0)](./LICENSE) [License (Apache 2.0)](./LICENSE)
UUIDs for Scala 3 with generation based on JUG, and serialization based on code UUID's for Scala 3 with generation based on JUG, and serialization based on code
from Jackson Databind. The only dependency is JUG, whereas the relevant Jackson from Jackson Databind. The only dependency is JUG, whereas the relevant Jackson
code is copied to this implementation (and slightly modified). code is copied to this implementation (and slightly modified).
This project uses the Apache 2.0 License due to the use of Jackson Databind
code.
- [Usage](#usage) - [Usage](#usage)
- [Dependency](#dependency)
- [Donate](#donate)
## Usage ## Usage
### Dependency
This artifact is available in the Garrity Software Maven repository. This artifact is available in the Garrity Software Maven repository.
```scala ```scala
@ -25,22 +17,5 @@ externalResolvers +=
"Garrity Software Releases" at "https://maven.garrity.co/releases" "Garrity Software Releases" at "https://maven.garrity.co/releases"
val GsUuid: ModuleID = val GsUuid: ModuleID =
"gs" %% "gs-uuid-v0" % "$VERSION" "gs" %% "gs-uuid-v0" % "0.1.0"
``` ```
### Type
```scala
import gs.uuid.v0.UUID
given UUID.Generator = UUID.Generator.version7()
val id = UUID.generate()
println(id.str()) // 292c9bc70d0f4d998053d7b2f72cd9b7
```
## Donate
Enjoy this project or want to help me achieve my [goals](https://garrity.co)?
Consider [Donating to Pat on Ko-fi](https://ko-fi.com/gspfm).

View file

@ -1,26 +1,17 @@
val scala3: String = "3.6.4" val scala3: String = "3.3.1"
ThisBuild / scalaVersion := scala3 ThisBuild / scalaVersion := scala3
ThisBuild / versionScheme := Some("semver-spec") ThisBuild / versionScheme := Some("semver-spec")
ThisBuild / gsProjectName := "gs-uuid" ThisBuild / gsProjectName := "gs-uuid"
ThisBuild / licenses := Seq(
"Apache 2.0" -> url(
"https://git.garrity.co/garrity-software/gs-uuid/src/branch/main/LICENSE"
)
)
val sharedSettings = Seq( val sharedSettings = Seq(
scalaVersion := scala3, scalaVersion := scala3,
version := semVerSelected.value, version := semVerSelected.value
coverageFailOnMinimum := true,
coverageMinimumStmtTotal := 100,
coverageMinimumBranchTotal := 100
) )
lazy val testSettings = Seq( lazy val testSettings = Seq(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "1.1.0" % Test "org.scalameta" %% "munit" % "1.0.0-M10" % Test
) )
) )
@ -31,6 +22,6 @@ lazy val `gs-uuid` = project
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
.settings( .settings(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"com.fasterxml.uuid" % "java-uuid-generator" % "5.1.0" "com.fasterxml.uuid" % "java-uuid-generator" % "4.1.1"
) )
) )

View file

@ -1 +1 @@
sbt.version=1.10.11 sbt.version=1.9.8

View file

@ -1,33 +1,10 @@
def selectCredentials(): Credentials = credentials += Credentials(Path.userHome / ".sbt" / ".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( externalResolvers := Seq(
"Garrity Software Mirror" at "https://maven.garrity.co/releases", "Garrity Software Mirror" at "https://maven.garrity.co/releases",
"Garrity Software Releases" at "https://maven.garrity.co/gs" "Garrity Software Releases" at "https://maven.garrity.co/gs"
) )
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.1.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8")
addSbtPlugin("gs" % "sbt-garrity-software" % "0.5.0") addSbtPlugin("gs" % "sbt-garrity-software" % "0.2.0")
addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0") addSbtPlugin("gs" % "sbt-gs-semver" % "0.2.0")

View file

@ -5,9 +5,9 @@ import java.util.Arrays;
/** /**
* UUID serialization and deserialization. This is a direct copy of Jackson * UUID serialization and deserialization. This is a direct copy of Jackson
* Databind (under the Apache 2.0 license at time of writing) with extremely * Databind (also under the Apache 2.0 license at time of writing) with
* minor modifications to remove dashes from the output and to likewise support * extremely minor modifications to remove dashes from the output and to
* parsing with/without dashes. * likewise support parsing with/without dashes.
*/ */
public final class UUIDFormat { public final class UUIDFormat {
private UUIDFormat() {} private UUIDFormat() {}
@ -33,7 +33,7 @@ public final class UUIDFormat {
* @param uuid The UUID to render. * @param uuid The UUID to render.
* @return Hexadecimal representation of the UUID. * @return Hexadecimal representation of the UUID.
*/ */
public static String toHexWithoutDashes(final UUID uuid) { public static String toHex(final UUID uuid) {
final char[] ch = new char[32]; final char[] ch = new char[32];
// Example: // Example:
@ -64,34 +64,6 @@ public final class UUIDFormat {
return new String(ch, 0, 32); return new String(ch, 0, 32);
} }
/**
* <p>Render the given UUID as a 36-character string using lowercase
* hexadecimal with dashes.</p>
*
* @param uuid The UUID to render.
* @return Hexadecimal representation of the UUID.
*/
public static String toHexWithDashes(final UUID uuid) {
final char[] ch = new char[36];
final long msb = uuid.getMostSignificantBits();
_appendInt((int) (msb >> 32), ch, 0);
ch[8] = '-';
int i = (int) msb;
_appendShort(i >>> 16, ch, 9);
ch[13] = '-';
_appendShort(i, ch, 14);
ch[18] = '-';
final long lsb = uuid.getLeastSignificantBits();
_appendShort((int) (lsb >>> 48), ch, 19);
ch[23] = '-';
_appendShort((int) (lsb >>> 32), ch, 24);
_appendInt((int) lsb, ch, 28);
return new String(ch, 0, 36);
}
/** /**
* <p>Render the given UUID as a 16-byte array.</p> * <p>Render the given UUID as a 16-byte array.</p>
* *

View file

@ -2,8 +2,7 @@ package gs.uuid.v0
import com.fasterxml.uuid.Generators import com.fasterxml.uuid.Generators
/** Alias for the `java.util.UUID` type, which represents a 128-bit (16-byte) /** Alias for the `java.util.UUID` type, which represents a 128-bit value.
* value.
* *
* ## ID Generation * ## ID Generation
* *
@ -17,17 +16,19 @@ import com.fasterxml.uuid.Generators
* *
* ## Serialization * ## Serialization
* *
* This library supports the following representations: * This library uses a custom variant of the JDK 17 implementation that removes
* dashes from the output and is likewise capable of parsing those values.
* *
* - Hexadecimal string without dashes * {{{
* - Byte array * val example: UUID = UUID(java.util.UUID.randomUUID())
* * val serialized = example.str() // or example.withoutDashes()
* Serialization code is based on Jackson Databind (Apache 2.0). * // example value = 899efa6f40ed45189efa6f40ed9518ed
* }}}
*/ */
opaque type UUID = java.util.UUID opaque type UUID = java.util.UUID
object UUID: object UUID:
/** Express any `java.util.UUID` as a GS UUID. /** Express any `java.util.UUID` as a Meager UUID.
* *
* @param uuid * @param uuid
* The input UUID. * The input UUID.
@ -36,21 +37,6 @@ object UUID:
*/ */
def apply(uuid: java.util.UUID): UUID = uuid def apply(uuid: java.util.UUID): UUID = uuid
/** Parse a byte array as a [[UUID]].
*
* See `toBytes()` for the inverse of this function.
*
* @param bytes
* The array of bytes to parse.
* @return
* The new [[UUID]], or `None` if the bytes do not represent a valid UUID.
*/
def fromBytes(bytes: Array[Byte]): Option[UUID] =
scala.util
.Try(UUIDFormat.fromBytes(bytes))
.map(uuid => Some(apply(uuid)))
.getOrElse(None)
given CanEqual[UUID, UUID] = CanEqual.derived given CanEqual[UUID, UUID] = CanEqual.derived
/** Generate a new UUID. /** Generate a new UUID.
@ -66,16 +52,6 @@ object UUID:
G: Generator G: Generator
): UUID = G.next() ): UUID = G.next()
/** @return
* New v4 UUID (Random).
*/
def v4(): UUID = Generator.version4.next()
/** @return
* New v7 UUID (Epoch + Counter + Random).
*/
def v7(): UUID = Generator.version7.next()
/** Parse the given string as a UUID. /** Parse the given string as a UUID.
* *
* @param str * @param str
@ -86,14 +62,6 @@ object UUID:
*/ */
def parse(str: String): Option[UUID] = fromString(str) def parse(str: String): Option[UUID] = fromString(str)
/** Parse the given string as a UUID.
*
* @param str
* The UUID, which is expected to be in a hexadecimal format with no
* dashes.
* @return
* The parsed UUID value, or `None` if the value does not represent a UUID.
*/
def fromString(str: String): Option[UUID] = def fromString(str: String): Option[UUID] =
scala.util scala.util
.Try(UUIDFormat.fromHex(str)) .Try(UUIDFormat.fromHex(str))
@ -101,48 +69,16 @@ object UUID:
.getOrElse(None) .getOrElse(None)
extension (uid: UUID) extension (uid: UUID)
/** @return def toUUID: java.util.UUID = uid
* The underlying `java.util.UUID`.
*/
def toUUID(): java.util.UUID = uid
/** @return def str(): String = withoutDashes()
* The byte array representation of this UUID.
*/
def toBytes(): Array[Byte] = UUIDFormat.toBytes(uid)
/** Serialize this UUID. def withoutDashes(): String = UUIDFormat.toHex(uid)
*
* @param dashes
* Whether to use dashes in the output (default = false).
* @return
* Hexadecimal string representation of this UUID.
*/
def str(dashes: Boolean = false): String = withoutDashes()
/** @return
* Hexadecimal string representation of this UUID, without dashes.
*/
def withoutDashes(): String = UUIDFormat.toHexWithoutDashes(uid)
/** @return
* Hexadecimal string representation of this UUID, with dashes.
*/
def withDashes(): String = UUIDFormat.toHexWithDashes(uid)
/** @return
* The least significant bits of this UUID.
*/
def lsb(): Long = uid.getLeastSignificantBits() def lsb(): Long = uid.getLeastSignificantBits()
/** @return
* The most significant bits of this UUID.
*/
def msb(): Long = uid.getMostSignificantBits() def msb(): Long = uid.getMostSignificantBits()
/** @return
* `true` if this UUID is `0`, `false` otherwise.
*/
def isZero(): Boolean = lsb() == 0L && msb() == 0L def isZero(): Boolean = lsb() == 0L && msb() == 0L
/** Type class for UUID generation. /** Type class for UUID generation.
@ -155,11 +91,11 @@ object UUID:
object Generator: object Generator:
/** Instantiate a new Type 4 generator. /** Instantiate a new Type 4 generator.
*/ */
lazy val version4: Generator = new Version4 def version4: Generator = new Version4
/** Instantiate a new Type 7 generator. /** Instantiate a new Type 7 generator.
*/ */
lazy val version7: Generator = new Version7 def version7: Generator = new Version7
/** Type 4 (Random) implementation of a UUID generator. /** Type 4 (Random) implementation of a UUID generator.
*/ */
@ -167,8 +103,8 @@ object UUID:
private val gen = Generators.randomBasedGenerator() private val gen = Generators.randomBasedGenerator()
override def next(): UUID = gen.generate() override def next(): UUID = gen.generate()
/** Type 7 (Unix Epoch Time + Counter + Random) implementation of a UUID /** Type 7 (Unix Epoch Time + Random) implementation of a UUID generator.
* generator. Consider using this rather than Type 1 or Type 6. * Consider using this rather than Type 1 or Type 6.
* *
* This type is defined in [IETF New UUID Formats * This type is defined in [IETF New UUID Formats
* Draft](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7) * Draft](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7)

View file

@ -11,16 +11,7 @@ class UUIDTests extends munit.FunSuite:
val base = v4.next() val base = v4.next()
val str = base.str() val str = base.str()
val parsed = UUID.parse(str) val parsed = UUID.parse(str)
assertEquals(parsed, Some(base)) assert(parsed == Some(base))
}
test(
"should instantiate a type 4 UUID, serialize it, and parse the result (helper function)"
) {
val base = UUID.v4()
val str = base.str()
val parsed = UUID.parse(str)
assertEquals(parsed, Some(base))
} }
test( test(
@ -29,16 +20,7 @@ class UUIDTests extends munit.FunSuite:
val base = v7.next() val base = v7.next()
val str = base.str() val str = base.str()
val parsed = UUID.parse(str) val parsed = UUID.parse(str)
assertEquals(parsed, Some(base)) assert(parsed == Some(base))
}
test(
"should instantiate a type 7 UUID, serialize it, and parse the result (helper function)"
) {
val base = UUID.v7()
val str = base.str()
val parsed = UUID.parse(str)
assertEquals(parsed, Some(base))
} }
test("should instantiate from any java.util.UUID") { test("should instantiate from any java.util.UUID") {
@ -46,39 +28,18 @@ class UUIDTests extends munit.FunSuite:
val base = UUID(raw) val base = UUID(raw)
val str = base.str() val str = base.str()
val parsed = UUID.fromString(str) val parsed = UUID.fromString(str)
assertEquals(parsed, Some(base)) assert(parsed == Some(base))
assertEquals(parsed.map(_.toUUID()), Some(raw)) assert(parsed.map(_.toUUID) == Some(raw))
} }
test("should successfully parse a UUID with dashes") { test("should successfully parse a UUID with dashes") {
val raw = java.util.UUID.randomUUID()
val base = UUID(raw)
assertEquals(UUID.parse(base.withDashes()), Some(base))
}
test(
"should successfully parse a UUID with dashes, generated with this library"
) {
val base = java.util.UUID.randomUUID() val base = java.util.UUID.randomUUID()
assertEquals(UUID.parse(base.toString()), Some(UUID(base))) assert(UUID.parse(base.toString()) == Some(UUID(base)))
} }
test("should fail to parse a non-hex string") { test("should fail to parse a non-hex string") {
val input = "ghijklmnoped45189efa6f40ed9518ed" val input = "ghijklmnoped45189efa6f40ed9518ed"
assertEquals(UUID.parse(input), None) assert(UUID.parse(input) == None)
}
test("should round trip to/from a byte array") {
given UUID.Generator = v7
val base = UUID.generate()
val bytes = base.toBytes()
val parsed = UUID.fromBytes(bytes)
assertEquals(parsed, Some(base))
}
test("should fail to parse an invalid byte array") {
val bytes = Array[Byte](1)
assertEquals(UUID.fromBytes(bytes), None)
} }
test("should generate using an available type class instance") { test("should generate using an available type class instance") {
@ -86,14 +47,7 @@ class UUIDTests extends munit.FunSuite:
val base = doGen val base = doGen
val str = base.str() val str = base.str()
val parsed = UUID.parse(str) val parsed = UUID.parse(str)
assertEquals(parsed, Some(base)) assert(parsed == Some(base))
}
test("should return lsb, msb, and if the UUID is zero") {
val uuid = UUID.parse("00000000-0000-0000-0000-000000000000")
assertEquals(uuid.map(_.lsb()), Some(0L))
assertEquals(uuid.map(_.msb()), Some(0L))
assertEquals(uuid.map(_.isZero()), Some(true))
} }
private def doGen( private def doGen(