https://github.com/aleris/typeid-kotlin
A Kotlin implementation of TypeID (Type-safe, K-sortable, globally unique identifier inspired by Stripe IDs).
https://github.com/aleris/typeid-kotlin
typeid uuidv7
Last synced: 3 months ago
JSON representation
A Kotlin implementation of TypeID (Type-safe, K-sortable, globally unique identifier inspired by Stripe IDs).
- Host: GitHub
- URL: https://github.com/aleris/typeid-kotlin
- Owner: aleris
- License: apache-2.0
- Created: 2024-05-17T04:22:13.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2024-11-29T13:57:06.000Z (over 1 year ago)
- Last Synced: 2024-11-29T14:44:28.732Z (over 1 year ago)
- Topics: typeid, uuidv7
- Language: Kotlin
- Homepage: https://aleris.github.io/typeid-kotlin/
- Size: 608 KB
- Stars: 4
- Watchers: 2
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# typeid-kotlin


## A Kotlin implementation of [TypeID](https://github.com/jetpack-io/typeid).
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).
Based on the Java implementation from [fxlae/typeid-java](https://github.com/fxlae/typeid-java).
This implementation adds a more complete type safety including id and their prefixes and uses an idiomatic Kotlin API.
[API Documentation](https://aleris.github.io/typeid-kotlin/)
## Dependency
To use with Maven:
```xml
earth.adi
typeid-kotlin
1.0.1
```
To use via Gradle:
```kotlin
implementation("earth.adi:typeid-kotlin:1.0.1")
```
## Usage
The `TypeId` class is the main entry point for working with TypeIDs.
The class can be used to generate `Id` instances or parse them from strings.
They are typesafe, immutable and thread-safe.
### Generating ids
To use the typed features of the library, you need to define your typed id associated with an entity.
```kotlin
// Define your identifiable entity type:
data class User(val id: UserId) // can contain other fields
// Define a typealias for the user id.
typealias UserId = Id
```
#### generate
To generate a new `Id`, based on UUIDv7 as per specification:
```kotlin
// create a reusable TypeId instance, can be stored in a DI container
val typeId = typeId()
val userId: UserId = typeId.generate()
println(userId) // prints something like user_01h455vb4pex5vsknk084sn02q
println(typeId.typedPrefix.prefix) // "user"
println(typeId.uuid) // java.util.UUID(01890a5d-ac96-774b-bcce-b302099a8057)
```
This is inferring the Java type of the typeid to `UserId`.
Alternatively, specifying the entity type, for example for `User`,
will also generate the associated id `UserId`:
```kotlin
val userId = typeId.generate()
```
Or specify the type explicitly, which can also be used from Java code as it does not rely on Kotlin type inference:
```kotlin
val userId = typeId.generate(User::class.java)
```
If the type of the id can be inferred, it will also work seamlessly:
```kotlin
data class User(val id: UserId)
val user = User(typeId.generate()) // infers UserId
```
Alternatively, directly use the static methods in `TypeId`:
```kotlin
val userId: UserId = TypeId.generate()
```
Using an explicit string prefix will instead generate a `RawId`:
```kotlin
val rawId: RawId = typeId.generate("custom")
println(rawId) // prints something like custom_01h455vb4pex5vsknk084sn02q
```
Raw ids are just a string with a prefix and a UUID, without any Java/Kotlin type information,
so it is better to use typed ids whenever possible (see [Type safety](#type-safety) below).
All methods described below also have raw variants.
#### of
To construct (or reconstruct) an `Id` from an `UUID`:
```kotlin
val userId: UserId = typeId.of(someUUID)
```
or for a `RawId`:
```kotlin
val userId: RawId = TypeId.of("user", someUUID)
```
### Parsing from 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 an id:
```kotlin
val userId: UserId = typeId.parse("user_01h455vb4pex5vsknk084sn02q")
```
Invalid inputs will result in an `IllegalArgumentException`, with a message explaining the cause of the parsing failure.
To parse a `RawId`:
```kotlin
val rawId: RawId = TypeId.parse("custom_01h455vb4pex5vsknk084sn02q")
```
Will create a `RawId` instance with the prefix 'custom'.
#### parseToValidated
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):
```kotlin
val userId: UserId = typeId.parseToValidated("user_01h455vb4pex5vsknk084sn02q")
when(userId) {
is Validated.Valid -> {
val userId = userId.id
// Proceed with userId
}
is Validated.Invalid -> {
val error = userId.error
// Optionally, do something with the error message
}
}
```
The `Validated` class includes a couple of functional style helper methods like `filter` and `map`.
Example:
```kotlin
typeId
.parseToValidated("user_01h455vb4pex5vsknk084sn02q")
.filter { it.id == idFromSomewhereElse }
.map { it.id }
.ifValid { println("Valid id: $it") }
```
Another safe alternative for working with validated is to use Kotlin functions like:
```kotlin
val id = typeId.parseToValidated("user_01h455vb4pex5vsknk084sn02q")
.takeIf { it is Validated.Valid }
?.let { it as Validated.Valid }
?.id
if (id != null) {
println("Valid id: $id")
}
```
These approaches are much faster when the input is untrusted and can result in lots of exceptions otherwise
(see [Benchmarks](#benchmarks)).
### isId
Check if a string is a valid id of the given type:
```kotlin
val isUserId = typeId.isId("user_01h455vb4pex5vsknk084sn02q")
```
Is a convenience method that uses `parseToValidated` and returns a boolean.
It can be used to check if a string is a valid id of an expected type
as it returns `false` if the id is valid but of a different type.
### Type safety
At its base, a `typeid` is just a prefix followed by `_` and an encoded UUID
(see the [spec](https://github.com/jetify-com/typeid/tree/main/spec)).
After it is encoded is just a string.
This could result in bugs if you accidentally mix up ids from different entities.
```kotlin
val id: RawId = typeId.generate("user")
// ... sometime later
val orgExists = someService.checkIfOrganizationExists(id)
// returns false most of the time so the bug may be hard to find
```
The library provides a type-safe way to work with these ids, by associating them with a specific type.
1. Fail if unexpected prefix is used
```kotlin
// fails if id does not have a `user` prefix
val userId: UserId = typeId.parse(id)
```
2. Compile time safety
```kotlin
val id: UserId = typeId.parse(text)
// ... sometime later
val orgExists = someService.checkIfOrganizationExists(id)
// compile error, as id is of type `Id` (or `UserId` if using a typealias),
// not Id
```
### Customizing prefixes
The `TypeId` class can be customized to use a specific prefix for the generated ids
associated with an entity type.
For example to register a custom prefix for the `Organization` entity:
```kotlin
val typeId = typeId().withCustomPrefix(TypedPrefix("org"))
println(typeId.generate()) // prints something like org_01h455vb4pex5vsknk084sn02q
```
Another possibility is to add the `TypedPrefix` annotation to the entity instance:
```kotlin
@TypeIdPrefix("cust")
data class Customer(override val id: CustomerId)
```
This can also be useful when you want a different entity interface (maybe defined in a different module).
For example, define an interface with the `@TypeIdPrefix` annotation,
which is implemented by the entity class:
```kotlin
@TypeIdPrefix("cust")
interface CustomerIdentifiable {
val id: CustomerId
}
typealias CustomerId = Id
data class Customer(override val id: CustomerId) : CustomerIdentifiable
```
If the `@TypeIdPrefix` is present (on the entity or one of its interfaces) TypeId will use that.
Note that the prefixes registered through the `TypeId` instance will take precedence
over the ones defined with annotations, you should use just one of the two methods to define prefixes.
### Customizing the UUID generator
By default, the library uses the `UUIDv7` generator, as per typeid specification,
but you can provide your own generator.
```kotlin
// use Java UUID random generator
val typeId = typeId().withUUIDGenerator { UUID.randomUUID() }
// or using com.fasterxml.uuid:java-uuid-generator
val typeId = typeId().withUUIDGenerator { Generators.randomBasedGenerator().generate() }
```
### Serialization and deserialization
The ids in this library have built-in serialization and deserialization support
for Java, Kotlin (kotlinx.serialization), and Jackson.
#### Kotlin (kotlinx.serialization)
Both `Id` and `RawId` have `@Serializable` and can be used with `kotlinx.serialization`.
You need to include the actual serialization dependency in your project.
For example, with CBOR:
`include("io.github.microutils:kotlin-serialization-cbor:1.6.3")`
```kotlin
val bytes = Cbor.encodeToByteArray>(id)
val deserialized = Cbor.decodeFromByteArray>(bytes)
```
#### Jackson
The library provides a Jackson module to serialize and deserialize `Id` instances.
```kotlin
private val objectMapper = jacksonObjectMapper().registerModule(typeId.jacksonModule())
data class UserAndOrganization(
val user: User,
val organization: Organization,
)
val userAndOrganization =
UserAndOrganization(
User(typeId.parse("user_01hy0d96sgfx0rh975kqkspchh")),
Organization(typeId.parse("org_01hy0sk45qfmdsdme1j703yjet")),
)
val writtenJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(userAndOrganization)
// writes:
// {
// "user" : {
// "id" : "user_01hy0d96sgfx0rh975kqkspchh"
// },
// "organization" : {
// "id" : "org_01hy0sk45qfmdsdme1j703yjet"
// }
// }
val read = objectMapper.readValue(writtenJson)
// read.user.id is same as typeId.parse("user_01hy0d96sgfx0rh975kqkspchh")
```
## Using it with Spring
See [Spring Snippets](https://github.com/aleris/typeid-kotlin/wiki/Using-kotlin-TypeId-type‐safe-ids-with-Spring)
for examples on how to use `TypeId` with Spring Data and WebMvc by creating converters and formatters.
## Building From Source
Details
```console
~$ git clone https://github.com/aleris/typeid-kotlin.git
~$ cd typeid-kotling
~/typeid-kotlin sdk use java 17.0.9-tem
~/typeid-kotlin ./gradlew build
```
## Releasing
Details
```console
~$ cd typeid-kotling
# Update version in build.gradle.kts
~/typeid-kotlin ./gradlew updateReadmeVersion # updates the version in README.md from build.gradle.kts
~/typeid-kotlin ./gradlew jreleaserConfig # just to double check the configuration
~/typeid-kotlin ./gradlew clean
~/typeid-kotlin ./gradlew publish
~/typeid-kotlin ./gradlew jreleaserFullRelease
```
## Benchmarks
Details
There is a small [JMH](https://github.com/openjdk/jmh) microbenchmark included:
```console
~/typeid-kotlin ./gradlew jmh
```
In a single-threaded run, all operations perform in the range of millions of calls per second,
which should be enough for most use cases
(used setup: Eclipse Temurin 17 JDK, 2021 MacBook Pro, run on version 1.0.0).
| Benchmark | Mode | Cnt | Score | Error | Units |
|---------------------------------|-------|----:|---------------:|-----------------:|-------|
| `generate` | thrpt | 4 | 2.785.895,675 | ± 791.836,864 | ops/s |
| `generate` + `toString` | thrpt | 4 | 2.060.627,959 | ± 1.185.777,089 | ops/s |
| `of` | thrpt | 4 | 20.084.528,045 | ± 3.3543.123,085 | ops/s |
| `of` + `toString` | thrpt | 4 | 5.853.485,485 | ± 1262.620,609 | ops/s |
| `parse` (Error) | thrpt | 4 | 862.446,936 | ± 63.583,514 | ops/s |
| `parse` (Success) | thrpt | 4 | 9.335.663,639 | ± 733.015,389 | ops/s |
| `parseRaw` (Error) | thrpt | 4 | 841.795,541 | ± 143.272,942 | ops/s |
| `parseRaw` (Success) | thrpt | 4 | 13.555.610,086 | ± 5.390.579,926 | ops/s |
| `parseToValidated` (Error) | thrpt | 4 | 20.242.071,304 | ± 2.514.786,867 | ops/s |
| `parseToValidated` (Success) | thrpt | 4 | 7.145.891,307 | ± 8.080.357,687 | ops/s |
| `parseToValidatedRaw` (Error) | thrpt | 4 | 39.712.927,570 | ± 8.692.614,496 | ops/s |
| `parseToValidatedRaw` (Success) | thrpt | 4 | 12.377.605,683 | ± 4.873.224,352 | ops/s |
| `toString` | thrpt | 4 | 9.783.700,478 | ± 2.152.238,948 | ops/s |