Skip to content

Commit a0edd2c

Browse files
committed
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
1 parent 59d6b26 commit a0edd2c

File tree

37 files changed

+2137
-562
lines changed

37 files changed

+2137
-562
lines changed

README.md

Lines changed: 160 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,198 @@
11
# typeid-java
22

3-
![example workflow](https://github.com/fxlae/typeid-java/actions/workflows/build-on-push.yml/badge.svg)
3+
![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)
44

55
## A Java implementation of [TypeID](https://github.com/jetpack-io/typeid).
66

7-
TypeIDs are a modern, **type-safe**, globally unique identifier based on the upcoming
7+
TypeIDs are a modern, type-safe, globally unique identifier based on the upcoming
88
UUIDv7 standard. They provide a ton of nice properties that make them a great choice
99
as the primary identifiers for your data in a database, APIs, and distributed systems.
1010
Read more about TypeIDs in their [spec](https://github.com/jetpack-io/typeid).
1111

1212
## Installation
1313

14-
Maven:
14+
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:
15+
16+
- `typeid-java`: Requires at least Java 17. Opt for this one if the Java version is not a concern
17+
- *OR* `typeid-java-jdk8`: Supports all versions from Java 8 onwards. It handles all relevant use cases, albeit with less syntactic sugar
18+
19+
To install via Maven:
1520

1621
```xml
1722
<dependency>
1823
<groupId>de.fxlae</groupId>
19-
<artifactId>typeid-java</artifactId>
20-
<version>0.1.0</version>
24+
<artifactId>typeid-java</artifactId> <!-- or 'typeid-java-jdk8' -->
25+
<version>0.2.0</version>
2126
</dependency>
2227
```
2328

24-
Gradle:
29+
For installation via Gradle:
2530

2631
```kotlin
27-
implementation("de.fxlae:typeid-java:0.1.0")
32+
implementation("de.fxlae:typeid-java:0.2.0") // or ...typeid-java-jdk8:0.2.0
2833
```
2934

30-
## Requirements
31-
- Java 8 or higher
32-
3335
## Usage
34-
An instance of `TypeID` can be obtained in several ways. It is immutable and thread-safe. Examples:
3536

36-
Generate a new `TypeID`, based on UUIDv7:
37+
`TypeId` instances can be obtained in several ways. They are immutable and thread-safe.
38+
39+
### Generating new TypeIDs
40+
41+
To generate a new `TypeId`, based on UUIDv7 as per specification:
42+
43+
```java
44+
var typeId = TypeId.generate("user");
45+
typeId.toString(); // "user_01h455vb4pex5vsknk084sn02q"
46+
typeId.prefix(); // "user"
47+
typeId.uuid(); // java.util.UUID(01890a5d-ac96-774b-bcce-b302099a8057), based on UUIDv7
48+
```
49+
50+
To construct (or reconstruct) a `TypeId` from existing arguments, which can also be used as an "extension point" to plug-in custom UUID generators:
3751

3852
```java
39-
TypeId typeId = TypeId.generate();
40-
typeId.getPrefix(); // ""
41-
typeId.getUuid(); // v7, java.util.UUID(01890a5d-ac96-774b-bcce-b302099a8057)
42-
typeId.toString(); // "01h455vb4pex5vsknk084sn02q"
43-
44-
TypeId typeId = TypeId.generate("someprefix");
45-
typeId.getPrefix(); // "someprefix"
46-
typeId.toString(); // "someprefix_01h455vb4pex5vsknk084sn02q"
53+
var typeId = TypeId.of("user", UUID.randomUUID()); // a TypeId based on UUIDv4
4754
```
55+
### Parsing TypeID strings
56+
57+
For parsing, the library supports both an imperative programming model and a more functional style.
58+
The most straightforward way to parse the textual representation of a TypeID:
4859

49-
Construct a `TypeID` from arguments (any UUID version):
5060
```java
51-
TypeId typeId = TypeId.of("someprefix", UUID.randomUUID());
52-
typeId.getUuid(); // v4, java.util.UUID(9c8ec0e7-020b-4caf-87c0-38fb6c0ebbe2)
61+
var typeId = TypeId.parse("user_01h455vb4pex5vsknk084sn02q");
5362
```
5463

55-
Obtain an instance of `TypeID` from a text string (any UUID version):
64+
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):
65+
5666
```java
57-
TypeId typeId = TypeId.parse("01h455vb4pex5vsknk084sn02q");
58-
TypeId typeId = TypeId.parse("someprefix_01h455vb4pex5vsknk084sn02q");
67+
var maybeTypeId = TypeId.parseToOptional("user_01h455vb4pex5vsknk084sn02q");
68+
69+
// or, if you are interested in possible errors, provide handlers for success and failure
70+
var maybeTypeId = TypeId.parse("...",
71+
Optional::of, // (1) Function<TypeId, T>, called on success
72+
message -> { // (2) Function<String, T>, called on failure
73+
log.warn("Parsing failed: {}", message);
74+
return Optional.empty();
75+
});
76+
```
77+
**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`**.
78+
79+
When using `typeid-java`:
80+
- the type `TypeId` is implemented as a Java `record`
81+
- 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>`
82+
83+
`Validated<TypeId>` can be of subtype:
84+
- `Valid<TypeId>`: encapsulates a successfully parsed `TypeId`
85+
- or otherwise `Invalid<TypeId>`: contains an error message
86+
87+
A simplistic method to interact with `Validated` is to manually unwrap it, analogous to `java.util.Optional.get`:
88+
89+
```java
90+
var validated = TypeId.parseToValidated("user_01h455vb4pex5vsknk084sn02q");
91+
92+
if(validated.isValid) {
93+
var typeId = validated.get();
94+
// Proceed with typeId
95+
} else {
96+
var message = validated.message();
97+
// Optionally, do something with the error message (or omit this branch completely)
98+
}
99+
```
100+
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`.
101+
102+
A safe alternative involves methods that can be called without risk, namely:
103+
104+
- For transformations: `map`, `flatMap`, `filter`, `orElse`
105+
- For implementing side effects: `ifValid` and `ifInvalid`
106+
107+
```java
108+
// transform
109+
var mappedToPrefix = TypeId.parseToValidated("user_01h455vb4pex5vsknk084sn02q");
110+
.map(TypeId::prefix) // Validated<TypeId> -> Validated<String>
111+
.filter("Not a cat! :(", prefix -> !"cat".equals(prefix)); // the predicate fails
112+
113+
// execute side effects, e.g. logging
114+
mappedToPrefix.ifValid(prefix -> log.info(prefix)) // called on success, so not in this case
115+
mappedToPrefix.ifInvalid(message -> log.warn(message)) // logs "Not a cat! :("
116+
```
117+
118+
`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:
119+
120+
```java
121+
// this compiles and runs with oracle openjdk-21-ea+30 (preview enabled)
122+
123+
var report = switch(TypeId.parseToValidated("...")) {
124+
case Valid(TypeId(var prefix, var uuid)) when "user".equals(prefix) -> "user with UUID" + uuid;
125+
case Valid(TypeId(var prefix, _)) -> "Not a user, ignore the UUID. Prefix is " + prefix;
126+
case Invalid(var message) -> "Parsing failed :( ... " + message;
127+
}
59128
```
129+
Note the absent (and superfluous) default case. Exhaustiveness is checked during compilation!
130+
131+
## But wait, isn't this less type-safe than it could be?
132+
<details>
133+
<summary>Details</summary>
134+
135+
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.
136+
137+
Here's how a more type-safe variant could look, which I have implemented experimentally (currently not included in the artifact):
138+
139+
```java
140+
TypeId<User> typeId = TypeId<>.generate(USER);
141+
TypeId<User> anotherTypeId = TypeId<>.parse(USER, "user_01h455vb4pex5vsknk084sn02q");
142+
```
143+
144+
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:
145+
146+
```java
147+
static final User USER = new User();
148+
record User() implements TypedPrefix {
149+
@Override
150+
public String name() {
151+
return "user";
152+
}
153+
}
154+
```
155+
156+
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.
157+
158+
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.
159+
</details>
160+
161+
## A word on UUIDv7
162+
<details>
163+
<summary>Details</summary>
164+
165+
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.
166+
167+
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.
168+
169+
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).
170+
171+
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`.
172+
173+
</details>
174+
175+
## Building From Source & Benchmarks
176+
<details>
177+
<summary>Details</summary>
60178

61-
## Building From Source
62179
```console
63180
foo@bar:~$ git clone https://github.com/fxlae/typeid-java.git
64181
foo@bar:~$ cd typeid-java
65182
foo@bar:~/typeid-java$ ./gradlew build
66-
```
183+
```
184+
185+
There is a small [JMH](https://github.com/openjdk/jmh) microbenchmark included:
186+
```console
187+
foo@bar:~/typeid-java$ ./gradlew jmh
188+
```
189+
190+
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).
191+
192+
| method | op/s |
193+
|----------------------------------|----------------------:|
194+
| `TypeId.generate` + `toString` | 9.1M |
195+
| `TypeId.parse` | 9.8M |
196+
197+
The library strives to avoid heap allocations as much as possible. The only allocations made are for return values and data from `SecureRandom`.
198+
</details>

