Skip to content

Commit 1b26150

Browse files
Add support for kotlin serialization (#224)
1 parent fc4d9d0 commit 1b26150

File tree

7 files changed

+121
-11
lines changed

7 files changed

+121
-11
lines changed

build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent
55
plugins {
66
java
77
kotlin("jvm") version "1.9.20" apply false
8+
kotlin("plugin.serialization") version "1.9.20" apply false
89

910
id("net.ltgt.errorprone") version "3.0.1"
1011
id("com.github.jk1.dependency-license-report") version "2.0"
@@ -70,7 +71,10 @@ allprojects {
7071
arrayOf(
7172
"io.vertx:vertx-stack-depchain", // Vertx bom file
7273
"com.google.guava:guava-parent", // Guava bom
73-
"org.jetbrains.kotlinx:kotlinx-coroutines-core", // Kotlinx coroutines bom file
74+
// kotlinx dependencies are APL 2, but somehow the plugin doesn't recognize that.
75+
"org.jetbrains.kotlinx:kotlinx-coroutines-core",
76+
"org.jetbrains.kotlinx:kotlinx-serialization-core",
77+
"org.jetbrains.kotlinx:kotlinx-serialization-json",
7478
)
7579

7680
allowedLicensesFile = file("$rootDir/config/allowed-licenses.json")

examples/src/main/kotlin/dev/restate/sdk/examples/CounterKt.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
99
package dev.restate.sdk.examples
1010

11-
import dev.restate.sdk.common.CoreSerdes
1211
import dev.restate.sdk.common.StateKey
1312
import dev.restate.sdk.examples.generated.*
1413
import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder
1514
import dev.restate.sdk.kotlin.KeyedContext
15+
import dev.restate.sdk.kotlin.KtSerdes
1616
import org.apache.logging.log4j.LogManager
1717

1818
class CounterKt : CounterRestateKt.CounterRestateKtImplBase() {
1919

2020
private val LOG = LogManager.getLogger(CounterKt::class.java)
2121

22-
private val TOTAL = StateKey.of("total", CoreSerdes.JSON_LONG)
22+
private val TOTAL = StateKey.of<Long>("total", KtSerdes.json())
2323

2424
override suspend fun reset(context: KeyedContext, request: CounterRequest) {
2525
context.clear(TOTAL)

sdk-api-kotlin/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import com.google.protobuf.gradle.id
33
plugins {
44
java
55
kotlin("jvm")
6+
kotlin("plugin.serialization")
67
`library-publishing-conventions`
78
}
89

@@ -12,6 +13,8 @@ dependencies {
1213
api(project(":sdk-common"))
1314

1415
implementation(kotlinLibs.kotlinx.coroutines)
16+
implementation(kotlinLibs.kotlinx.serialization.core)
17+
implementation(kotlinLibs.kotlinx.serialization.json)
1518

1619
testImplementation(project(":sdk-core"))
1720
testImplementation(testingLibs.junit.jupiter)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.kotlin
10+
11+
import dev.restate.sdk.common.Serde
12+
import java.nio.charset.StandardCharsets
13+
import kotlinx.serialization.KSerializer
14+
import kotlinx.serialization.json.Json
15+
import kotlinx.serialization.serializer
16+
17+
object KtSerdes {
18+
19+
/** Creates a [Serde] implementation using the `kotlinx.serialization` json module. */
20+
inline fun <reified T> json(): Serde<T> {
21+
return json(serializer())
22+
}
23+
24+
/** Creates a [Serde] implementation using the `kotlinx.serialization` json module. */
25+
fun <T> json(serializer: KSerializer<T>): Serde<T> {
26+
return object : Serde<T> {
27+
override fun serialize(value: T?): ByteArray {
28+
return Json.encodeToString(serializer, value!!).encodeToByteArray()
29+
}
30+
31+
override fun deserialize(value: ByteArray?): T {
32+
return Json.decodeFromString(serializer, String(value!!, StandardCharsets.UTF_8))
33+
}
34+
}
35+
}
36+
}

sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,7 @@ sealed interface Context {
212212
sealed interface KeyedContext : Context {
213213

214214
/**
215-
* Gets the state stored under key, deserializing the raw value using the registered
216-
* [dev.restate.sdk.core.serde.Serde] in the interceptor.
215+
* Gets the state stored under key, deserializing the raw value using the [StateKey.serde].
217216
*
218217
* @param key identifying the state to get and its type.
219218
* @return the value containing the stored state deserialized.
@@ -229,8 +228,7 @@ sealed interface KeyedContext : Context {
229228
suspend fun stateKeys(): Collection<String>
230229

231230
/**
232-
* Sets the given value under the given key, serializing the value using the registered
233-
* [dev.restate.sdk.core.serde.Serde] in the interceptor.
231+
* Sets the given value under the given key, serializing the value using the [StateKey.serde].
234232
*
235233
* @param key identifying the value to store and its type.
236234
* @param value to store under the given key.

sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/StateTest.kt

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
99
package dev.restate.sdk.kotlin
1010

11+
import com.google.protobuf.kotlin.toByteStringUtf8
12+
import dev.restate.generated.service.protocol.Protocol
1113
import dev.restate.sdk.common.CoreSerdes
1214
import dev.restate.sdk.common.StateKey
15+
import dev.restate.sdk.core.ProtoUtils.*
1316
import dev.restate.sdk.core.StateTestSuite
14-
import dev.restate.sdk.core.testservices.GreeterGrpcKt
15-
import dev.restate.sdk.core.testservices.GreetingRequest
16-
import dev.restate.sdk.core.testservices.GreetingResponse
17-
import dev.restate.sdk.core.testservices.greetingResponse
17+
import dev.restate.sdk.core.TestDefinitions.TestDefinition
18+
import dev.restate.sdk.core.TestDefinitions.testInvocation
19+
import dev.restate.sdk.core.testservices.*
1820
import io.grpc.BindableService
21+
import java.util.stream.Stream
1922
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.serialization.Serializable
24+
import kotlinx.serialization.encodeToString
25+
import kotlinx.serialization.json.Json
2026

2127
class StateTest : StateTestSuite() {
2228
private class GetState :
@@ -51,4 +57,63 @@ class StateTest : StateTestSuite() {
5157
override fun setNullState(): BindableService {
5258
throw UnsupportedOperationException("The kotlin type system enforces non null state values")
5359
}
60+
61+
// --- Test using KTSerdes
62+
63+
@Serializable data class Data(var a: Int, val b: String)
64+
65+
private class GetAndSetStateUsingKtSerdes : GreeterRestateKt.GreeterRestateKtImplBase() {
66+
67+
companion object {
68+
val DATA: StateKey<Data> = StateKey.of("STATE", KtSerdes.json())
69+
}
70+
71+
override suspend fun greet(context: KeyedContext, request: GreetingRequest): GreetingResponse {
72+
val state = context.get(DATA)!!
73+
state.a += 1
74+
context.set(DATA, state)
75+
76+
return greetingResponse { message = "Hello $state" }
77+
}
78+
}
79+
80+
override fun definitions(): Stream<TestDefinition> {
81+
return Stream.concat(
82+
super.definitions(),
83+
Stream.of(
84+
testInvocation(GetAndSetStateUsingKtSerdes(), GreeterGrpc.getGreetMethod())
85+
.withInput(
86+
startMessage(3),
87+
inputMessage(greetingRequest("Till")),
88+
getStateMessage("STATE", Data(1, "Till")),
89+
setStateMessage("STATE", Data(2, "Till")))
90+
.expectingOutput(
91+
outputMessage(greetingResponse("Hello " + Data(2, "Till"))), END_MESSAGE)
92+
.named("With GetState and SetState"),
93+
testInvocation(GetAndSetStateUsingKtSerdes(), GreeterGrpc.getGreetMethod())
94+
.withInput(
95+
startMessage(2),
96+
inputMessage(greetingRequest("Till")),
97+
getStateMessage("STATE", Data(1, "Till")))
98+
.expectingOutput(
99+
setStateMessage("STATE", Data(2, "Till")),
100+
outputMessage(greetingResponse("Hello " + Data(2, "Till"))),
101+
END_MESSAGE)
102+
.named("With GetState already completed"),
103+
))
104+
}
105+
106+
companion object {
107+
fun getStateMessage(key: String, data: Data): Protocol.GetStateEntryMessage.Builder {
108+
return Protocol.GetStateEntryMessage.newBuilder()
109+
.setKey(key.toByteStringUtf8())
110+
.setValue(Json.encodeToString(data).toByteStringUtf8())
111+
}
112+
113+
fun setStateMessage(key: String, data: Data): Protocol.SetStateEntryMessage.Builder {
114+
return Protocol.SetStateEntryMessage.newBuilder()
115+
.setKey(key.toByteStringUtf8())
116+
.setValue(Json.encodeToString(data).toByteStringUtf8())
117+
}
118+
}
54119
}

settings.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ dependencyResolutionManagement {
8686
create("kotlinLibs") {
8787
library("kotlinx-coroutines", "org.jetbrains.kotlinx", "kotlinx-coroutines-core")
8888
.version("1.7.3")
89+
library("kotlinx-serialization-core", "org.jetbrains.kotlinx", "kotlinx-serialization-core")
90+
.version("1.6.2")
91+
library("kotlinx-serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json")
92+
.version("1.6.2")
8993
}
9094
create("testingLibs") {
9195
version("junit-jupiter", "5.9.1")

0 commit comments

Comments
 (0)