Skip to content

Commit

Permalink
Add support for kotlin serialization (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
slinkydeveloper authored Feb 13, 2024
1 parent fc4d9d0 commit 1b26150
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 11 deletions.
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent
plugins {
java
kotlin("jvm") version "1.9.20" apply false
kotlin("plugin.serialization") version "1.9.20" apply false

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

allowedLicensesFile = file("$rootDir/config/allowed-licenses.json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.examples

import dev.restate.sdk.common.CoreSerdes
import dev.restate.sdk.common.StateKey
import dev.restate.sdk.examples.generated.*
import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder
import dev.restate.sdk.kotlin.KeyedContext
import dev.restate.sdk.kotlin.KtSerdes
import org.apache.logging.log4j.LogManager

class CounterKt : CounterRestateKt.CounterRestateKtImplBase() {

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

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

override suspend fun reset(context: KeyedContext, request: CounterRequest) {
context.clear(TOTAL)
Expand Down
3 changes: 3 additions & 0 deletions sdk-api-kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import com.google.protobuf.gradle.id
plugins {
java
kotlin("jvm")
kotlin("plugin.serialization")
`library-publishing-conventions`
}

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

implementation(kotlinLibs.kotlinx.coroutines)
implementation(kotlinLibs.kotlinx.serialization.core)
implementation(kotlinLibs.kotlinx.serialization.json)

testImplementation(project(":sdk-core"))
testImplementation(testingLibs.junit.jupiter)
Expand Down
36 changes: 36 additions & 0 deletions sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/KtSerdes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.kotlin

import dev.restate.sdk.common.Serde
import java.nio.charset.StandardCharsets
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer

object KtSerdes {

/** Creates a [Serde] implementation using the `kotlinx.serialization` json module. */
inline fun <reified T> json(): Serde<T> {
return json(serializer())
}

/** Creates a [Serde] implementation using the `kotlinx.serialization` json module. */
fun <T> json(serializer: KSerializer<T>): Serde<T> {
return object : Serde<T> {
override fun serialize(value: T?): ByteArray {
return Json.encodeToString(serializer, value!!).encodeToByteArray()
}

override fun deserialize(value: ByteArray?): T {
return Json.decodeFromString(serializer, String(value!!, StandardCharsets.UTF_8))
}
}
}
}
6 changes: 2 additions & 4 deletions sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,7 @@ sealed interface Context {
sealed interface KeyedContext : Context {

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

/**
* Sets the given value under the given key, serializing the value using the registered
* [dev.restate.sdk.core.serde.Serde] in the interceptor.
* Sets the given value under the given key, serializing the value using the [StateKey.serde].
*
* @param key identifying the value to store and its type.
* @param value to store under the given key.
Expand Down
73 changes: 69 additions & 4 deletions sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/StateTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.kotlin

import com.google.protobuf.kotlin.toByteStringUtf8
import dev.restate.generated.service.protocol.Protocol
import dev.restate.sdk.common.CoreSerdes
import dev.restate.sdk.common.StateKey
import dev.restate.sdk.core.ProtoUtils.*
import dev.restate.sdk.core.StateTestSuite
import dev.restate.sdk.core.testservices.GreeterGrpcKt
import dev.restate.sdk.core.testservices.GreetingRequest
import dev.restate.sdk.core.testservices.GreetingResponse
import dev.restate.sdk.core.testservices.greetingResponse
import dev.restate.sdk.core.TestDefinitions.TestDefinition
import dev.restate.sdk.core.TestDefinitions.testInvocation
import dev.restate.sdk.core.testservices.*
import io.grpc.BindableService
import java.util.stream.Stream
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class StateTest : StateTestSuite() {
private class GetState :
Expand Down Expand Up @@ -51,4 +57,63 @@ class StateTest : StateTestSuite() {
override fun setNullState(): BindableService {
throw UnsupportedOperationException("The kotlin type system enforces non null state values")
}

// --- Test using KTSerdes

@Serializable data class Data(var a: Int, val b: String)

private class GetAndSetStateUsingKtSerdes : GreeterRestateKt.GreeterRestateKtImplBase() {

companion object {
val DATA: StateKey<Data> = StateKey.of("STATE", KtSerdes.json())
}

override suspend fun greet(context: KeyedContext, request: GreetingRequest): GreetingResponse {
val state = context.get(DATA)!!
state.a += 1
context.set(DATA, state)

return greetingResponse { message = "Hello $state" }
}
}

override fun definitions(): Stream<TestDefinition> {
return Stream.concat(
super.definitions(),
Stream.of(
testInvocation(GetAndSetStateUsingKtSerdes(), GreeterGrpc.getGreetMethod())
.withInput(
startMessage(3),
inputMessage(greetingRequest("Till")),
getStateMessage("STATE", Data(1, "Till")),
setStateMessage("STATE", Data(2, "Till")))
.expectingOutput(
outputMessage(greetingResponse("Hello " + Data(2, "Till"))), END_MESSAGE)
.named("With GetState and SetState"),
testInvocation(GetAndSetStateUsingKtSerdes(), GreeterGrpc.getGreetMethod())
.withInput(
startMessage(2),
inputMessage(greetingRequest("Till")),
getStateMessage("STATE", Data(1, "Till")))
.expectingOutput(
setStateMessage("STATE", Data(2, "Till")),
outputMessage(greetingResponse("Hello " + Data(2, "Till"))),
END_MESSAGE)
.named("With GetState already completed"),
))
}

companion object {
fun getStateMessage(key: String, data: Data): Protocol.GetStateEntryMessage.Builder {
return Protocol.GetStateEntryMessage.newBuilder()
.setKey(key.toByteStringUtf8())
.setValue(Json.encodeToString(data).toByteStringUtf8())
}

fun setStateMessage(key: String, data: Data): Protocol.SetStateEntryMessage.Builder {
return Protocol.SetStateEntryMessage.newBuilder()
.setKey(key.toByteStringUtf8())
.setValue(Json.encodeToString(data).toByteStringUtf8())
}
}
}
4 changes: 4 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ dependencyResolutionManagement {
create("kotlinLibs") {
library("kotlinx-coroutines", "org.jetbrains.kotlinx", "kotlinx-coroutines-core")
.version("1.7.3")
library("kotlinx-serialization-core", "org.jetbrains.kotlinx", "kotlinx-serialization-core")
.version("1.6.2")
library("kotlinx-serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json")
.version("1.6.2")
}
create("testingLibs") {
version("junit-jupiter", "5.9.1")
Expand Down

0 comments on commit 1b26150

Please sign in to comment.