build-conventions/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
plugins {
2+
`kotlin-dsl`
3+
}
4+
5+
repositories {
6+
gradlePluginPortal()
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
implementation("com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1")
12+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = "build-conventions"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
plugins {
2+
java
3+
jacoco
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
val versionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
11+
12+
dependencies {
13+
versionCatalog.findLibrary("junit.bom").ifPresent {
14+
testImplementation(platform(it))
15+
}
16+
testImplementation("org.junit.jupiter:junit-jupiter")
17+
versionCatalog.findLibrary("jackson.dataformat.yaml").ifPresent {
18+
testImplementation(it)
19+
}
20+
versionCatalog.findLibrary("assertj.core").ifPresent {
21+
testImplementation(it)
22+
}
23+
}
24+
25+
tasks.withType<JavaCompile> {
26+
options.encoding = "UTF-8"
27+
}
28+
29+
tasks.test {
30+
useJUnitPlatform()
31+
finalizedBy(tasks.jacocoTestReport)
32+
}

build.gradle.kts renamed to build-conventions/src/main/kotlin/de/fxlae/typeid/typeid.library-conventions.gradle.kts

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,25 @@ plugins {
22
`java-library`
33
`maven-publish`
44
signing
5+
id("com.github.johnrengelman.shadow")
56
}
67

78
group = "de.fxlae"
8-
version = "0.1.1-SNAPSHOT"
9+
version = "0.2.0"
910

10-
repositories {
11-
mavenCentral()
12-
}
13-
14-
dependencies {
15-
implementation("com.fasterxml.uuid:java-uuid-generator:4.2.0")
16-
testImplementation(platform("org.junit:junit-bom:5.9.1"))
17-
testImplementation("org.junit.jupiter:junit-jupiter")
18-
testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2")
19-
}
20-
21-
tasks.compileJava {
22-
options.release.set(8)
23-
}
24-
25-
tasks.test {
26-
useJUnitPlatform()
11+
java {
12+
withJavadocJar()
13+
withSourcesJar()
2714
}
2815

2916
tasks.jar {
3017
manifest {
31-
attributes(mapOf("Implementation-Title" to project.name,
32-
"Implementation-Version" to project.version))
18+
attributes(
19+
mapOf(
20+
"Implementation-Title" to mavenArtifactId,
21+
"Implementation-Version" to project.version
22+
)
23+
)
3324
}
3425
}
3526

@@ -39,27 +30,50 @@ tasks.javadoc {
3930
}
4031
}
4132

42-
java {
43-
withJavadocJar()
44-
withSourcesJar()
33+
tasks.named("jar").configure {
34+
enabled = false
35+
}
36+
37+
tasks.withType<GenerateModuleMetadata> {
38+
enabled = false
39+
}
40+
41+
val mavenArtifactId: String by project
42+
val mavenArtifactDescription: String by project
43+
44+
tasks {
45+
shadowJar {
46+
configurations = listOf(project.configurations.compileClasspath.get())
47+
include("de/fxlae/**")
48+
from(project(":lib:shared").sourceSets.main.get().output)
49+
archiveClassifier.set("")
50+
archiveBaseName.set(mavenArtifactId)
51+
}
52+
build {
53+
dependsOn(shadowJar)
54+
}
55+
}
56+
57+
val providedConfigurationName = "provided"
58+
59+
configurations {
60+
create(providedConfigurationName)
61+
}
62+
63+
sourceSets {
64+
main.get().compileClasspath += configurations.getByName(providedConfigurationName)
65+
test.get().compileClasspath += configurations.getByName(providedConfigurationName)
66+
test.get().runtimeClasspath += configurations.getByName(providedConfigurationName)
4567
}
4668

4769
publishing {
4870
publications {
4971
create<MavenPublication>("mavenJava") {
50-
artifactId = "typeid-java"
72+
artifactId = mavenArtifactId
5173
from(components["java"])
52-
versionMapping {
53-
usage("java-api") {
54-
fromResolutionOf("runtimeClasspath")
55-
}
56-
usage("java-runtime") {
57-
fromResolutionResult()
58-
}
59-
}
6074
pom {
61-
name.set("typeid-java")
62-
description.set("A TypeID implementation for Java")
75+
name.set(mavenArtifactId)
76+
description.set(mavenArtifactDescription)
6377
url.set("https://github.com/fxlae/typeid-java")
6478
licenses {
6579
license {
@@ -99,4 +113,3 @@ signing {
99113
useInMemoryPgpKeys(signingKey, signingPassword)
100114
sign(publishing.publications["mavenJava"])
101115
}
102-

0 commit comments

Comments
 (0)