From a0edd2c8492106e027a235bfe27da08da5bd8529 Mon Sep 17 00:00:00 2001 From: fxlae Date: Mon, 3 Jul 2023 11:35:15 +0200 Subject: [PATCH] release 0.2.0 update library to typeid-spec 0.2.0 ("enforce that the first suffix character is in the 0-7 range") separate artifacts for Java 8 and Java 17 --- README.md | 188 ++++++++++-- build-conventions/build.gradle.kts | 12 + build-conventions/settings.gradle.kts | 1 + .../typeid/typeid.java-conventions.gradle.kts | 32 ++ .../typeid.library-conventions.gradle.kts | 83 +++--- gradle/libs.versions.toml | 18 ++ gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 30 +- gradlew.bat | 15 +- lib/java17/build.gradle.kts | 24 ++ lib/java17/gradle.properties | 2 + .../jmh/java/de/fxlae/typeid/TypeIdBench.java | 90 ++++++ .../src/main/java/de/fxlae/typeid/TypeId.java | 194 ++++++++++++ .../java/de/fxlae/typeid/util/Validated.java | 276 ++++++++++++++++++ .../test/java/de/fxlae/typeid/SpecTest.java | 9 + .../java/de/fxlae/typeid/TypeIdFacade.java | 70 +++++ .../test/java/de/fxlae/typeid/TypeIdTest.java | 29 ++ .../de/fxlae/typeid/util/ValidatedTest.java | 201 +++++++++++++ lib/java8/build.gradle.kts | 14 + lib/java8/gradle.properties | 2 + .../src/main/java/de/fxlae/typeid/TypeId.java | 227 ++++++++++++++ .../test/java/de/fxlae/typeid/SpecTest8.java | 9 + .../java/de/fxlae/typeid/TypeIdFacade8.java | 72 +++++ .../java/de/fxlae/typeid/TypeIdTest8.java | 9 + lib/shared/build.gradle.kts | 24 ++ .../java/de/fxlae/typeid/lib/TypeIdLib.java | 233 +++++++++++++++ .../de/fxlae/typeid/AbstractSpecTest.java | 70 +++-- .../de/fxlae/typeid/AbstractTypeIdTest.java | 257 ++++++++++++++++ .../java/de/fxlae/typeid/TypeIdInstance.java | 12 + .../de/fxlae/typeid/TypeIdStaticContext.java | 21 ++ .../src}/test/resources/spec/invalid.yml | 9 +- .../shared/src}/test/resources/spec/valid.yml | 9 +- settings.gradle.kts | 6 +- src/main/java/de/fxlae/typeid/TypeId.java | 268 ----------------- .../java/de/fxlae/typeid/UuidProvider.java | 26 -- src/test/java/de/fxlae/typeid/TypeIdTest.java | 154 ---------- 37 files changed, 2137 insertions(+), 562 deletions(-) create mode 100644 build-conventions/build.gradle.kts create mode 100644 build-conventions/settings.gradle.kts create mode 100644 build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.java-conventions.gradle.kts rename build.gradle.kts => build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.library-conventions.gradle.kts (60%) create mode 100644 gradle/libs.versions.toml create mode 100644 lib/java17/build.gradle.kts create mode 100644 lib/java17/gradle.properties create mode 100644 lib/java17/src/jmh/java/de/fxlae/typeid/TypeIdBench.java create mode 100644 lib/java17/src/main/java/de/fxlae/typeid/TypeId.java create mode 100644 lib/java17/src/main/java/de/fxlae/typeid/util/Validated.java create mode 100644 lib/java17/src/test/java/de/fxlae/typeid/SpecTest.java create mode 100644 lib/java17/src/test/java/de/fxlae/typeid/TypeIdFacade.java create mode 100644 lib/java17/src/test/java/de/fxlae/typeid/TypeIdTest.java create mode 100644 lib/java17/src/test/java/de/fxlae/typeid/util/ValidatedTest.java create mode 100644 lib/java8/build.gradle.kts create mode 100644 lib/java8/gradle.properties create mode 100644 lib/java8/src/main/java/de/fxlae/typeid/TypeId.java create mode 100644 lib/java8/src/test/java/de/fxlae/typeid/SpecTest8.java create mode 100644 lib/java8/src/test/java/de/fxlae/typeid/TypeIdFacade8.java create mode 100644 lib/java8/src/test/java/de/fxlae/typeid/TypeIdTest8.java create mode 100644 lib/shared/build.gradle.kts create mode 100644 lib/shared/src/main/java/de/fxlae/typeid/lib/TypeIdLib.java rename src/test/java/de/fxlae/typeid/SpecTest.java => lib/shared/src/test/java/de/fxlae/typeid/AbstractSpecTest.java (69%) create mode 100644 lib/shared/src/test/java/de/fxlae/typeid/AbstractTypeIdTest.java create mode 100644 lib/shared/src/test/java/de/fxlae/typeid/TypeIdInstance.java create mode 100644 lib/shared/src/test/java/de/fxlae/typeid/TypeIdStaticContext.java rename {src => lib/shared/src}/test/resources/spec/invalid.yml (91%) rename {src => lib/shared/src}/test/resources/spec/valid.yml (90%) delete mode 100644 src/main/java/de/fxlae/typeid/TypeId.java delete mode 100644 src/main/java/de/fxlae/typeid/UuidProvider.java delete mode 100644 src/test/java/de/fxlae/typeid/TypeIdTest.java diff --git a/README.md b/README.md index f3c1dd4..827970c 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,198 @@ # typeid-java -![example workflow](https://github.com/fxlae/typeid-java/actions/workflows/build-on-push.yml/badge.svg) +![Build Status](https://github.com/fxlae/typeid-java/actions/workflows/build-on-push.yml/badge.svg) ![Maven Central](https://img.shields.io/maven-central/v/de.fxlae/typeid-java) ![License Info](https://img.shields.io/github/license/fxlae/typeid-java) ## A Java implementation of [TypeID](https://github.com/jetpack-io/typeid). -TypeIDs are a modern, **type-safe**, globally unique identifier based on the upcoming +TypeIDs are a modern, type-safe, globally unique identifier based on the upcoming UUIDv7 standard. They provide a ton of nice properties that make them a great choice as the primary identifiers for your data in a database, APIs, and distributed systems. Read more about TypeIDs in their [spec](https://github.com/jetpack-io/typeid). ## Installation -Maven: +This library is designed to support all current LTS versions, including Java 8, whilst also making use of the features provided by the latest or upcoming Java versions. As a result, it is offered in two variants: + +- `typeid-java`: Requires at least Java 17. Opt for this one if the Java version is not a concern +- *OR* `typeid-java-jdk8`: Supports all versions from Java 8 onwards. It handles all relevant use cases, albeit with less syntactic sugar + +To install via Maven: ```xml de.fxlae - typeid-java - 0.1.0 + typeid-java + 0.2.0 ``` -Gradle: +For installation via Gradle: ```kotlin -implementation("de.fxlae:typeid-java:0.1.0") +implementation("de.fxlae:typeid-java:0.2.0") // or ...typeid-java-jdk8:0.2.0 ``` -## Requirements -- Java 8 or higher - ## Usage -An instance of `TypeID` can be obtained in several ways. It is immutable and thread-safe. Examples: -Generate a new `TypeID`, based on UUIDv7: +`TypeId` instances can be obtained in several ways. They are immutable and thread-safe. + +### Generating new TypeIDs + +To generate a new `TypeId`, based on UUIDv7 as per specification: + +```java +var typeId = TypeId.generate("user"); +typeId.toString(); // "user_01h455vb4pex5vsknk084sn02q" +typeId.prefix(); // "user" +typeId.uuid(); // java.util.UUID(01890a5d-ac96-774b-bcce-b302099a8057), based on UUIDv7 +``` + +To construct (or reconstruct) a `TypeId` from existing arguments, which can also be used as an "extension point" to plug-in custom UUID generators: ```java -TypeId typeId = TypeId.generate(); -typeId.getPrefix(); // "" -typeId.getUuid(); // v7, java.util.UUID(01890a5d-ac96-774b-bcce-b302099a8057) -typeId.toString(); // "01h455vb4pex5vsknk084sn02q" - -TypeId typeId = TypeId.generate("someprefix"); -typeId.getPrefix(); // "someprefix" -typeId.toString(); // "someprefix_01h455vb4pex5vsknk084sn02q" +var typeId = TypeId.of("user", UUID.randomUUID()); // a TypeId based on UUIDv4 ``` +### Parsing TypeID strings + +For parsing, the library supports both an imperative programming model and a more functional style. +The most straightforward way to parse the textual representation of a TypeID: -Construct a `TypeID` from arguments (any UUID version): ```java -TypeId typeId = TypeId.of("someprefix", UUID.randomUUID()); -typeId.getUuid(); // v4, java.util.UUID(9c8ec0e7-020b-4caf-87c0-38fb6c0ebbe2) +var typeId = TypeId.parse("user_01h455vb4pex5vsknk084sn02q"); ``` -Obtain an instance of `TypeID` from a text string (any UUID version): +Invalid inputs will result in an `IllegalArgumentException`, with a message explaining the cause of the parsing failure. If you prefer working with errors modeled as return values rather than exceptions, this is also possible (and is *much* more performant for untrusted input, as no stacktrace is involved at all): + ```java -TypeId typeId = TypeId.parse("01h455vb4pex5vsknk084sn02q"); -TypeId typeId = TypeId.parse("someprefix_01h455vb4pex5vsknk084sn02q"); +var maybeTypeId = TypeId.parseToOptional("user_01h455vb4pex5vsknk084sn02q"); + +// or, if you are interested in possible errors, provide handlers for success and failure +var maybeTypeId = TypeId.parse("...", + Optional::of, // (1) Function, called on success + message -> { // (2) Function, called on failure + log.warn("Parsing failed: {}", message); + return Optional.empty(); + }); +``` +**Everything shown so far works for both artifacts, `typeid-java` as well as `typeid-java-jdk8`. The following section is about features that are only available when using `typeid-java`**. + +When using `typeid-java`: +- the type `TypeId` is implemented as a Java `record` +- it has an additional method that *can* be used for parsing, `TypeId.parseToValidated`, which returns a "monadic-like" structure: `Validated`, or in this particular context, `Validated` + +`Validated` can be of subtype: +- `Valid`: encapsulates a successfully parsed `TypeId` +- or otherwise `Invalid`: contains an error message + +A simplistic method to interact with `Validated` is to manually unwrap it, analogous to `java.util.Optional.get`: + +```java +var validated = TypeId.parseToValidated("user_01h455vb4pex5vsknk084sn02q"); + +if(validated.isValid) { + var typeId = validated.get(); + // Proceed with typeId +} else { + var message = validated.message(); + // Optionally, do something with the error message (or omit this branch completely) +} +``` +Note: Checking `validated.isValid` is advisable for untrusted input. Similar to `Optional.get`, invoking `Validated.get` for invalid TypeIds (or `Validated.message` for valid TypeIds) will lead to a `NoSuchElementException`. + +A safe alternative involves methods that can be called without risk, namely: + +- For transformations: `map`, `flatMap`, `filter`, `orElse` +- For implementing side effects: `ifValid` and `ifInvalid` + +```java +// transform +var mappedToPrefix = TypeId.parseToValidated("user_01h455vb4pex5vsknk084sn02q"); + .map(TypeId::prefix) // Validated -> Validated + .filter("Not a cat! :(", prefix -> !"cat".equals(prefix)); // the predicate fails + +// execute side effects, e.g. logging +mappedToPrefix.ifValid(prefix -> log.info(prefix)) // called on success, so not in this case +mappedToPrefix.ifInvalid(message -> log.warn(message)) // logs "Not a cat! :(" +``` + +`Validated` and its implementations `Valid` and `Invalid` form a sealed type hierarchy. This feature becomes especially useful in future Java versions, beginning with Java 21, which will facilitate Record Patterns (destructuring) and Pattern Matching for switch: + +```java +// this compiles and runs with oracle openjdk-21-ea+30 (preview enabled) + +var report = switch(TypeId.parseToValidated("...")) { + case Valid(TypeId(var prefix, var uuid)) when "user".equals(prefix) -> "user with UUID" + uuid; + case Valid(TypeId(var prefix, _)) -> "Not a user, ignore the UUID. Prefix is " + prefix; + case Invalid(var message) -> "Parsing failed :( ... " + message; +} ``` +Note the absent (and superfluous) default case. Exhaustiveness is checked during compilation! + +## But wait, isn't this less type-safe than it could be? +
+ Details + +That's correct. The prefix of a TypeId is currently just a simple `String`. If you want to validate the prefix against a specific "type" of prefix, this subtly means you'll have to perform a string comparison. + +Here's how a more type-safe variant could look, which I have implemented experimentally (currently not included in the artifact): + +```java +TypeId typeId = TypeId<>.generate(USER); +TypeId anotherTypeId = TypeId<>.parse(USER, "user_01h455vb4pex5vsknk084sn02q"); +``` + +The downside to this approach is that each possible prefix type has to be defined manually. In particular, one must ensure that the embedded prefix name is syntactically correct: + +```java +static final User USER = new User(); +record User() implements TypedPrefix { + @Override + public String name() { + return "user"; + } +} +``` + +This method would still be an improvement, as it allows `TypeId`s to be passed around in the code in a type-safe manner. However, the preferred solution would be to validate the names of the prefix types at compile time. This solution is somewhat more complex and might require, for instance, the use of an annotation processor. + +If I find the motivation, I will complete the experimental version and integrate it as a separate variant into its own package (e.g., `..typed`), which can be used alternatively. +
+ +## A word on UUIDv7 +
+ Details + +TypeIDs are purposefully based on UUIDv7, one of several new UUID versions. UUIDs of version 7 begin with the current timestamp represented in the most significant bits, enabling their generation in a monotonically increasing order. This feature presents certain advantages, such as when using indexes in a database. Indexes based on B-Trees significantly benefit from monotonically ascending values. + +However, the [IETF specification for the new UUID versions](https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis) is a draft yet to be finalized, meaning modifications can still be introduced, including to UUIDv7. Additionally, the specification grants certain liberties in regards to the structure of a version 7 UUID. It must always commence with a timestamp (with a minimum precision of a millisecond, but potentially more if necessary), but in the least significant bits, aside from random values, it may or may not optionally include a counter and an InstanceId. + +For these reasons, this library uses a robust implementation of UUIDs for Java (as its only runtime-dependency) , specifically [java-uuid-generator (JUG)](https://github.com/cowtowncoder/java-uuid-generator). It adheres closely to the specification and, for instance, utilizes `SecureRandom` for generating random numbers, as strongly recommended by the specification (see [section 6.8](https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#section-6.8) of the sepcification). + +Nevertheless, as stated earlier, it is possible to use any other UUID generator implementation and/or UUID version by invoking `TypeId.of` instead of `TypeId.generate`. + +
+ +## Building From Source & Benchmarks +
+ Details -## Building From Source ```console foo@bar:~$ git clone https://github.com/fxlae/typeid-java.git foo@bar:~$ cd typeid-java foo@bar:~/typeid-java$ ./gradlew build -``` \ No newline at end of file +``` + +There is a small [JMH](https://github.com/openjdk/jmh) microbenchmark included: +```console +foo@bar:~/typeid-java$ ./gradlew jmh +``` + +In a single-threaded run, all operations perform in the range of millions of calls per second, which should be sufficient for most use cases (used setup: Eclipse Temurin 17 OpenJDK 64-Bit Server VM, AMD 2019gen CPU @ 3.6Ghz, 16GiB memory). + +| method | op/s | +|----------------------------------|----------------------:| +| `TypeId.generate` + `toString` | 9.1M | +| `TypeId.parse` | 9.8M | + +The library strives to avoid heap allocations as much as possible. The only allocations made are for return values and data from `SecureRandom`. +
\ No newline at end of file diff --git a/build-conventions/build.gradle.kts b/build-conventions/build.gradle.kts new file mode 100644 index 0000000..35419e0 --- /dev/null +++ b/build-conventions/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation("com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1") +} diff --git a/build-conventions/settings.gradle.kts b/build-conventions/settings.gradle.kts new file mode 100644 index 0000000..8c4ad09 --- /dev/null +++ b/build-conventions/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "build-conventions" \ No newline at end of file diff --git a/build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.java-conventions.gradle.kts b/build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.java-conventions.gradle.kts new file mode 100644 index 0000000..c3d9a55 --- /dev/null +++ b/build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.java-conventions.gradle.kts @@ -0,0 +1,32 @@ +plugins { + java + jacoco +} + +repositories { + mavenCentral() +} + +val versionCatalog = extensions.getByType().named("libs") + +dependencies { + versionCatalog.findLibrary("junit.bom").ifPresent { + testImplementation(platform(it)) + } + testImplementation("org.junit.jupiter:junit-jupiter") + versionCatalog.findLibrary("jackson.dataformat.yaml").ifPresent { + testImplementation(it) + } + versionCatalog.findLibrary("assertj.core").ifPresent { + testImplementation(it) + } +} + +tasks.withType { + options.encoding = "UTF-8" +} + +tasks.test { + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} diff --git a/build.gradle.kts b/build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.library-conventions.gradle.kts similarity index 60% rename from build.gradle.kts rename to build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.library-conventions.gradle.kts index 5732a1f..fe8de59 100644 --- a/build.gradle.kts +++ b/build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.library-conventions.gradle.kts @@ -2,34 +2,25 @@ plugins { `java-library` `maven-publish` signing + id("com.github.johnrengelman.shadow") } group = "de.fxlae" -version = "0.1.1-SNAPSHOT" +version = "0.2.0" -repositories { - mavenCentral() -} - -dependencies { - implementation("com.fasterxml.uuid:java-uuid-generator:4.2.0") - testImplementation(platform("org.junit:junit-bom:5.9.1")) - testImplementation("org.junit.jupiter:junit-jupiter") - testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2") -} - -tasks.compileJava { - options.release.set(8) -} - -tasks.test { - useJUnitPlatform() +java { + withJavadocJar() + withSourcesJar() } tasks.jar { manifest { - attributes(mapOf("Implementation-Title" to project.name, - "Implementation-Version" to project.version)) + attributes( + mapOf( + "Implementation-Title" to mavenArtifactId, + "Implementation-Version" to project.version + ) + ) } } @@ -39,27 +30,50 @@ tasks.javadoc { } } -java { - withJavadocJar() - withSourcesJar() +tasks.named("jar").configure { + enabled = false +} + +tasks.withType { + enabled = false +} + +val mavenArtifactId: String by project +val mavenArtifactDescription: String by project + +tasks { + shadowJar { + configurations = listOf(project.configurations.compileClasspath.get()) + include("de/fxlae/**") + from(project(":lib:shared").sourceSets.main.get().output) + archiveClassifier.set("") + archiveBaseName.set(mavenArtifactId) + } + build { + dependsOn(shadowJar) + } +} + +val providedConfigurationName = "provided" + +configurations { + create(providedConfigurationName) +} + +sourceSets { + main.get().compileClasspath += configurations.getByName(providedConfigurationName) + test.get().compileClasspath += configurations.getByName(providedConfigurationName) + test.get().runtimeClasspath += configurations.getByName(providedConfigurationName) } publishing { publications { create("mavenJava") { - artifactId = "typeid-java" + artifactId = mavenArtifactId from(components["java"]) - versionMapping { - usage("java-api") { - fromResolutionOf("runtimeClasspath") - } - usage("java-runtime") { - fromResolutionResult() - } - } pom { - name.set("typeid-java") - description.set("A TypeID implementation for Java") + name.set(mavenArtifactId) + description.set(mavenArtifactDescription) url.set("https://github.com/fxlae/typeid-java") licenses { license { @@ -99,4 +113,3 @@ signing { useInMemoryPgpKeys(signingKey, signingPassword) sign(publishing.publications["mavenJava"]) } - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..82a6d04 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +[versions] +java-uuid-generator = "4.2.0" +jackson = "2.15.2" +junit = "5.9.1" +assertj = "3.24.2" +shadow = "8.1.1" +jmh = "0.7.1" + +[plugins] +jmh = { id = "me.champeau.jmh", version.ref = "jmh" } + +[libraries] +java-uuid-generator = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "java-uuid-generator" } +junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } +jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } +shadow = { module = "com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin", version.ref = "shadow" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 41134 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 delta 38514 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

Ed3W8DKk=GWZ^H1%j3(j1_6^9H~ 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ad20ae9..62f495d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Fri Jun 30 20:28:51 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..fcb6fca 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +130,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +197,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in @@ -205,6 +213,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/lib/java17/build.gradle.kts b/lib/java17/build.gradle.kts new file mode 100644 index 0000000..17d9067 --- /dev/null +++ b/lib/java17/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("typeid.java-conventions") + id("typeid.library-conventions") + alias(libs.plugins.jmh) +} + +tasks.compileJava { + options.release.set(17) +} + +dependencies { + "provided"(project(":lib:shared")) + implementation(libs.java.uuid.generator) + testImplementation(project(path = ":lib:shared", configuration = "testArtifacts")) + jmh(project(":lib:shared")) +} + +jmh { + warmupIterations.set(3) + iterations.set(2) + threads.set(1) + fork.set(1) + //includes.set(listOf("TypeIdBench.parseWithError*")) +} diff --git a/lib/java17/gradle.properties b/lib/java17/gradle.properties new file mode 100644 index 0000000..759c4e6 --- /dev/null +++ b/lib/java17/gradle.properties @@ -0,0 +1,2 @@ +mavenArtifactId=typeid-java +mavenArtifactDescription=A TypeID implementation for Java \ No newline at end of file diff --git a/lib/java17/src/jmh/java/de/fxlae/typeid/TypeIdBench.java b/lib/java17/src/jmh/java/de/fxlae/typeid/TypeIdBench.java new file mode 100644 index 0000000..67ce821 --- /dev/null +++ b/lib/java17/src/jmh/java/de/fxlae/typeid/TypeIdBench.java @@ -0,0 +1,90 @@ +package de.fxlae.typeid; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.Optional; +import java.util.UUID; + +public class TypeIdBench { + + @Benchmark + public void parse(Blackhole bh, Inputs inputs) { + bh.consume(TypeId.parse(inputs.validTypeId)); + } + + @Benchmark + public void of(Blackhole bh, Inputs inputs) { + bh.consume(TypeId.of(inputs.prefix, inputs.uuid)); + } + + @Benchmark + public void generate(Blackhole bh, Inputs inputs) { + bh.consume(TypeId.generate(inputs.prefix)); + } + + @Benchmark + public void toString(Blackhole bh, Inputs inputs) { + bh.consume(inputs.typeId.toString()); + } + + @Benchmark + public void generateAndToString(Blackhole bh, Inputs inputs) { + TypeId typeId = TypeId.generate(inputs.prefix); + bh.consume(typeId.toString()); + } + + @Benchmark + public void ofAndToString(Blackhole bh, Inputs inputs) { + TypeId typeId = TypeId.of(inputs.prefix, inputs.uuid); + bh.consume(typeId.toString()); + } + + @Benchmark + public void parseWithErrorAsException(Blackhole bh, Inputs inputs) { + try { + TypeId.parse(inputs.invalidTypeId); + } catch (IllegalArgumentException e) { + bh.consume(e.getMessage()); + } + } + + @Benchmark + public void parseWithErrorAsValue(Blackhole bh, Inputs inputs) { + String result = TypeId.parse(inputs.invalidTypeId, + typeId -> null, + message -> message); + bh.consume(result); + } + + + public void xx(Blackhole bh, Inputs inputs) { + var maybeTypeId = TypeId.parse(inputs.invalidTypeId, + Optional::of, + message -> { + System.out.println(message); + return Optional.empty(); + }); + bh.consume(maybeTypeId); + } + + @State(Scope.Benchmark) + public static class Inputs { + + UUID uuid; + String validTypeId; + String invalidTypeId; + String prefix; + TypeId typeId; + + @Setup(Level.Trial) + public void setup() { + uuid = UUID.fromString("01890a5d-ac96-774b-bcce-b302099a8057"); + validTypeId = "prefix_01h455vb4pex5vsknk084sn02q"; + invalidTypeId = "prefix_01h455vb4pexÖvsknk084sn02q"; + prefix = "prefix"; + typeId = TypeId.of(prefix, uuid); + } + } + +} diff --git a/lib/java17/src/main/java/de/fxlae/typeid/TypeId.java b/lib/java17/src/main/java/de/fxlae/typeid/TypeId.java new file mode 100644 index 0000000..0c2d675 --- /dev/null +++ b/lib/java17/src/main/java/de/fxlae/typeid/TypeId.java @@ -0,0 +1,194 @@ +package de.fxlae.typeid; + +import de.fxlae.typeid.lib.TypeIdLib; +import de.fxlae.typeid.util.Validated; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; + +import static de.fxlae.typeid.lib.TypeIdLib.encode; + +/** + * A {@code record} for representing TypeIDs. + * + * @param prefix the prefix of the {@link TypeId} to create. Might be an empty string, but not null. + * @param uuid the {@link UUID} of the {@link TypeId} to create. + */ +public record TypeId(String prefix, UUID uuid) { + + /** + * @param prefix the prefix + * @param uuid the UUID + * @throws NullPointerException if the prefix and/or UUID is null + * @throws IllegalArgumentException if the prefix is invalid + */ + public TypeId { + + Objects.requireNonNull(prefix); + Objects.requireNonNull(uuid); + + String err = TypeIdLib.validatePrefixOnInput(prefix, prefix.length()); + if (err != TypeIdLib.VALID_REF) { + throw new IllegalArgumentException(err); + } + } + + /** + * Creates a new prefixed {@link TypeId} based on UUIDv7. + * + * @param prefix the prefix to use + * @return the new {@link TypeId} + * @throws NullPointerException if the prefix is null + * @throws IllegalArgumentException if the prefix is invalid + */ + public static TypeId generate(String prefix) { + return of(prefix, TypeIdLib.getUuidV7()); + } + + /** + * Creates a new {@link TypeId} without prefix, based on UUIDv7. + *

Note: "no prefix" means empty string, not null. + * + * @return the new {@link TypeId}. + */ + public static TypeId generate() { + return of("", TypeIdLib.getUuidV7()); + } + + /** + * Creates a new {@link TypeId} without prefix, based on the given {@link UUID}. + *

The {@link UUID} can be of any version. + *

Note: "no prefix" means empty string, not null. + * + * @param uuid the {@link UUID} to use + * @return the new {@link TypeId} + * @throws NullPointerException if the UUID is null + */ + public static TypeId of(UUID uuid) { + return of("", uuid); + } + + /** + * Creates a new {@link TypeId}, based on the given prefix and {@link UUID}. + * + * @param prefix the prefix to use + * @param uuid the {@link UUID} to use + * @return the new {@link TypeId} + * @throws NullPointerException if the prefix and/or UUID is null + * @throws IllegalArgumentException if the prefix is invalid + */ + public static TypeId of(String prefix, UUID uuid) { + return new TypeId(prefix, uuid); + } + + /** + * Parses the textual representation of a TypeID and returns a {@link TypeId} instance. + * + * @param text the textual representation. + * @return the new {@link TypeId}. + * @throws NullPointerException if the text is null + * @throws IllegalArgumentException if the text is invalid + */ + public static TypeId parse(final String text) { + + int separatorIndex = TypeIdLib.findSeparatorIndex(text); + String err = TypeIdLib.validateInput(text, separatorIndex); + + if (err != TypeIdLib.VALID_REF) { + throw new IllegalArgumentException(err); + } + + return new TypeId( + TypeIdLib.extractPrefix(text, separatorIndex), + TypeIdLib.decodeSuffixOnInput(text, separatorIndex)); + } + + /** + * Parses the textual representation of a TypeID and returns an {@link Optional}. + * + * @param text the textual representation of the TypeID + * @return an {@link Optional} containing a {@link TypeId} or an empty {@link TypeId} in case of validation errors + * @throws NullPointerException if the text is null + */ + public static Optional parseToOptional(final String text) { + + int separatorIndex = TypeIdLib.findSeparatorIndex(text); + String err = TypeIdLib.validateInput(text, separatorIndex); + + if (err != TypeIdLib.VALID_REF) { + return Optional.empty(); + } + + return Optional.of(new TypeId( + TypeIdLib.extractPrefix(text, separatorIndex), + TypeIdLib.decodeSuffixOnInput(text, separatorIndex))); + } + + /** + * Parses the textual representation of a TypeID and returns a {@link Validated}. + * + * @param text the textual representation of the TypeID + * @return a valid {@link Validated} containing a {@link TypeId} or an invalid {@link Validated} with an error message + * @throws NullPointerException if the text is null + */ + public static Validated parseToValidated(final String text) { + + int separatorIndex = TypeIdLib.findSeparatorIndex(text); + String err = TypeIdLib.validateInput(text, separatorIndex); + + if (err != TypeIdLib.VALID_REF) { + return Validated.invalid(err); + } + + return Validated.valid(new TypeId( + TypeIdLib.extractPrefix(text, separatorIndex), + TypeIdLib.decodeSuffixOnInput(text, separatorIndex))); + } + + /** + * Parses the textual representation of a TypeID and executes a handler {@link Function}, depending + * on the outcome. Both provided functions must have the same return type. + * + * @param text the textual representation of the TypeID + * @param okHandler the {@link Function} that is executed if the TypeID is valid, providing the {@link TypeId} + * @param errorHandler the {@link Function} that is executed if the TypeID could not be parsed, providing the error message + * @param the result type of the handler {@link Function} that was executed + * @return the result of the handler {@link Function} that was executed + * @throws NullPointerException if the okHandler and/or errorHandler is null + */ + public static T parse( + final String text, + Function okHandler, + Function errorHandler) { + + Objects.requireNonNull(okHandler); + Objects.requireNonNull(errorHandler); + + int separatorIndex = TypeIdLib.findSeparatorIndex(text); + String err = TypeIdLib.validateInput(text, separatorIndex); + + if (err != TypeIdLib.VALID_REF) { + return errorHandler.apply(err); + } + + TypeId typeId = new TypeId( + TypeIdLib.extractPrefix(text, separatorIndex), + TypeIdLib.decodeSuffixOnInput(text, separatorIndex)); + + return okHandler.apply(typeId); + } + + + /** + * Returns the textual representation of this {@link TypeId}. + * + * @return the textual representation. + */ + @Override + public String toString() { + return encode(prefix, uuid); + } + +} \ No newline at end of file diff --git a/lib/java17/src/main/java/de/fxlae/typeid/util/Validated.java b/lib/java17/src/main/java/de/fxlae/typeid/util/Validated.java new file mode 100644 index 0000000..d959bb6 --- /dev/null +++ b/lib/java17/src/main/java/de/fxlae/typeid/util/Validated.java @@ -0,0 +1,276 @@ +package de.fxlae.typeid.util; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A container for a value that is either valid or not. + * The two states are represented by {@link Valid} and {@link Invalid} respectively. + * In this library, in is used to represent the result of parsing a TypeId string. However, + * the data structure itself is completely independent of TypeIDs. + * + * @param the type of the contained value. + */ +public sealed interface Validated { + + /** + * Returns a new valid {@link Validated} with the given value. + * + * @param value the value to wrap + * @param the type of the value + * @return the valid {@link Validated} + * @throws NullPointerException if the provided value is null + */ + static Validated valid(T value) { + return new Valid<>(Objects.requireNonNull(value)); + } + + /** + * Returns a new invalid {@link Validated} with the given error message. + * + * @param message the error message + * @param the type of the invalid value + * @return the invalid {@link Validated} + * @throws NullPointerException if the message is null + */ + static Validated invalid(String message) { + return new Invalid<>(Objects.requireNonNull(message)); + } + + /** + * Returns an {@link Optional} with the valid value, otherwise an empty {@link Optional}. + * In the latter case, the error message is lost. + * + * @return the {@link Optional} + */ + Optional toOptional(); + + /** + * Returns {@code true} if the value is valid, otherwise {@code false}. + * + * @return {@code true} if the value is valid, otherwise {@code false} + */ + boolean isValid(); + + /** + * Returns the value if it's valid, otherwise throws. + * + * @return the valid value + * @throws NoSuchElementException if the value is invalid + */ + T get(); + + /** + * Returns the value if valid, otherwise return {@code other}. + * + * @param other the value to be returned if the value is invalid, can be {@code null} + * @return the value if valid, else {@code other} + */ + T orElse(T other); + + /** + * Returns the message if it's invalid, otherwise throws. + * + * @return the message + * @throws NoSuchElementException if the value is valid + */ + String message(); + + /** + * Applies the provided mapping function if the value is valid, otherwise returns the current instance + * + * @param mapper the mapping function + * @param The type of the mapping function's result + * @return the result of applying the mapping function to the value of this {@link Validated} instance + * @throws NullPointerException if the mapping function is null + */ + Validated map(Function mapper); + + /** + * Applies the provided mapping function if the value is valid, otherwise returns the current instance + * + * @param mapper the mapping function + * @param The type parameter of the mapping function's returned {@link Validated} + * @return the result of applying the mapping function to the value of this {@link Validated} instance + * @throws NullPointerException if the mapping function is null + */ + Validated flatMap(Function> mapper); + + /** + * If the value is valid and matches the predicate, return it as a valid + * {@link Validated}, otherwise return an invalid {@link Validated} with the given + * error message. If the value is invalid in the first place, return the current invalid + * {@link Validated} + * + * @param message the message in case the predicate doesn't match + * @param predicate the predicate that checks the value + * @return the resulting {@link Validated} + * @throws NullPointerException if the message and/or predicate is null + */ + Validated filter(String message, Predicate predicate); + + /** + * Applies the consuming function if the value is valid, otherwise does nothing. + * + * @param valueConsumer the value {@link Consumer} + * @throws NullPointerException if the {@link Consumer} is null + */ + void ifValid(Consumer valueConsumer); + + /** + * Applies the message consuming function if the value is invalid, otherwise does nothing. + * + * @param messageConsumer the message {@link Consumer} + * @throws NullPointerException if the {@link Consumer} is null + */ + void ifInvalid(Consumer messageConsumer); + + /** + * Implementation of a "valid {@link Validated}". + * + * @param value the value to wrap + * @param the type of the value + */ + record Valid(T value) implements Validated { + + /** + * @param value the value to wrap + * @throws NullPointerException if the value is null + */ + public Valid { + Objects.requireNonNull(value); + } + + @Override + public Optional toOptional() { + return Optional.of(value); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public T get() { + return value; + } + + @Override + public T orElse(T other) { + return value; + } + + @Override + public String message() { + throw new NoSuchElementException("no message, element is valid"); + } + + @Override + public Validated map(Function mapper) { + Objects.requireNonNull(mapper); + return valid(mapper.apply(value)); + } + + @Override + public Validated flatMap(Function> mapper) { + Objects.requireNonNull(mapper); + return mapper.apply(value); + } + + @Override + public Validated filter(String message, Predicate predicate) { + Objects.requireNonNull(message); + Objects.requireNonNull(predicate); + if (predicate.test(value)) { + return this; + } else { + return invalid(message); + } + } + + @Override + public void ifValid(Consumer valueConsumer) { + Objects.requireNonNull(valueConsumer); + valueConsumer.accept(value); + } + + @Override + public void ifInvalid(Consumer messageConsumer) { + Objects.requireNonNull(messageConsumer); + // do nothing + } + } + + /** + * Implementation of an "invalid {@link Validated}". + * + * @param message the error message + */ + record Invalid(String message) implements Validated { + + /** + * @param message the error message + * @throws NullPointerException if the value is null + */ + public Invalid { + Objects.requireNonNull(message); + } + + @Override + public Optional toOptional() { + return Optional.empty(); + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public T get() { + throw new NoSuchElementException("Validation failed: " + message); + } + + @Override + public T orElse(T other) { + return other; + } + + @Override + public Validated map(Function mapper) { + Objects.requireNonNull(mapper); + return invalid(message); + } + + @Override + public Validated flatMap(Function> mapper) { + Objects.requireNonNull(mapper); + return invalid(message); + } + + @Override + public Validated filter(String message, Predicate predicate) { + Objects.requireNonNull(message); + Objects.requireNonNull(predicate); + return this; + } + + @Override + public void ifValid(Consumer valueConsumer) { + Objects.requireNonNull(valueConsumer); + // do nothing + } + + @Override + public void ifInvalid(Consumer messageConsumer) { + Objects.requireNonNull(messageConsumer); + messageConsumer.accept(message); + } + } + +} diff --git a/lib/java17/src/test/java/de/fxlae/typeid/SpecTest.java b/lib/java17/src/test/java/de/fxlae/typeid/SpecTest.java new file mode 100644 index 0000000..6beddf6 --- /dev/null +++ b/lib/java17/src/test/java/de/fxlae/typeid/SpecTest.java @@ -0,0 +1,9 @@ +package de.fxlae.typeid; + +class SpecTest extends AbstractSpecTest { + + @Override + TypeIdStaticContext createStaticFacade() { + return new TypeIdFacade(); + } +} \ No newline at end of file diff --git a/lib/java17/src/test/java/de/fxlae/typeid/TypeIdFacade.java b/lib/java17/src/test/java/de/fxlae/typeid/TypeIdFacade.java new file mode 100644 index 0000000..fdfcf7f --- /dev/null +++ b/lib/java17/src/test/java/de/fxlae/typeid/TypeIdFacade.java @@ -0,0 +1,70 @@ +package de.fxlae.typeid; + +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; + +public class TypeIdFacade implements TypeIdStaticContext { + @Override + public TypeIdInstance generate(String prefix) { + return wrap(TypeId.generate(prefix)); + } + + @Override + public TypeIdInstance generate() { + return wrap(TypeId.generate()); + } + + @Override + public TypeIdInstance of(UUID uuid) { + return wrap(TypeId.of(uuid)); + } + + @Override + public TypeIdInstance of(String prefix, UUID uuid) { + return wrap(TypeId.of(prefix, uuid)); + } + + @Override + public TypeIdInstance parse(String text) { + return wrap(TypeId.parse(text)); + } + + @Override + public Optional parseToOptional(String text) { + return TypeId.parseToOptional(text).map(this::wrap); + } + + @Override + public O parse(String text, Function okHandler, Function errorHandler) { + Function wrapAsFunction = (this::wrap); + return TypeId.parse(text, wrapAsFunction.andThen(okHandler), errorHandler); + } + + private TypeIdInstance wrap(final TypeId typeId) { + + return new TypeIdInstance() { + + @Override + public String prefix() { + return typeId.prefix(); + } + + @Override + public UUID uuid() { + return typeId.uuid(); + } + + @Override + public String toString() { + return typeId.toString(); + } + + @Override + public Object getWrapped() { + return typeId; + } + + }; + } +} diff --git a/lib/java17/src/test/java/de/fxlae/typeid/TypeIdTest.java b/lib/java17/src/test/java/de/fxlae/typeid/TypeIdTest.java new file mode 100644 index 0000000..7103c4c --- /dev/null +++ b/lib/java17/src/test/java/de/fxlae/typeid/TypeIdTest.java @@ -0,0 +1,29 @@ +package de.fxlae.typeid; + +import de.fxlae.typeid.util.Validated; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TypeIdTest extends AbstractTypeIdTest { + + @Override + TypeIdStaticContext createStaticFacade() { + return new TypeIdFacade(); + } + + @Test + void shouldReturnAValidValidatedOnParseSuccess() { + Validated validated = TypeId.parseToValidated(AbstractTypeIdTest.SOME_TYPE_ID); + assertThat(validated.isValid()).isTrue(); + assertThat(validated.get().prefix()).isEqualTo(SOME_PREFIX); + assertThat(validated.get().uuid()).isEqualTo(SOME_UUID); + } + + @Test + void shouldReturnAnInvalidValidatedOnParseFailure() { + Validated validated = TypeId.parseToValidated("some invalid typeid"); + assertThat(validated.isValid()).isFalse(); + assertThat(validated.message()).contains("illegal length"); + } +} diff --git a/lib/java17/src/test/java/de/fxlae/typeid/util/ValidatedTest.java b/lib/java17/src/test/java/de/fxlae/typeid/util/ValidatedTest.java new file mode 100644 index 0000000..8e2218b --- /dev/null +++ b/lib/java17/src/test/java/de/fxlae/typeid/util/ValidatedTest.java @@ -0,0 +1,201 @@ +package de.fxlae.typeid.util; + +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ValidatedTest { + + static final String VALUE = "the value"; + static final String ERROR_MESSAGE = "the error message"; + + static final Validated VALID_VALIDATED = Validated.valid(VALUE); + static final Validated INVALID_VALIDATED = Validated.invalid(ERROR_MESSAGE); + + @Test + void validShouldCreateValidInstanceWhenValid() { + assertThat(VALID_VALIDATED) + .isNotNull() + .isInstanceOfSatisfying( + Validated.Valid.class, + v -> assertThat(v.value()).isEqualTo(VALUE)); + } + + @Test + void invalidShouldCreateInvalidInstanceWhenInvalid() { + assertThat(INVALID_VALIDATED) + .isNotNull() + .isInstanceOfSatisfying( + Validated.Invalid.class, + iv -> assertThat(iv.message()).isEqualTo(ERROR_MESSAGE)); + } + + @Test + void isValidShouldReturnTrueWhenValid() { + assertThat(VALID_VALIDATED.isValid()).isTrue(); + } + + @Test + void isValidShouldReturnFalseWhenInvalid() { + assertThat(INVALID_VALIDATED.isValid()).isFalse(); + } + + @Test + void toOptionalShouldReturnNonEmptyOptionalWhenValid() { + assertThat(VALID_VALIDATED.toOptional()).contains(VALUE); + } + + @Test + void toOptionalShouldReturnEmptyOptionalWhenInvalid() { + assertThat(INVALID_VALIDATED.toOptional()).isEmpty(); + } + + @Test + void getShouldReturnValueWhenValid() { + assertThat(VALID_VALIDATED.get()).isEqualTo(VALUE); + } + + @Test + void getShouldThrowWhenInvalid() { + assertThatThrownBy(INVALID_VALIDATED::get) + .isInstanceOf(NoSuchElementException.class) + .hasMessageContaining(ERROR_MESSAGE); + } + + @Test + void orElseShouldReturnValueWhenValid() { + assertThat(VALID_VALIDATED.orElse("other")).isEqualTo(VALUE); + } + + @Test + void orElseShouldReturnOtherWhenValid() { + assertThat(INVALID_VALIDATED.orElse("other")).isEqualTo("other"); + assertThat(INVALID_VALIDATED.orElse(null)).isNull(); + } + + @Test + void messageShouldReturnMessageWhenInvalid() { + assertThat(INVALID_VALIDATED.message()).isEqualTo(ERROR_MESSAGE); + } + + @Test + void messageShouldThrowWhenValid() { + assertThatThrownBy(VALID_VALIDATED::message) + .isInstanceOf(NoSuchElementException.class); + } + + @Test + void mapShouldApplyWhenValid() { + Validated validated = Validated.valid(1); + Validated mapped = validated.map(Object::toString); + assertThat(mapped.get()).isEqualTo("1"); + } + + @Test + void mapShouldNotApplyWhenInvalid() { + Validated mapped = INVALID_VALIDATED.map(Object::toString); + assertThat(mapped).isInstanceOfSatisfying( + Validated.Invalid.class, + iv -> assertThat(iv.message()).isEqualTo(ERROR_MESSAGE)); + } + + @Test + void flatMapShouldApplyWhenValid() { + Validated validated = Validated.valid(1); + Validated mapped1 = validated.flatMap(i -> Validated.valid(i.toString())); + Validated mapped2 = validated.flatMap(i -> Validated.invalid(ERROR_MESSAGE)); + + assertThat(mapped1.get()).isEqualTo("1"); + assertThat(mapped2).isInstanceOfSatisfying( + Validated.Invalid.class, + iv -> assertThat(iv.message()).isEqualTo(ERROR_MESSAGE)); + } + + @Test + void flatMapShouldNotApplyWhenInvalid() { + Validated mapped = INVALID_VALIDATED.flatMap(Validated::valid); + assertThat(mapped).isInstanceOfSatisfying( + Validated.Invalid.class, + iv -> assertThat(iv.message()).isEqualTo(ERROR_MESSAGE)); + } + + @Test + void filterShouldApplyForMatchingPredicateWhenValid() { + Validated filtered = VALID_VALIDATED.filter(ERROR_MESSAGE, v -> v.equals(VALUE)); + assertThat(filtered).isEqualTo(VALID_VALIDATED); + } + + @Test + void filterShouldNotApplyForNotMatchingPredicateWhenValid() { + Validated filtered = VALID_VALIDATED.filter(ERROR_MESSAGE, v -> v.equals("something else")); + assertThat(filtered).isInstanceOfSatisfying( + Validated.Invalid.class, + iv -> assertThat(iv.message()).isEqualTo(ERROR_MESSAGE)); + } + + @Test + void filterShouldNotApplyWhenInvalid() { + Validated filtered = INVALID_VALIDATED.filter("yet another " + ERROR_MESSAGE, v -> v.equals("something else")); + assertThat(filtered).isInstanceOfSatisfying( + Validated.Invalid.class, + iv -> assertThat(iv.message()).isEqualTo(ERROR_MESSAGE)); + } + + @Test + void ifValidShouldExecuteWhenValid() { + AtomicReference str = new AtomicReference<>(null); + VALID_VALIDATED.ifValid(str::set); + assertThat(str.get()).isEqualTo(VALUE); + } + + @Test + void ifValidShouldNotExecuteWhenInvalid() { + AtomicReference str = new AtomicReference<>(null); + INVALID_VALIDATED.ifValid(str::set); + assertThat(str.get()).isNull(); + } + + @Test + void ifInvalidShouldExecuteWhenInvalid() { + AtomicReference str = new AtomicReference<>(null); + INVALID_VALIDATED.ifInvalid(str::set); + assertThat(str.get()).isEqualTo(ERROR_MESSAGE); + } + + @Test + void ifInvalidShouldNotExecuteWhenValid() { + AtomicReference str = new AtomicReference<>(null); + VALID_VALIDATED.ifInvalid(str::set); + assertThat(str.get()).isNull(); + } + + @Test + void shouldThrowWhenCalledWithNullParams() { + + assertThatThrownBy(() -> Validated.valid(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> Validated.invalid(null)).isInstanceOf(NullPointerException.class); + + assertThatThrownBy(() -> VALID_VALIDATED.filter(null, v -> v.equals(VALUE))).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> INVALID_VALIDATED.filter(null, v -> v.equals(VALUE))).isInstanceOf(NullPointerException.class); + + assertThatThrownBy(() -> VALID_VALIDATED.filter(ERROR_MESSAGE, null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> INVALID_VALIDATED.filter(ERROR_MESSAGE, null)).isInstanceOf(NullPointerException.class); + + assertThatThrownBy(() -> VALID_VALIDATED.map(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> INVALID_VALIDATED.map(null)).isInstanceOf(NullPointerException.class); + + assertThatThrownBy(() -> VALID_VALIDATED.flatMap(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> INVALID_VALIDATED.flatMap(null)).isInstanceOf(NullPointerException.class); + + assertThatThrownBy(() -> VALID_VALIDATED.ifValid(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> INVALID_VALIDATED.ifValid(null)).isInstanceOf(NullPointerException.class); + + assertThatThrownBy(() -> VALID_VALIDATED.ifInvalid(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> INVALID_VALIDATED.ifInvalid(null)).isInstanceOf(NullPointerException.class); + } + +} diff --git a/lib/java8/build.gradle.kts b/lib/java8/build.gradle.kts new file mode 100644 index 0000000..c2f608a --- /dev/null +++ b/lib/java8/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("typeid.java-conventions") + id("typeid.library-conventions") +} + +tasks.compileJava { + options.release.set(8) +} + +dependencies { + "provided"(project(":lib:shared")) + implementation(libs.java.uuid.generator) + testImplementation(project(path = ":lib:shared", configuration = "testArtifacts")) +} diff --git a/lib/java8/gradle.properties b/lib/java8/gradle.properties new file mode 100644 index 0000000..6278003 --- /dev/null +++ b/lib/java8/gradle.properties @@ -0,0 +1,2 @@ +mavenArtifactId=typeid-java-jdk8 +mavenArtifactDescription=A TypeID implementation for Java (JDK8 compat) \ No newline at end of file diff --git a/lib/java8/src/main/java/de/fxlae/typeid/TypeId.java b/lib/java8/src/main/java/de/fxlae/typeid/TypeId.java new file mode 100644 index 0000000..f88473d --- /dev/null +++ b/lib/java8/src/main/java/de/fxlae/typeid/TypeId.java @@ -0,0 +1,227 @@ +package de.fxlae.typeid; + +import de.fxlae.typeid.lib.TypeIdLib; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; + +import static de.fxlae.typeid.lib.TypeIdLib.encode; + +/** + * A type for representing TypeIDs. + */ +public final class TypeId { + + private final UUID uuid; + + private final String prefix; + + /** + * Creates new {@link TypeId} + * + * @param prefix the prefix + * @param uuid the UUID + * @throws NullPointerException if the prefix and/or UUID is null + * @throws IllegalArgumentException if the prefix is invalid + */ + private TypeId(String prefix, UUID uuid) { + + Objects.requireNonNull(prefix); + Objects.requireNonNull(uuid); + + String err = TypeIdLib.validatePrefixOnInput(prefix, prefix.length()); + if (err != TypeIdLib.VALID_REF) { + throw new IllegalArgumentException(err); + } + + this.prefix = prefix; + this.uuid = uuid; + } + + /** + * Creates a new prefixed {@link TypeId} based on UUIDv7. + * + * @param prefix the prefix to use + * @return the new {@link TypeId} + * @throws NullPointerException if the prefix is null + * @throws IllegalArgumentException if the prefix is invalid + */ + public static TypeId generate(String prefix) { + return of(prefix, TypeIdLib.getUuidV7()); + } + + /** + * Creates a new {@link TypeId} without prefix, based on UUIDv7. + *

Note: "no prefix" means empty string, not null. + * + * @return the new {@link TypeId}. + */ + public static TypeId generate() { + return of("", TypeIdLib.getUuidV7()); + } + + /** + * Creates a new {@link TypeId} without prefix, based on the given {@link UUID}. + *

The {@link UUID} can be of any version. + *

Note: "no prefix" means empty string, not null. + * + * @param uuid the {@link UUID} to use + * @return the new {@link TypeId} + * @throws NullPointerException if the UUID is null + */ + public static TypeId of(UUID uuid) { + return of("", uuid); + } + + /** + * Creates a new {@link TypeId}, based on the given prefix and {@link UUID}. + * + * @param prefix the prefix to use + * @param uuid the {@link UUID} to use + * @return the new {@link TypeId} + * @throws NullPointerException if the prefix and/or UUID is null + * @throws IllegalArgumentException if the prefix is invalid + */ + public static TypeId of(String prefix, UUID uuid) { + return new TypeId(prefix, uuid); + } + + /** + * Parses the textual representation of a TypeID and returns a {@link TypeId} instance. + * + * @param text the textual representation. + * @return the new {@link TypeId}. + * @throws NullPointerException if the text is null + * @throws IllegalArgumentException if the text is invalid + */ + public static TypeId parse(final String text) { + + int separatorIndex = TypeIdLib.findSeparatorIndex(text); + String err = TypeIdLib.validateInput(text, separatorIndex); + + if (err != TypeIdLib.VALID_REF) { + throw new IllegalArgumentException(err); + } + + return new TypeId( + TypeIdLib.extractPrefix(text, separatorIndex), + TypeIdLib.decodeSuffixOnInput(text, separatorIndex)); + } + + /** + * Parses the textual representation of a TypeID and executes a handler {@link Function}, depending + * on the outcome. Both provided functions must have the same return type. + * + * @param text the textual representation of the TypeID + * @param okHandler the {@link Function} that is executed if the TypeID is valid, providing the {@link TypeId} + * @param errorHandler the {@link Function} that is executed if the TypeID could not be parsed, providing the error message + * @param the result type of the handler {@link Function} that was executed + * @return the result of the handler {@link Function} that was executed + * @throws NullPointerException if the okHandler and/or errorHandler is null + */ + public static T parse( + final String text, + Function okHandler, + Function errorHandler) { + + Objects.requireNonNull(okHandler); + Objects.requireNonNull(errorHandler); + + int separatorIndex = TypeIdLib.findSeparatorIndex(text); + String err = TypeIdLib.validateInput(text, separatorIndex); + + if (err != TypeIdLib.VALID_REF) { + return errorHandler.apply(err); + } + + TypeId typeId = new TypeId( + TypeIdLib.extractPrefix(text, separatorIndex), + TypeIdLib.decodeSuffixOnInput(text, separatorIndex)); + + return okHandler.apply(typeId); + } + + /** + * Parses the textual representation of a TypeID and returns an {@link Optional}. + * + * @param text the textual representation of the TypeID + * @return an {@link Optional} containing a {@link TypeId} or an empty {@link TypeId} in case of validation errors + * @throws NullPointerException if the text is null + */ + public static Optional parseToOptional(final String text) { + + int separatorIndex = TypeIdLib.findSeparatorIndex(text); + String err = TypeIdLib.validateInput(text, separatorIndex); + + if (err != TypeIdLib.VALID_REF) { + return Optional.empty(); + } + + return Optional.of(new TypeId( + TypeIdLib.extractPrefix(text, separatorIndex), + TypeIdLib.decodeSuffixOnInput(text, separatorIndex))); + } + + /** + * Returns the prefix of this {@link TypeId}. + * + * @return the prefix + */ + public String getPrefix() { + return prefix; + } + + /** + * Returns the prefix of this {@link TypeId}. + *

This is an alias for {@link #getPrefix()} + * + * @return the prefix + */ + public String prefix() { + return getPrefix(); + } + + /** + * Returns the underlying {@link UUID} of this {@link TypeId}. + * + * @return the {@link UUID} + */ + public UUID getUuid() { + return uuid; + } + + /** + * Returns the underlying {@link UUID} of this {@link TypeId}. + *

This is an alias for {@link #getUuid()} + * + * @return the {@link UUID}. + */ + public UUID uuid() { + return getUuid(); + } + + /** + * Returns the textual representation of this {@link TypeId}. + * + * @return the textual representation. + */ + @Override + public String toString() { + return encode(prefix, uuid); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TypeId typeId = (TypeId) o; + return Objects.equals(uuid, typeId.uuid) && Objects.equals(prefix, typeId.prefix); + } + + @Override + public int hashCode() { + return Objects.hash(uuid, prefix); + } +} diff --git a/lib/java8/src/test/java/de/fxlae/typeid/SpecTest8.java b/lib/java8/src/test/java/de/fxlae/typeid/SpecTest8.java new file mode 100644 index 0000000..c6fdb78 --- /dev/null +++ b/lib/java8/src/test/java/de/fxlae/typeid/SpecTest8.java @@ -0,0 +1,9 @@ +package de.fxlae.typeid; + +class SpecTest8 extends AbstractSpecTest { + + @Override + TypeIdStaticContext createStaticFacade() { + return new TypeIdFacade8(); + } +} \ No newline at end of file diff --git a/lib/java8/src/test/java/de/fxlae/typeid/TypeIdFacade8.java b/lib/java8/src/test/java/de/fxlae/typeid/TypeIdFacade8.java new file mode 100644 index 0000000..563bb47 --- /dev/null +++ b/lib/java8/src/test/java/de/fxlae/typeid/TypeIdFacade8.java @@ -0,0 +1,72 @@ +package de.fxlae.typeid; + +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; + +public class TypeIdFacade8 implements TypeIdStaticContext { + + @Override + public TypeIdInstance generate(String prefix) { + return wrap(TypeId.generate(prefix)); + } + + @Override + public TypeIdInstance generate() { + return wrap(TypeId.generate()); + } + + @Override + public TypeIdInstance of(UUID uuid) { + return wrap(TypeId.of(uuid)); + } + + @Override + public TypeIdInstance of(String prefix, UUID uuid) { + return wrap(TypeId.of(prefix, uuid)); + } + + @Override + public TypeIdInstance parse(String text) { + return wrap(TypeId.parse(text)); + } + + @Override + public Optional parseToOptional(String text) { + return TypeId.parseToOptional(text).map(this::wrap); + } + + @Override + public O parse(String text, Function okHandler, Function errorHandler) { + Function wrapAsFunction = (this::wrap); + return TypeId.parse(text, wrapAsFunction.andThen(okHandler), errorHandler); + } + + private TypeIdInstance wrap(final TypeId typeId) { + + return new TypeIdInstance() { + + @Override + public String prefix() { + return typeId.prefix(); + } + + @Override + public UUID uuid() { + return typeId.uuid(); + } + + @Override + public String toString() { + return typeId.toString(); + } + + @Override + public Object getWrapped() { + return typeId; + } + + }; + } + +} diff --git a/lib/java8/src/test/java/de/fxlae/typeid/TypeIdTest8.java b/lib/java8/src/test/java/de/fxlae/typeid/TypeIdTest8.java new file mode 100644 index 0000000..d904878 --- /dev/null +++ b/lib/java8/src/test/java/de/fxlae/typeid/TypeIdTest8.java @@ -0,0 +1,9 @@ +package de.fxlae.typeid; + +public class TypeIdTest8 extends AbstractTypeIdTest { + + @Override + TypeIdStaticContext createStaticFacade() { + return new TypeIdFacade8(); + } +} diff --git a/lib/shared/build.gradle.kts b/lib/shared/build.gradle.kts new file mode 100644 index 0000000..6019a6b --- /dev/null +++ b/lib/shared/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("typeid.java-conventions") +} + +tasks.compileJava { + options.release.set(8) +} + +configurations { + create("testArtifacts") +} + +tasks.register("testJar") { + from(sourceSets["test"].output) + archiveClassifier.set("tests") +} + +artifacts { + add("testArtifacts", tasks["testJar"]) +} + +dependencies { + implementation("com.fasterxml.uuid:java-uuid-generator:4.2.0") +} diff --git a/lib/shared/src/main/java/de/fxlae/typeid/lib/TypeIdLib.java b/lib/shared/src/main/java/de/fxlae/typeid/lib/TypeIdLib.java new file mode 100644 index 0000000..c7e85ec --- /dev/null +++ b/lib/shared/src/main/java/de/fxlae/typeid/lib/TypeIdLib.java @@ -0,0 +1,233 @@ +package de.fxlae.typeid.lib; + +import com.fasterxml.uuid.Generators; +import com.fasterxml.uuid.impl.TimeBasedEpochGenerator; + +import java.util.Objects; +import java.util.UUID; + +public final class TypeIdLib { + + public static final String VALID_REF = "VALID_REF"; + private static final char SEPARATOR = '_'; + private static final int PREFIX_MAX_LENGTH = 63; + private static final String SUFFIX_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz"; + private static final int SUFFIX_LENGTH = 26; + + // inspired by base32.go from the official go implementation + // https://github.com/jetpack-io/typeid-go/blob/main/base32/base32.go + // lookup: [ascii pos] -> binary representation of block + + // sentinel value for characters that are not part of the alphabets + private static final long NOOP = Long.MAX_VALUE; + + // these values currently are longs because they are directly shifted into the two longs + // of a UUID + private static final long[] SUFFIX_LOOKUP = new long[]{ + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, 0x00, 0x01, // 0, 1 + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, NOOP, NOOP, // 2, 3, 4, 5, 6, 7, 8, 9 + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, 0x0A, 0x0B, 0x0C, // a, b, c + 0x0D, 0x0E, 0x0F, 0x10, 0x11, NOOP, 0x12, 0x13, NOOP, 0x14, // d, e, f, g, h, j, k, m + 0x15, NOOP, 0x16, 0x17, 0x18, 0x19, 0x1A, NOOP, 0x1B, 0x1C, // n, p, q, r, s, value, v, w + 0x1D, 0x1E, 0x1F, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, // x, y, z + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, NOOP, + NOOP, NOOP, NOOP, NOOP, NOOP, NOOP + }; + private static final TimeBasedEpochGenerator generator = Generators.timeBasedEpochGenerator(); + + private TypeIdLib() { + } + + public static UUID decodeSuffixOnInput(final String input, final int separatorIndex) { + + final int start = (separatorIndex == -1) ? 0 : separatorIndex + 1; + + long lsb = 0; + long msb = 0; + + // decode characters [25] to [14] into the LSBs + lsb |= (SUFFIX_LOOKUP[input.charAt(25 + start)]); + lsb |= (SUFFIX_LOOKUP[input.charAt(24 + start)]) << 5; + lsb |= (SUFFIX_LOOKUP[input.charAt(23 + start)]) << 10; + lsb |= (SUFFIX_LOOKUP[input.charAt(22 + start)]) << 15; + lsb |= (SUFFIX_LOOKUP[input.charAt(21 + start)]) << 20; + lsb |= (SUFFIX_LOOKUP[input.charAt(20 + start)]) << 25; + lsb |= (SUFFIX_LOOKUP[input.charAt(19 + start)]) << 30; + lsb |= (SUFFIX_LOOKUP[input.charAt(18 + start)]) << 35; + lsb |= (SUFFIX_LOOKUP[input.charAt(17 + start)]) << 40; + lsb |= (SUFFIX_LOOKUP[input.charAt(16 + start)]) << 45; + lsb |= (SUFFIX_LOOKUP[input.charAt(15 + start)]) << 50; + lsb |= (SUFFIX_LOOKUP[input.charAt(14 + start)]) << 55; + + // decode the overlap between LSBs and MSBs (character [13]) + long bitsAtOverlap = SUFFIX_LOOKUP[input.charAt(13 + start)]; + lsb |= (bitsAtOverlap & 0xF) << 60; + msb |= (bitsAtOverlap & 0x10) >>> 4; + + // decode characters [12] to [0] into the MSBs + msb |= (SUFFIX_LOOKUP[input.charAt(12 + start)]) << 1; + msb |= (SUFFIX_LOOKUP[input.charAt(11 + start)]) << 6; + msb |= (SUFFIX_LOOKUP[input.charAt(10 + start)]) << 11; + msb |= (SUFFIX_LOOKUP[input.charAt(9 + start)]) << 16; + msb |= (SUFFIX_LOOKUP[input.charAt(8 + start)]) << 21; + msb |= (SUFFIX_LOOKUP[input.charAt(7 + start)]) << 26; + msb |= (SUFFIX_LOOKUP[input.charAt(6 + start)]) << 31; + msb |= (SUFFIX_LOOKUP[input.charAt(5 + start)]) << 36; + msb |= (SUFFIX_LOOKUP[input.charAt(4 + start)]) << 41; + msb |= (SUFFIX_LOOKUP[input.charAt(3 + start)]) << 46; + msb |= (SUFFIX_LOOKUP[input.charAt(2 + start)]) << 51; + msb |= (SUFFIX_LOOKUP[input.charAt(1 + start)]) << 56; + msb |= (SUFFIX_LOOKUP[input.charAt(start)]) << 61; + + return new UUID(msb, lsb); + } + + public static String encode(final String prefix, final UUID uuid) { + + final long msb = uuid.getMostSignificantBits(); + final long lsb = uuid.getLeastSignificantBits(); + + StringBuilder sb; + if (prefix.isEmpty()) { + sb = new StringBuilder(26); + } else { + sb = new StringBuilder(27 + prefix.length()); + sb.append(prefix).append(SEPARATOR); + } + + // encode the MSBs except the last bit, as the block it belongs to overlaps with the LSBs + sb.append(SUFFIX_ALPHABET.charAt((int) (msb >>> 61) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 56) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 51) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 46) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 41) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 36) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 31) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 26) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 21) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 16) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 11) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 6) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (msb >>> 1) & 0x1F)); + + // encode the overlap between MSBs (1 bit) and LSBs (4 bits) + long overlap = ((msb & 0x1) << 4) | (lsb >>> 60); + sb.append(SUFFIX_ALPHABET.charAt((int) overlap)); + + // encode the rest of LSBs + sb.append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 55) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 50) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 45) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 40) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 35) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 30) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 25) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 20) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 15) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 10) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) (lsb >>> 5) & 0x1F)) + .append(SUFFIX_ALPHABET.charAt((int) lsb & 0x1F)); + + return sb.toString(); + } + + public static int findSeparatorIndex(String input) { + return (input == null) ? -1 : input.lastIndexOf(SEPARATOR); + } + + public static String extractPrefix(String input, int separatorIndex) { + if (separatorIndex == -1) { + return ""; + } else { + return input.substring(0, separatorIndex); + } + } + + public static String validateInput(String input, int separatorIndex) { + + Objects.requireNonNull(input); + + if (input.isEmpty()) { + return "Provided TypeId must not be empty"; + } + + // empty prefix, but with unexpected separator + if (separatorIndex == 0) { + return "TypeId with empty prefix must not contain the separator '_'"; + } + + String suffixErr = TypeIdLib.validateSuffixOnInput(input, separatorIndex); + if (suffixErr != TypeIdLib.VALID_REF) { + return suffixErr; + } + + return TypeIdLib.validatePrefixOnInput(input, separatorIndex); + } + + // validates the suffix without creating an intermediary object for it + private static String validateSuffixOnInput(final String input, final int separatorIndex) { + + final int start = (separatorIndex != -1) ? separatorIndex + 1 : 0; + + if (input.length() - start != SUFFIX_LENGTH) { + return "Suffix with illegal length, must be " + SUFFIX_LENGTH; + } + + if (((SUFFIX_LOOKUP[input.charAt(start)] >>> 3) & 0x3) > 0) { + return "Illegal leftmost suffix character, must be one of [01234567]"; + } + + for (int i = start; i < input.length(); i++) { + char c = input.charAt(i); + if (c >= SUFFIX_LOOKUP.length || SUFFIX_LOOKUP[c] == NOOP) { + return "Illegal character in suffix, must be one of [" + SUFFIX_ALPHABET + "]"; + } + } + + return VALID_REF; + } + + // validates the prefix without creating an intermediary object for it + public static String validatePrefixOnInput(final String input, final int separatorIndex) { + + // empty prefix, no separator + if (separatorIndex == -1) { + return VALID_REF; + } + + if (separatorIndex > PREFIX_MAX_LENGTH) { + return "Prefix with illegal length, must not have more than " + PREFIX_MAX_LENGTH + " characters"; + } + + for (int i = 0; i < separatorIndex; i++) { + char c = input.charAt(i); + if (!(c >= 'a' && c <= 'z')) { + return "Illegal character in prefix, must be one of [a-z]"; + } + } + + return VALID_REF; + } + + public static UUID getUuidV7() { + return generator.generate(); + } +} diff --git a/src/test/java/de/fxlae/typeid/SpecTest.java b/lib/shared/src/test/java/de/fxlae/typeid/AbstractSpecTest.java similarity index 69% rename from src/test/java/de/fxlae/typeid/SpecTest.java rename to lib/shared/src/test/java/de/fxlae/typeid/AbstractSpecTest.java index 0bdb578..1fb5de6 100644 --- a/src/test/java/de/fxlae/typeid/SpecTest.java +++ b/lib/shared/src/test/java/de/fxlae/typeid/AbstractSpecTest.java @@ -5,12 +5,12 @@ import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.io.File; import java.io.IOException; import java.util.List; import java.util.UUID; @@ -24,41 +24,56 @@ * especially against valid.yml * and * invalid.yml. - * */ -class SpecTest { +abstract class AbstractSpecTest { + + TypeIdStaticContext staticFacade; + + private static Stream provideSpecValid() throws IOException { + return loadSpec("/spec/valid.yml", SpecValid.class) + .stream() + .map(s -> Arguments.of(s.name, s.typeid, s.prefix, UUID.fromString(s.uuid))); + } + + private static Stream provideSpecInvalid() throws IOException { + return loadSpec("/spec/invalid.yml", SpecInvalid.class) + .stream() + .map(s -> Arguments.of(s.name, s.typeid, s.description)); + } + + static List loadSpec(String path, Class clazz) throws IOException { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + CollectionType javaType = mapper.getTypeFactory() + .constructCollectionType(List.class, clazz); + return mapper.readValue(AbstractSpecTest.class.getResourceAsStream(path), javaType); + } + + @BeforeEach + void setupFacade() { + this.staticFacade = createStaticFacade(); + } + + abstract TypeIdStaticContext createStaticFacade(); @ParameterizedTest @MethodSource("provideSpecValid") void testEncodeAgainstSpecValid(String name, String typeIdAsString, String prefix, UUID uuid) { - TypeId typeId = TypeId.of(prefix, uuid); + TypeIdInstance typeId = staticFacade.of(prefix, uuid); assertEquals(typeIdAsString, typeId.toString()); } @ParameterizedTest @MethodSource("provideSpecValid") void testDecodeAgainstSpecValid(String name, String typeIdAsString, String prefix, UUID uuid) { - TypeId typeId = TypeId.parse(typeIdAsString); - assertEquals(prefix, typeId.getPrefix()); - assertEquals(uuid, typeId.getUuid()); + TypeIdInstance typeId = staticFacade.parse(typeIdAsString); + assertEquals(prefix, typeId.prefix()); + assertEquals(uuid, typeId.uuid()); } @ParameterizedTest @MethodSource("provideSpecInvalid") void testDecodeAgainstSpecInvalid(String name, String typeIdAsString, String description) { - Assertions.assertThrows(IllegalArgumentException.class, () -> TypeId.parse(typeIdAsString)); - } - - private static Stream provideSpecValid() throws IOException { - return loadSpec(new File("src/test/resources/spec/valid.yml"), SpecValid.class) - .stream() - .map(s -> Arguments.of(s.name, s.typeid, s.prefix, UUID.fromString(s.uuid))); - } - - private static Stream provideSpecInvalid() throws IOException { - return loadSpec(new File("src/test/resources/spec/invalid.yml"), SpecInvalid.class) - .stream() - .map(s -> Arguments.of(s.name, s.typeid, s.description)); + Assertions.assertThrows(IllegalArgumentException.class, () -> staticFacade.parse(typeIdAsString), description); } /** @@ -69,10 +84,10 @@ private static Stream provideSpecInvalid() throws IOException { void testRandomIds() { for (int i = 0; i < 2000; i++) { UUID uuid = UUID.randomUUID(); - TypeId typeId1 = TypeId.of("test", uuid); - TypeId typeId2 = TypeId.parse(typeId1.toString()); - assertEquals("test", typeId2.getPrefix()); - assertEquals(uuid, typeId2.getUuid()); + TypeIdInstance typeId1 = staticFacade.of("test", uuid); + TypeIdInstance typeId2 = staticFacade.parse(typeId1.toString()); + assertEquals("test", typeId2.prefix()); + assertEquals(uuid, typeId2.uuid()); } } @@ -90,11 +105,4 @@ static class SpecInvalid { String typeid; String description; } - - static List loadSpec(File file, Class clazz) throws IOException { - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - CollectionType javaType = mapper.getTypeFactory() - .constructCollectionType(List.class, clazz); - return mapper.readValue(file, javaType); - } } \ No newline at end of file diff --git a/lib/shared/src/test/java/de/fxlae/typeid/AbstractTypeIdTest.java b/lib/shared/src/test/java/de/fxlae/typeid/AbstractTypeIdTest.java new file mode 100644 index 0000000..8be5153 --- /dev/null +++ b/lib/shared/src/test/java/de/fxlae/typeid/AbstractTypeIdTest.java @@ -0,0 +1,257 @@ +package de.fxlae.typeid; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Note: the actual encoding and decoding of TypeIDs is mainly tested by {@link AbstractSpecTest}. + *

This class is indented to test auxiliary methods, e.g. regarding the construction of TypeId instances. + */ +public abstract class AbstractTypeIdTest { + + static final UUID SOME_UUID = UUID.fromString("01890a5d-ac96-774b-bcce-b302099a8057"); + static final String SOME_PREFIX = "theprefix"; + static final String SOME_SUFFIX = "01h455vb4pex5vsknk084sn02q"; + static final String SOME_TYPE_ID = SOME_PREFIX + "_" + SOME_SUFFIX; + + TypeIdStaticContext staticFacade; + + @BeforeEach + void setupFacade() { + this.staticFacade = createStaticFacade(); + } + + abstract TypeIdStaticContext createStaticFacade(); + + @Test + void generateShouldReturnTypeIdForUuidV7() { + TypeIdInstance typeId = staticFacade.generate(); + assertNotNull(typeId); + assertAll( + () -> assertEquals("", typeId.prefix()), + () -> assertEquals('7', typeId.uuid().toString().charAt(14))); + } + + @Test + void generateWithPrefixShouldReturnTypeIdForUuidV7() { + TypeIdInstance typeId = staticFacade.generate(SOME_PREFIX); + assertNotNull(typeId); + assertAll( + () -> assertEquals(SOME_PREFIX, typeId.prefix()), + () -> assertEquals('7', typeId.uuid().toString().charAt(14))); + } + + @Test + void generateWithInvalidPrefixShouldFail() { + assertAll( + () -> assertThrows( + IllegalArgumentException.class, + () -> staticFacade.generate("i think this prefix is not allowed"))); + } + + @Test + void generateWithNullPrefixShouldFail() { + assertAll( + () -> assertThrows( + NullPointerException.class, + () -> staticFacade.generate(null))); + } + + @Test + void ofWithUuidShouldReturnTypeId() { + TypeIdInstance typeId = staticFacade.of(SOME_UUID); + assertNotNull(typeId); + assertAll( + () -> assertEquals("", typeId.prefix()), + () -> assertEquals(SOME_UUID, typeId.uuid())); + } + + @Test + void ofWithNullUuidShouldFail() { + assertThrows( + NullPointerException.class, + () -> staticFacade.of(null)); + } + + @Test + void ofWithPrefixAndUuidShouldReturnTypeId() { + TypeIdInstance typeId = staticFacade.of(SOME_PREFIX, SOME_UUID); + assertNotNull(typeId); + assertAll( + () -> assertEquals(SOME_PREFIX, typeId.prefix()), + () -> assertEquals(SOME_UUID, typeId.uuid())); + } + + @Test + void ofWithInvalidPrefixOrInvalidUuidShouldFail() { + assertAll( + () -> assertThrows( + IllegalArgumentException.class, + () -> staticFacade.of("i think this prefix is not allowed", SOME_UUID)) + ); + } + + @Test + void ofWithNullPrefixOrNullUuidShouldFail() { + assertAll( + () -> assertThrows( + NullPointerException.class, + () -> staticFacade.of(null, SOME_UUID)), + () -> assertThrows( + NullPointerException.class, + () -> staticFacade.of(SOME_PREFIX, null)) + ); + } + + @Test + void parseWithoutPrefixWithSuffixShouldReturnTypeId() { + TypeIdInstance typeId = staticFacade.parse(SOME_SUFFIX); + assertNotNull(typeId); + assertAll( + () -> assertEquals("", typeId.prefix()), + () -> assertEquals(SOME_UUID, typeId.uuid())); + } + + @Test + void parseWithPrefixWithSuffixShouldReturnTypeId() { + TypeIdInstance typeId = staticFacade.parse(SOME_PREFIX + "_" + SOME_SUFFIX); + assertNotNull(typeId); + assertAll( + () -> assertEquals(SOME_PREFIX, typeId.prefix()), + () -> assertEquals(SOME_UUID, typeId.uuid())); + } + + @ParameterizedTest + @ValueSource(strings = { + "01h455vb4pex5vsknk084sn02q", // suffix only + "abcdefghijklmnopqrstuvw_01h455vb4pex5vsknk084sn02q", // prefix with all allowed chars + "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss_01h455vb4pex5vsknk084sn02q" // prefix with 63 chars + }) + void parseWithValidInputsShouldReturnTypeId(String input) { + TypeIdInstance typeId = staticFacade.parse(input); + assertNotNull(typeId); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "_", + "someprefix_", // no suffix at all + "_01h455vb4pex5vsknk084sn02q", // suffix only, but with preceding underscore + "sömeprefix_01h455vb4pex5vsknk084sn02q", // prefix with 'ö' + "someprefix_01h455öb4pex5vsknk084sn02q", // suffix with 'ö' + "sOmeprefix_01h455vb4pex5vsknk084sn02q", // prefix with 'O' + "someprefix_01h455Vb4pex5vsknk084sn02q", // suffix with 'V' + "someprefix_01h455lb4pex5vsknk084sn02q", // suffix with 'l' + "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss_01h455vb4pex5vsknk084sn02q", // prefix with 64 chars + "someprefix_01h455vb4pex5vsknk084sn02", // suffix with 25 chars + "someprefix_01h455vb4pex5vsknk084sn02q2", // suffix with 27 chars + "someprefix_81h455vb4pex5vsknk084sn02q" // leftmost suffix char is != 0-7 + }) + void parseWithInvalidInputShouldFail(String input) { + assertThrows( + IllegalArgumentException.class, + () -> staticFacade.parse(input)); + } + + @Test + void parseWithNullInputShouldFail() { + assertThrows( + NullPointerException.class, + () -> staticFacade.parse(null)); + } + + @Test + void parseWithHandlersShouldReturnTypeIdOnSuccess() { + + String result = staticFacade.parse(SOME_TYPE_ID, + TypeIdInstance::toString, + error -> error); + + assertNotNull(result); + assertEquals(SOME_TYPE_ID, result); + } + + @Test + void parseWithHandlersShouldReturnMessageOnFailure() { + + String result = staticFacade.parse("?_" + SOME_SUFFIX, + TypeIdInstance::toString, + error -> error); + + assertNotNull(result); + assertEquals("Illegal character in prefix, must be one of [a-z]", result); + } + + @Test + void parseToOptionalShouldReturnNonEmptyOptionalOnParseSuccess() { + Optional result = staticFacade.parseToOptional(SOME_TYPE_ID); + assertThat(result).isNotEmpty(); + assertThat(result.get().prefix()).isEqualTo(SOME_PREFIX); + assertThat(result.get().uuid()).isEqualTo(SOME_UUID); + } + + @Test + void parseToOptionalShouldReturnEmptyOptionalOnParseFailure() { + Optional result = staticFacade.parseToOptional("some invalid typeid"); + assertThat(result).isEmpty(); + } + + @Test + void toStringShouldReturnTypeIdAsString() { + TypeIdInstance typeId = staticFacade.of(SOME_PREFIX, SOME_UUID); + assertNotNull(typeId); + assertEquals(SOME_PREFIX + "_" + SOME_SUFFIX, typeId.toString()); + } + + @Test + void toStringWithoutPrefixShouldReturnTypeIdAsStringWithoutUnderscore() { + TypeIdInstance typeId = staticFacade.of(SOME_UUID); + assertNotNull(typeId); + assertEquals(SOME_SUFFIX, typeId.toString()); + } + + @Test + void equalsShouldSucceedForEqualTypeIds() { + + TypeIdInstance typeIdA = staticFacade.of(SOME_UUID); + TypeIdInstance typeIdB = staticFacade.of(SOME_UUID); + + // reflexivity + assertEquals(typeIdA.getWrapped(), typeIdA.getWrapped()); + + // symmetry + assertEquals(typeIdA.getWrapped(), typeIdB.getWrapped()); + assertEquals(typeIdB.getWrapped(), typeIdA.getWrapped()); + } + + @Test + void equalsShouldFailForAnythingElse() { + + TypeIdInstance typeIdA = staticFacade.of(SOME_UUID); + TypeIdInstance typeIdB = staticFacade.of("different", SOME_UUID); + TypeIdInstance typeIdC = staticFacade.of(UUID.fromString("00000000-0000-0000-0000-000000000000")); + Object otherType = new Object(); + + assertNotEquals(typeIdA.getWrapped(), typeIdB.getWrapped()); + assertNotEquals(typeIdA.getWrapped(), typeIdC.getWrapped()); + assertNotEquals(null, typeIdA.getWrapped()); + assertNotEquals(typeIdA.getWrapped(), otherType); + } + + @Test + void hashCodeShouldBeTheSameForSameTypeIds() { + TypeIdInstance typeIdA = staticFacade.of(SOME_UUID); + TypeIdInstance typeIdB = staticFacade.of(SOME_UUID); + assertEquals(typeIdA.getWrapped().hashCode(), typeIdB.getWrapped().hashCode()); + } + +} \ No newline at end of file diff --git a/lib/shared/src/test/java/de/fxlae/typeid/TypeIdInstance.java b/lib/shared/src/test/java/de/fxlae/typeid/TypeIdInstance.java new file mode 100644 index 0000000..f0f2de2 --- /dev/null +++ b/lib/shared/src/test/java/de/fxlae/typeid/TypeIdInstance.java @@ -0,0 +1,12 @@ +package de.fxlae.typeid; + +import java.util.UUID; + +/** + * A test facade for testing both Java 8 and Java 17 variants with a common test set + */ +public interface TypeIdInstance { + String prefix(); + UUID uuid(); + Object getWrapped(); +} diff --git a/lib/shared/src/test/java/de/fxlae/typeid/TypeIdStaticContext.java b/lib/shared/src/test/java/de/fxlae/typeid/TypeIdStaticContext.java new file mode 100644 index 0000000..46505fa --- /dev/null +++ b/lib/shared/src/test/java/de/fxlae/typeid/TypeIdStaticContext.java @@ -0,0 +1,21 @@ +package de.fxlae.typeid; + +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; + +/** + * A test facade for testing both Java 8 and Java 17 variants with a common test set + */ +public interface TypeIdStaticContext { + TypeIdInstance generate(String prefix); + TypeIdInstance generate(); + TypeIdInstance of(UUID uuid); + TypeIdInstance of(String prefix, UUID uuid); + TypeIdInstance parse(final String text); + Optional parseToOptional(final String text); + O parse( + final String text, + Function okHandler, + Function errorHandler); +} diff --git a/src/test/resources/spec/invalid.yml b/lib/shared/src/test/resources/spec/invalid.yml similarity index 91% rename from src/test/resources/spec/invalid.yml rename to lib/shared/src/test/resources/spec/invalid.yml index c1470a2..3f288e7 100644 --- a/src/test/resources/spec/invalid.yml +++ b/lib/shared/src/test/resources/spec/invalid.yml @@ -4,7 +4,7 @@ # Each example contains an invalid TypeID string. Implementations are expected # to throw an error when attempting to parse/validate these strings. # -# Last updated: 2023-06-29 +# Last updated: 2023-07-05 - name: prefix-uppercase typeid: "PREFIX_00000000000000000000000000" @@ -31,7 +31,7 @@ description: "The prefix can't have any spaces" - name: prefix-64-chars - # 123456789 123456789 123456789 123456789 123456789 123456789 1234 + # 123456789 123456789 123456789 123456789 123456789 123456789 1234 typeid: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000" description: "The prefix can't be 64 characters, it needs to be 63 characters or less" @@ -81,3 +81,8 @@ # This example would be valid if we were using the crockford hyphenation rules typeid: "prefix_123456789-0123456789-0123456" description: "The suffix can't ignore hyphens as in the crockford encoding" + +- name: suffix-overflow + # This is the first suffix that overflows into 129 bits + typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz" + description: "The suffix should encode at most 128-bits" diff --git a/src/test/resources/spec/valid.yml b/lib/shared/src/test/resources/spec/valid.yml similarity index 90% rename from src/test/resources/spec/valid.yml rename to lib/shared/src/test/resources/spec/valid.yml index 964c96f..8f63250 100644 --- a/src/test/resources/spec/valid.yml +++ b/lib/shared/src/test/resources/spec/valid.yml @@ -17,13 +17,13 @@ # decoding and re-encoding the id, the result is the same as the original. # # In other words, the following property should always hold: -# random_typeid: encode(decode(random_typeid)) +# random_typeid == encode(decode(random_typeid)) # # Finally, while implementations should be able to decode the values below, # note that not all of them are UUIDv7s. When *generating* new random typeids, # implementations should always use UUIDv7s. # -# Last updated: 2023-06-29 +# Last updated: 2023-07-05 - name: nil typeid: "00000000000000000000000000" @@ -50,6 +50,11 @@ prefix: "" uuid: "00000000-0000-0000-0000-000000000020" +- name: max-valid + typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz" + prefix: "" + uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff" + - name: valid-alphabet typeid: "prefix_0123456789abcdefghjkmnpqrs" prefix: "prefix" diff --git a/settings.gradle.kts b/settings.gradle.kts index e6a9fc7..d4fa996 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,6 @@ -rootProject.name = "typeid-java" +pluginManagement { + includeBuild("build-conventions") +} +rootProject.name = "typeid-java" +include("lib:shared", "lib:java8", "lib:java17") diff --git a/src/main/java/de/fxlae/typeid/TypeId.java b/src/main/java/de/fxlae/typeid/TypeId.java deleted file mode 100644 index 6a23503..0000000 --- a/src/main/java/de/fxlae/typeid/TypeId.java +++ /dev/null @@ -1,268 +0,0 @@ -package de.fxlae.typeid; - -import java.util.Objects; -import java.util.UUID; - -/** - * An implementation of TypeID for Java. - */ -public final class TypeId { - - private static final char SEPARATOR = '_'; - private static final String PREFIX_ALPHABET = "abcdefghijklmnopqrstuvwxyz"; - private static final int PREFIX_MAX_LENGTH = 63; - private static final String SUFFIX_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz"; - - // inspired by base32.go from the official go implementation - // https://github.com/jetpack-io/typeid-go/blob/main/base32/base32.go - // lookup: [ascii pos] -> binary representation of block - // FF means invalid character that is not part of the suffix alphabet - private static final long[] SUFFIX_REVERSE_LOOKUP = new long[]{ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, - 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C, - 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14, - 0x15, 0xFF, 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C, - 0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF - }; - - private static final UuidProvider uuidProvider = UuidProvider.getDefault(); - - private final UUID uuid; - private final String prefix; - - private TypeId(String prefix, UUID uuid) { - this.prefix = prefix; - this.uuid = uuid; - } - - /** - * Returns the prefix of this {@link TypeId}. - * - * @return the prefix. - */ - public String getPrefix() { - return prefix; - } - - /** - * Returns the underlying {@link UUID} of this {@link TypeId}. - * - * @return the {@link UUID}. - */ - public UUID getUuid() { - return uuid; - } - - /** - * Creates a new prefixed {@link TypeId} based on UUIDv7. - * - * @param prefix the prefix to use. - * @return the new {@link TypeId}. - */ - public static TypeId generate(String prefix) throws IllegalArgumentException { - return of(prefix, uuidProvider.getUuidV7()); - } - - /** - * Creates a new {@link TypeId} without prefix, based on UUIDv7. - *

Note: "no prefix" means empty string, not null. - * - * @return the new {@link TypeId}. - */ - public static TypeId generate() { - return of("", uuidProvider.getUuidV7()); - } - - /** - * Creates a new {@link TypeId} without prefix, based on the given {@link UUID}. - *

The {@link UUID} can be of any version. - *

Note: "no prefix" means empty string, not null. - * - * @param uuid the {@link UUID} to use. - * @return the new {@link TypeId}. - */ - public static TypeId of(UUID uuid) throws IllegalArgumentException { - return of("", uuid); - } - - /** - * Creates a new {@link TypeId}, based on the given prefix and {@link UUID}. - * - * @param prefix the prefix to use. - * @param uuid the {@link UUID} to use. - * @return the new {@link TypeId}. - */ - public static TypeId of(String prefix, UUID uuid) throws IllegalArgumentException { - - if (uuid == null) { - throw new IllegalArgumentException("Provided UUID must not be null"); - } - - if (prefix == null) { - throw new IllegalArgumentException("Provided prefix must not be null"); - } - - if (prefix.length() > PREFIX_MAX_LENGTH) { - throw new IllegalArgumentException("Provided prefix must not be larger than " + PREFIX_MAX_LENGTH + " characters"); - } - - for (int i = 0; i < prefix.length(); i++) { - char c = prefix.charAt(i); - if (PREFIX_ALPHABET.indexOf(c) == -1) { - throw new IllegalArgumentException("Illegal prefix character: '" + c + "'"); - } - } - - return new TypeId(prefix, uuid); - } - - /** - * Parses the textual representation of a TypeID and returns a {@link TypeId} instance. - * - * @param text the textual representation. - * @return the new {@link TypeId}. - */ - public static TypeId parse(final String text) throws IllegalArgumentException { - - if (text == null) { - throw new IllegalArgumentException("Provided TypeId must not be null"); - } - - final int separatorIndex = text.lastIndexOf(SEPARATOR); - - if (separatorIndex == 0) { - throw new IllegalArgumentException("Provided TypeId must not start with '_'"); - } - - if (separatorIndex == text.length() - 1) { - throw new IllegalArgumentException("Provided TypeId must not end with '_'"); - } - - final String prefixSegment; - final String suffixSegment; - - if (separatorIndex != -1) { - prefixSegment = text.substring(0, separatorIndex); - suffixSegment = text.substring(separatorIndex + 1); - } else { - prefixSegment = ""; - suffixSegment = text; - } - - return of(prefixSegment, base32Decode(suffixSegment)); - } - - private static String base32Encode(UUID uuid) { - - final long msb = uuid.getMostSignificantBits(); - final long lsb = uuid.getLeastSignificantBits(); - StringBuilder stringBuilder = new StringBuilder(); - - // encode the MSBs except the last bit, as the block it belongs to overlaps with the LSBs - for (int i = 0; i < 13; i++) { - long block = (msb >>> (61 - 5 * i)) & 0x1F; - stringBuilder.append(SUFFIX_ALPHABET.charAt((int) block)); - } - - // encode the overlap between MSBs (1 bit) and LSBs (4 bits) - long overlap = ((msb & 0x1) << 4) | (lsb >>> 60); - stringBuilder.append(SUFFIX_ALPHABET.charAt((int) overlap)); - - // encode the rest of LSBs - for (int i = 1; i < 13; i++) { - long block = (lsb >>> (60 - 5 * i)) & 0x1F; - stringBuilder.append(SUFFIX_ALPHABET.charAt((int) block)); - } - - return stringBuilder.toString(); - } - - private static UUID base32Decode(final String suffix) { - - if (suffix.length() != 26) { - throw new IllegalArgumentException("Suffix with invalid length (expected: 26, actual: '" + suffix.length() + "')"); - } - - for (int i = 0; i < suffix.length(); i++) { - if (SUFFIX_REVERSE_LOOKUP[suffix.charAt(i)] == 0xFF) { - throw new IllegalArgumentException("Illegal base32 character: '" + suffix.charAt(i) + "'"); - } - } - - // the leftmost block of 5 bits has to start with 0b00 - if (((SUFFIX_REVERSE_LOOKUP[suffix.charAt(0)] >>> 3) & 0x3) > 0) { - throw new IllegalArgumentException("Illegal leftmost character: " + suffix.charAt(0)); - } - - long lsb = 0; - long msb = 0; - - // decode characters [25] to [14] into the LSBs - for (int i = 0; i < 12; i++) { - char c = suffix.charAt(25 - i); - lsb |= (SUFFIX_REVERSE_LOOKUP[c]) << 5 * i; - } - - // decode the overlap between LSBs and MSBs (character [13]) - long bitsAtOverlap = SUFFIX_REVERSE_LOOKUP[suffix.charAt(13)]; - lsb |= (bitsAtOverlap & 0xF) << 60; - msb |= (bitsAtOverlap & 0x10) >>> 4; - - // decode characters [12] to [0] into the MSBs - for (int i = 0; i < 13; i++) { - char c = suffix.charAt(12 - i); - msb |= (SUFFIX_REVERSE_LOOKUP[c]) << 5 * i + 1; - } - - return new UUID(msb, lsb); - } - - /** - * Returns the textual representation of this {@link TypeId}. - * - * @return the textual representation. - */ - @Override - public String toString() { - String suffix = base32Encode(uuid); - if (!prefix.isEmpty()) { - return prefix + "_" + suffix; - } else { - return suffix; - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TypeId typeId = (TypeId) o; - return Objects.equals(uuid, typeId.uuid) && Objects.equals(prefix, typeId.prefix); - } - - @Override - public int hashCode() { - return Objects.hash(uuid, prefix); - } -} diff --git a/src/main/java/de/fxlae/typeid/UuidProvider.java b/src/main/java/de/fxlae/typeid/UuidProvider.java deleted file mode 100644 index 532ecbd..0000000 --- a/src/main/java/de/fxlae/typeid/UuidProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.fxlae.typeid; - -import com.fasterxml.uuid.Generators; -import com.fasterxml.uuid.impl.TimeBasedEpochGenerator; - -import java.util.UUID; - -/** - * Encapsulates the UUIDv7 generator implementation. - */ -interface UuidProvider { - - UUID getUuidV7(); - - static UuidProvider getDefault() { - return new UuidProvider() { - private final TimeBasedEpochGenerator generator = Generators.timeBasedEpochGenerator(); - - @Override - public UUID getUuidV7() { - return generator.generate(); - } - }; - } - -} diff --git a/src/test/java/de/fxlae/typeid/TypeIdTest.java b/src/test/java/de/fxlae/typeid/TypeIdTest.java deleted file mode 100644 index 897f23c..0000000 --- a/src/test/java/de/fxlae/typeid/TypeIdTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package de.fxlae.typeid; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Note: the actual encoding and decoding of TypeIDs is mainly tested by {@link SpecTest}. - *

This class is indented to test auxiliary methods, e.g. regarding the construction of {@link TypeId} instances. - */ -class TypeIdTest { - - static final UUID SOME_UUID = UUID.fromString("01890a5d-ac96-774b-bcce-b302099a8057"); - static final String SOME_PREFIX = "theprefix"; - static final String SOME_SUFFIX = "01h455vb4pex5vsknk084sn02q"; - - @Test - void generateShouldReturnTypeIdForUuidV7() { - TypeId typeId = TypeId.generate(); - assertNotNull(typeId); - assertAll( - () -> assertEquals("", typeId.getPrefix()), - () -> assertEquals('7', typeId.getUuid().toString().charAt(14))); - } - - @Test - void generateWithPrefixShouldReturnTypeIdForUuidV7() { - TypeId typeId = TypeId.generate(SOME_PREFIX); - assertNotNull(typeId); - assertAll( - () -> assertEquals(SOME_PREFIX, typeId.getPrefix()), - () -> assertEquals('7', typeId.getUuid().toString().charAt(14))); - } - - @Test - void generateWithInvalidPrefixShouldFail() { - assertAll( - () -> assertThrows( - IllegalArgumentException.class, - () -> TypeId.generate("i think this prefix is not allowed")), - () -> assertThrows( - IllegalArgumentException.class, - () -> TypeId.generate(null))); - } - - @Test - void ofWithUuidShouldReturnTypeId() { - TypeId typeId = TypeId.of(SOME_UUID); - assertNotNull(typeId); - assertAll( - () -> assertEquals("", typeId.getPrefix()), - () -> assertEquals(SOME_UUID, typeId.getUuid())); - } - - @Test - void ofWithInvalidUuidShouldFail() { - assertThrows( - IllegalArgumentException.class, - () -> TypeId.of(null)); - } - - @Test - void ofWithPrefixAndUuidShouldReturnTypeId() { - TypeId typeId = TypeId.of(SOME_PREFIX, SOME_UUID); - assertNotNull(typeId); - assertAll( - () -> assertEquals(SOME_PREFIX, typeId.getPrefix()), - () -> assertEquals(SOME_UUID, typeId.getUuid())); - } - - @Test - void ofWithInvalidPrefixOrInvalidUuidShouldFail() { - assertAll( - () -> assertThrows( - IllegalArgumentException.class, - () -> TypeId.of("i think this prefix is not allowed", SOME_UUID)), - () -> assertThrows( - IllegalArgumentException.class, - () -> TypeId.of(null, SOME_UUID)), - () -> assertThrows( - IllegalArgumentException.class, - () -> TypeId.of(SOME_PREFIX, null)) - ); - } - - @Test - void parseWithoutPrefixWithSuffixShouldReturnTypeId() { - TypeId typeId = TypeId.parse(SOME_SUFFIX); - assertNotNull(typeId); - assertAll( - () -> assertEquals("", typeId.getPrefix()), - () -> assertEquals(SOME_UUID, typeId.getUuid())); - } - - @Test - void parseWithPrefixWithSuffixShouldReturnTypeId() { - TypeId typeId = TypeId.parse(SOME_PREFIX + "_" + SOME_SUFFIX); - assertNotNull(typeId); - assertAll( - () -> assertEquals(SOME_PREFIX, typeId.getPrefix()), - () -> assertEquals(SOME_UUID, typeId.getUuid())); - } - - @ParameterizedTest - @ValueSource(strings = { - "01h455vb4pex5vsknk084sn02q", // suffix only - "abcdefghijklmnopqrstuvw_01h455vb4pex5vsknk084sn02q", // prefix with all allowed chars - "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss_01h455vb4pex5vsknk084sn02q" // prefix with 63 chars - }) - void parseWithValidInputsShouldReturnTypeId(String input) { - TypeId typeId = TypeId.parse(input); - assertNotNull(typeId); - } - - @ParameterizedTest - @ValueSource(strings = { - "", - "_", - "someprefix_", // no suffix at all - "_01h455vb4pex5vsknk084sn02q", // suffix only, but with preceding underscore - "sömeprefix_01h455vb4pex5vsknk084sn02q", // prefix with 'ö' - "someprefix_01h455öb4pex5vsknk084sn02q", // suffix with 'ö' - "sOmeprefix_01h455vb4pex5vsknk084sn02q", // prefix with 'O' - "someprefix_01h455Vb4pex5vsknk084sn02q", // suffix with 'V' - "someprefix_01h455lb4pex5vsknk084sn02q", // suffix with 'l' - "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss_01h455vb4pex5vsknk084sn02q", // prefix with 64 chars - "someprefix_01h455vb4pex5vsknk084sn02", // suffix with 25 chars - "someprefix_01h455vb4pex5vsknk084sn02q2" // suffix with 27 chars - }) - void parseForInvalidInputShouldFail(String input) { - assertThrows( - IllegalArgumentException.class, - () -> TypeId.parse(input)); - } - - @Test - void toStringShouldReturnTypeIdAsString() { - TypeId typeId = TypeId.of(SOME_PREFIX, SOME_UUID); - assertNotNull(typeId); - assertEquals(SOME_PREFIX + "_" + SOME_SUFFIX, typeId.toString()); - } - - @Test - void toStringWithoutPrefixShouldReturnTypeIdAsStringWithoutUnderscore() { - TypeId typeId = TypeId.of(SOME_UUID); - assertNotNull(typeId); - assertEquals(SOME_SUFFIX, typeId.toString()); - } - -} \ No newline at end of file