Skip to content

Commit

Permalink
release 0.3.0 (#4)
Browse files Browse the repository at this point in the history
update library to typeid-spec 0.3.0 ("allow '_' in type prefix")
remove artifact for Java 8 (typeid-java-jdk8)
  • Loading branch information
fxlae authored Apr 14, 2024
1 parent a0edd2c commit 791ad7b
Show file tree
Hide file tree
Showing 33 changed files with 659 additions and 1,063 deletions.
10 changes: 4 additions & 6 deletions .github/workflows/build-on-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ jobs:
build-gradle-project:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/gradle-build-action@v2.5.1
with:
gradle-version: wrapper
arguments: build
- uses: gradle/actions/setup-gradle@v3
- run: ./gradlew build
11 changes: 5 additions & 6 deletions .github/workflows/publish-on-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/gradle-build-action@v2.5.1
with:
gradle-version: wrapper
arguments: build publishAllPublicationsToOSSRHRepository
- uses: gradle/actions/setup-gradle@v3
- run: ./gradlew build
- run: ./gradlew publishAllPublicationsToOSSRHRepository
env:
ORG_GRADLE_PROJECT_OSSRHUsername: ${{ secrets.OSSRH_USERNAME }}
ORG_GRADLE_PROJECT_OSSRHPassword: ${{ secrets.OSSRH_TOKEN }}
Expand Down
128 changes: 62 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,27 @@ Read more about TypeIDs in their [spec](https://github.com/jetpack-io/typeid).

## Installation

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:
Starting with version `0.3.0`, `typeid-java` requires at least Java 17.

- `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
<details>
<summary>(Details on Java 8+ support)</summary>
Up to version 0.2.0, a separate artifact called `typeid-java-jdk8` was published, supporting Java versions 8 and higher, and covering all relevant use cases, albeit with less syntactic sugar. If you are running Java 8 through 16, you can still use `typeid-java-jdk8:0.2.0`, which is still available and remains fully functional. However, it will no longer receive updates and is limited to the TypeId spec version 0.2.0.
</details>

To install via Maven:

```xml
<dependency>
<groupId>de.fxlae</groupId>
<artifactId>typeid-java</artifactId> <!-- or 'typeid-java-jdk8' -->
<version>0.2.0</version>
<artifactId>typeid-java</artifactId>
<version>0.3.0</version>
</dependency>
```

For installation via Gradle:

```kotlin
implementation("de.fxlae:typeid-java:0.2.0") // or ...typeid-java-jdk8:0.2.0
implementation("de.fxlae:typeid-java:0.3.0")
```

## Usage
Expand All @@ -38,6 +40,9 @@ implementation("de.fxlae:typeid-java:0.2.0") // or ...typeid-java-jdk8:0.2.0

### Generating new TypeIDs


#### generate

To generate a new `TypeId`, based on UUIDv7 as per specification:

```java
Expand All @@ -47,44 +52,39 @@ 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:
#### of

To construct (or reconstruct) a `TypeId` from existing arguments:

```java
var typeId = TypeId.of("user", UUID.randomUUID()); // a TypeId based on UUIDv4
var typeId = TypeId.of("user", someUuid);
```
As a side effect, `of` can also be used as an "extension point" to plug-in custom UUID generators.
### Parsing TypeID strings

For parsing, the library supports both an imperative programming model and a more functional style.

#### parse
The most straightforward way to parse the textual representation of a TypeID:

```java
var typeId = TypeId.parse("user_01h455vb4pex5vsknk084sn02q");
```

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):
Invalid inputs will result in an `IllegalArgumentException`, with a message explaining the cause of the parsing failure.

#### parseToOptional

It's also possible to obtain an `Optional<TypeId>` in cases where the concrete error message is not relevant.

```java
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<TypeId, T>, called on success
message -> { // (2) Function<String, T>, called on failure
log.warn("Parsing failed: {}", message);
return Optional.empty();
});
var maybeTypeId = TypeId.parseToOptional("user_01h455vb4pex5vsknk084sn02q");
```
**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<T>`, or in this particular context, `Validated<TypeId>`
#### parseToValidated

`Validated<TypeId>` can be of subtype:
- `Valid<TypeId>`: encapsulates a successfully parsed `TypeId`
- or otherwise `Invalid<TypeId>`: contains an error message
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 with high error rates, as no stacktrace is involved):

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");
Expand All @@ -99,14 +99,27 @@ if(validated.isValid) {
```
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:
`Validated` and its implementations `Valid` and `Invalid` form a sealed type hierarchy. This feature becomes especially useful in more recent Java versions, beginning with Java 21, which facilitates Record Patterns (destructuring) and Pattern Matching for switch (yes, `TypeId` is a `record`):

```java
// this compiles and runs from Java 21 onwards

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, var ignored)) -> "Not a user. Prefix is " + prefix;
case Invalid(var message) -> "Parsing failed :( ... " + message;
};
```
Note the absent (and superfluous) default case. Exhaustiveness is checked during compilation!

Another safe alternative for working with `Validated<TypeId>` 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");
var mappedToPrefix = TypeId.parseToValidated("dog_01h455vb4pex5vsknk084sn02q")
.map(TypeId::prefix) // Validated<TypeId> -> Validated<String>
.filter("Not a cat! :(", prefix -> !"cat".equals(prefix)); // the predicate fails

Expand All @@ -115,62 +128,46 @@ mappedToPrefix.ifValid(prefix -> log.info(prefix)) // called on success, so not
mappedToPrefix.ifInvalid(message -> log.warn(message)) // logs "Not a cat! :("
```

`Validated<T>` and its implementations `Valid<T>` and `Invalid<T>` 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>
<summary>Details</summary>

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):
Here's how more type-safe variants could look like, which I have implemented experimentally (**currently not included in the artifact**):

```java
TypeId<User> typeId = TypeId<>.generate(USER);
TypeId<User> anotherTypeId = TypeId<>.parse(USER, "user_01h455vb4pex5vsknk084sn02q");
TypeId<User> typeId = TypeId.generate(USER);
TypeId<User> 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:
The downside to this approach is that each possible prefix has to be defined manually as its own type that contains the prefix' string representation, e.g.:

```java
static final User USER = new User();
record User() implements TypedPrefix {
final class 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.
</details>

## A word on UUIDv7
<details>
<summary>Details</summary>

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.
static final User USER = new User();
```

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.
Another solution is to validate the names of the prefix types at compile time. This solution is somewhat more complex as it requires an annotation processor.

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).
```java
@TypeId(name = "UserId", prefix = "user")
class MyApp {}

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`.
UserId userId = UserId.generate();
UserId anotherUserId = UserId.parse("user_01h455vb4pex5vsknk084sn02q");
```

</details>
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.
</details>

## Building From Source & Benchmarks
<details>
Expand All @@ -187,12 +184,11 @@ There is a small [JMH](https://github.com/openjdk/jmh) microbenchmark included:
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).
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 Server VM, 2021 AMD mid-range notebook CPU).

| method | op/s |
|----------------------------------|----------------------:|
| `TypeId.generate` + `toString` | 9.1M |
| `TypeId.parse` | 9.8M |
| method | op/s |
|--------------------------------|------:|
| `TypeId.generate` + `toString` | 10.2M |
| `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`.
</details>
4 changes: 0 additions & 4 deletions build-conventions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,3 @@ repositories {
gradlePluginPortal()
mavenCentral()
}

dependencies {
implementation("com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ plugins {
`java-library`
`maven-publish`
signing
id("com.github.johnrengelman.shadow")
}

group = "de.fxlae"
version = "0.2.0"
version = "0.3.0"

java {
withJavadocJar()
Expand All @@ -30,42 +29,9 @@ tasks.javadoc {
}
}

tasks.named("jar").configure {
enabled = false
}

tasks.withType<GenerateModuleMetadata> {
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<MavenPublication>("mavenJava") {
Expand Down
13 changes: 5 additions & 8 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
[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"
java-uuid-generator = "5.0.0"
jackson = "2.17.0"
junit = "5.10.2"
assertj = "3.25.3"
jmh = "0.7.2"

[plugins]
jmh = { id = "me.champeau.jmh", version.ref = "jmh" }
Expand All @@ -14,5 +13,3 @@ java-uuid-generator = { module = "com.fasterxml.uuid:java-uuid-generator", versi
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" }

2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
5 changes: 1 addition & 4 deletions lib/java17/build.gradle.kts → lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ plugins {
}

tasks.compileJava {
options.release.set(17)
options.release = 17
}

dependencies {
"provided"(project(":lib:shared"))
implementation(libs.java.uuid.generator)
testImplementation(project(path = ":lib:shared", configuration = "testArtifacts"))
jmh(project(":lib:shared"))
}

jmh {
Expand Down
File renamed without changes.
9 changes: 0 additions & 9 deletions lib/java17/src/test/java/de/fxlae/typeid/SpecTest.java

This file was deleted.

Loading

0 comments on commit 791ad7b

Please sign in to comment.