Skip to content

Commit b635735

Browse files
committed
basic support for Kotlin Serialization
[closes #4]
1 parent 1365a87 commit b635735

File tree

14 files changed

+404
-0
lines changed

14 files changed

+404
-0
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,51 @@ But with the index, the `ObjectUuidTypesContributor` reads it, and registers the
227227
With the classes indexed, the system will know that the `User.Id` should be handled by `ObjectUuidType` and the `@Type(...)` can be dropped.
228228
This also simplifies usage on every other place, where Hibernate might need to resolve a type for the `Id` instance, like queries.
229229
230+
## Usage: (de)serialization with Jackson
231+
232+
This library provides `ObjectBigIntIdJacksonModule` and `ObjectUuidJacksonModule`, which can be registered automatically via the standard `java.util.ServiceLoader` mechanism, or explicitly.
233+
234+
## Usage: (de)serialization with Kotlin Serialization
235+
236+
This library supports two mechanism for the standard Kotlin Serialization.
237+
238+
### Kotlin Serialization: Explicit
239+
240+
This mechanism requires explicit setup for each ID class
241+
242+
```kotlin
243+
@Serializable(with = Id.Serializer::class)
244+
class UserId private constructor(id: UUID) : ObjectUuid<UserId>(id) {
245+
// standard boilerplate ...
246+
247+
object Serializer : ObjectUuidSerializer<UserId>(::UserId)
248+
}
249+
```
250+
251+
but in return every time you use it, it just works
252+
253+
```kotlin
254+
@Serializable
255+
data class UserDto(val id: UserId)
256+
```
257+
258+
### Kotlin Serialization: Contextual
259+
260+
With contextual, the standard ID boiler can be used, but you have to register the module
261+
262+
```kotlin
263+
val json = Json {
264+
serializersModule = ObjectUuidKotlinxSerializationModule.fromIndex
265+
}
266+
```
267+
268+
and then mark the type as `@Contextual` on every usage
269+
270+
```kotlin
271+
@Serializable
272+
data class UserDto(@Contextual val id: UserId)
273+
```
274+
230275
## More examples
231276
232277
To learn more you can explore the `testing/` directory of this library,

buildSrc/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ repositories {
88
}
99

1010
dependencies {
11+
val kotlinVersion = "2.1.0"
12+
13+
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
14+
implementation("org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}")
15+
1116
implementation("com.adarshr:gradle-test-logger-plugin:4.0.0")
1217
implementation("io.github.joselion:strict-null-check:3.5.0")
1318
implementation("net.ltgt.gradle:gradle-errorprone-plugin:4.1.0")

buildSrc/src/main/kotlin/framefork.java.gradle.kts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import net.ltgt.gradle.errorprone.errorprone
22
import net.ltgt.gradle.nullaway.nullaway
3+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
35

46
plugins {
57
`java-library`
@@ -8,6 +10,7 @@ plugins {
810
id("io.github.joselion.strict-null-check")
911
id("net.ltgt.errorprone")
1012
id("net.ltgt.nullaway")
13+
id("org.jetbrains.kotlin.jvm")
1114
}
1215

1316
repositories {
@@ -76,6 +79,19 @@ nullaway {
7679
annotatedPackages.add("org.framefork")
7780
}
7881

82+
tasks.withType<KotlinCompile>() {
83+
dependsOn("generatePackageInfo")
84+
85+
compilerOptions {
86+
freeCompilerArgs = listOf(
87+
"-Xjsr305=strict",
88+
"-Xjvm-default=all",
89+
"-Xsuppress-version-warnings",
90+
)
91+
jvmTarget = JvmTarget.JVM_21 // This option specifies the target version of the generated JVM bytecode
92+
}
93+
}
94+
7995
tasks.withType<JavaCompile> {
8096
options.encoding = "UTF-8"
8197
options.compilerArgs.add("-Xlint:all,-fallthrough,-processing,-serial,-classfile,-path,-this-escape")

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ hibernate-orm-v66 = { group = "org.hibernate.orm", name = "hibernate-core", vers
3434
hypersistence-utils-hibernate62 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-62", version = "3.9.4" }
3535
hypersistence-utils-hibernate63 = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version = "3.9.9" }
3636
hypersistence-tsid = { module = "io.hypersistence:hypersistence-tsid", version = "2.1.4" }
37+
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.0" }

modules/typed-ids/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ dependencies {
1414
annotationProcessor(libs.autoService.processor)
1515

1616
compileOnly(libs.jackson.databind)
17+
compileOnly(libs.kotlinx.serialization)
1718

1819
testImplementation(project(":typed-ids-testing"))
1920
testImplementation(libs.jackson.databind)
21+
testImplementation(libs.kotlinx.serialization)
2022
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
2123
}
2224

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.framefork.typedIds.bigint.json.kotlinxSerialization
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.modules.SerializersModule
5+
import org.framefork.typedIds.TypedIdsRegistry
6+
import org.framefork.typedIds.bigint.ObjectBigIntId
7+
import org.framefork.typedIds.bigint.ObjectBigIntIdTypeUtils
8+
import org.framefork.typedIds.common.ReflectionHacks
9+
import java.util.concurrent.ConcurrentHashMap
10+
11+
object ObjectBigIntIdKotlinxSerializationModule {
12+
13+
/**
14+
* Uses [org.framefork.typedIds.TypedIdsRegistry] to construct the module.
15+
* This requires the ID types are indexed compile-time.
16+
*/
17+
val fromIndex = SerializersModule {
18+
for (javaClass in TypedIdsRegistry.getObjectBigIntIdClasses()) {
19+
@Suppress("UNCHECKED_CAST")
20+
contextual(javaClass.kotlin, typedIdSerializerProvider(javaClass as Any as Class<out ObjectBigIntId<*>>))
21+
}
22+
}
23+
24+
private fun <T> typedIdSerializerProvider(javaClass: Class<T>): (typeArgumentsSerializers: List<KSerializer<*>>) -> KSerializer<*> where T : ObjectBigIntId<T> {
25+
val fromLongConstructor = Cache.typedIdConstructor(javaClass)
26+
return { ObjectBigIntIdSerializer(fromLongConstructor) }
27+
}
28+
29+
private object Cache {
30+
31+
private val constructorByType = ConcurrentHashMap<String, (Long) -> ObjectBigIntId<*>>()
32+
33+
@Suppress("UNCHECKED_CAST")
34+
fun <T : Any> typedIdConstructor(javaClass: Class<T>): (Long) -> T =
35+
constructorByType.computeIfAbsent(javaClass.name) {
36+
val mainConstructor = ReflectionHacks.getMainConstructor(javaClass, *arrayOf(Long::class.java));
37+
return@computeIfAbsent { raw -> ObjectBigIntIdTypeUtils.wrapBigIntToIdentifier(raw, mainConstructor) };
38+
} as (Long) -> T
39+
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.framefork.typedIds.bigint.json.kotlinxSerialization
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.descriptors.PrimitiveKind
5+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
6+
import kotlinx.serialization.descriptors.SerialDescriptor
7+
import kotlinx.serialization.encoding.Decoder
8+
import kotlinx.serialization.encoding.Encoder
9+
import org.framefork.typedIds.bigint.ObjectBigIntId
10+
11+
open class ObjectBigIntIdSerializer<T : ObjectBigIntId<*>>(
12+
private val fromLongConstructor: (Long) -> T,
13+
) : KSerializer<T> {
14+
15+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ObjectBigIntId", PrimitiveKind.STRING)
16+
17+
override fun serialize(encoder: Encoder, value: T) {
18+
encoder.encodeLong(value.toLong());
19+
}
20+
21+
override fun deserialize(decoder: Decoder): T {
22+
val rawLong = decoder.decodeLong()
23+
return fromLongConstructor.invoke(rawLong)
24+
}
25+
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.framefork.typedIds.uuid.json.kotlinxSerialization
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.modules.SerializersModule
5+
import org.framefork.typedIds.TypedIdsRegistry
6+
import org.framefork.typedIds.common.ReflectionHacks
7+
import org.framefork.typedIds.uuid.ObjectUuid
8+
import org.framefork.typedIds.uuid.ObjectUuidTypeUtils
9+
import java.util.*
10+
import java.util.concurrent.ConcurrentHashMap
11+
12+
object ObjectUuidKotlinxSerializationModule {
13+
14+
/**
15+
* Uses [org.framefork.typedIds.TypedIdsRegistry] to construct the module.
16+
* This requires the ID types are indexed compile-time.
17+
*/
18+
val fromIndex = SerializersModule {
19+
for (javaClass in TypedIdsRegistry.getObjectUuidClasses()) {
20+
@Suppress("UNCHECKED_CAST")
21+
contextual(javaClass.kotlin, typedIdSerializerProvider(javaClass as Any as Class<out ObjectUuid<*>>))
22+
}
23+
}
24+
25+
private fun <T> typedIdSerializerProvider(javaClass: Class<T>): (typeArgumentsSerializers: List<KSerializer<*>>) -> KSerializer<*> where T : ObjectUuid<T> {
26+
val fromUuidConstructor = Cache.typedIdConstructor(javaClass)
27+
return { ObjectUuidSerializer(fromUuidConstructor) }
28+
}
29+
30+
private object Cache {
31+
32+
private val constructorByType = ConcurrentHashMap<String, (UUID) -> ObjectUuid<*>>()
33+
34+
@Suppress("UNCHECKED_CAST")
35+
fun <T : Any> typedIdConstructor(javaClass: Class<T>): (UUID) -> T =
36+
constructorByType.computeIfAbsent(javaClass.name) {
37+
val mainConstructor = ReflectionHacks.getMainConstructor(javaClass, *arrayOf(UUID::class.java));
38+
return@computeIfAbsent { raw -> ObjectUuidTypeUtils.wrapUuidToIdentifier(raw, mainConstructor) };
39+
} as (UUID) -> T
40+
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.framefork.typedIds.uuid.json.kotlinxSerialization
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.descriptors.PrimitiveKind
5+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
6+
import kotlinx.serialization.descriptors.SerialDescriptor
7+
import kotlinx.serialization.encoding.Decoder
8+
import kotlinx.serialization.encoding.Encoder
9+
import org.framefork.typedIds.uuid.ObjectUuid
10+
import java.util.*
11+
12+
open class ObjectUuidSerializer<T : ObjectUuid<*>>(
13+
private val fromUuidConstructor: (UUID) -> T,
14+
) : KSerializer<T> {
15+
16+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ObjectUuid", PrimitiveKind.STRING)
17+
18+
override fun serialize(encoder: Encoder, value: T) {
19+
encoder.encodeString(value.toString());
20+
}
21+
22+
override fun deserialize(decoder: Decoder): T {
23+
val rawUuid = UUID.fromString(decoder.decodeString())
24+
return fromUuidConstructor.invoke(rawUuid)
25+
}
26+
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
plugins {
2+
id("framefork.java")
3+
kotlin("plugin.serialization")
4+
kotlin("kapt")
5+
}
6+
7+
dependencies {
8+
implementation(project(":typed-ids"))
9+
implementation(libs.kotlinx.serialization)
10+
11+
kapt(project(":typed-ids-index-java-classes-processor"))
12+
13+
testImplementation(project(":typed-ids-testing"))
14+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.framefork.typedIds.bigint.kotlinxSerialization
2+
3+
import kotlinx.serialization.Contextual
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.encodeToString
6+
import kotlinx.serialization.json.Json
7+
import org.assertj.core.api.Assertions.assertThat
8+
import org.framefork.typedIds.bigint.ObjectBigIntId
9+
import org.framefork.typedIds.bigint.json.kotlinxSerialization.ObjectBigIntIdKotlinxSerializationModule
10+
import org.junit.jupiter.api.Test
11+
12+
class ObjectBigIntIdKotlinxSerializationContextualTest {
13+
14+
val json = Json {
15+
serializersModule = ObjectBigIntIdKotlinxSerializationModule.fromIndex
16+
}
17+
18+
@Test
19+
fun functional() {
20+
val id = UserEntity.Id.from(42)
21+
val dto = UserDto(id)
22+
23+
val dtoJson = json.encodeToString(dto)
24+
assertThat(dtoJson).isEqualTo("""{"id":42}""")
25+
26+
val decodedDto = json.decodeFromString<UserDto>(dtoJson)
27+
assertThat(decodedDto).isEqualTo(dto)
28+
}
29+
30+
@Serializable
31+
data class UserDto(@Contextual val id: UserEntity.Id)
32+
33+
class UserEntity(val id: Id) {
34+
35+
class Id private constructor(id: Long) : ObjectBigIntId<Id>(id) {
36+
companion object {
37+
fun random() = randomBigInt(::Id)
38+
fun from(value: String) = fromString(::Id, value)
39+
fun from(value: Long) = fromLong(::Id, value)
40+
}
41+
}
42+
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.framefork.typedIds.bigint.kotlinxSerialization
2+
3+
import kotlinx.serialization.Serializable
4+
import kotlinx.serialization.encodeToString
5+
import kotlinx.serialization.json.Json
6+
import org.assertj.core.api.Assertions.assertThat
7+
import org.framefork.typedIds.bigint.ObjectBigIntId
8+
import org.framefork.typedIds.bigint.json.kotlinxSerialization.ObjectBigIntIdSerializer
9+
import org.junit.jupiter.api.Test
10+
11+
class ObjectBigIntIdKotlinxSerializationExplicitTest {
12+
13+
val json = Json.Default
14+
15+
@Test
16+
fun functional() {
17+
val id = UserEntity.Id.from(42)
18+
val dto = UserDto(id)
19+
20+
val dtoJson = json.encodeToString(dto)
21+
assertThat(dtoJson).isEqualTo("""{"id":42}""")
22+
23+
val decodedDto = json.decodeFromString<UserDto>(dtoJson)
24+
assertThat(decodedDto).isEqualTo(dto)
25+
}
26+
27+
@Serializable
28+
data class UserDto(val id: UserEntity.Id)
29+
30+
class UserEntity(val id: Id) {
31+
32+
@Serializable(with = Id.Serializer::class)
33+
class Id private constructor(id: Long) : ObjectBigIntId<Id>(id) {
34+
companion object {
35+
fun random() = randomBigInt(::Id)
36+
fun from(value: String) = fromString(::Id, value)
37+
fun from(value: Long) = fromLong(::Id, value)
38+
}
39+
40+
object Serializer : ObjectBigIntIdSerializer<Id>(::Id)
41+
}
42+
43+
}
44+
45+
}

0 commit comments

Comments
 (0)