Skip to content

Commit

Permalink
fix: kotlin generic serialization and tests (#870)
Browse files Browse the repository at this point in the history
Fix kotlin integration tests.
Fix gson serialization with generic types.

Before this change, generic requests like `HttpRequest<GetRequest>`
would serialize the `body` field to a map instead of the concrete type
of the request.
  • Loading branch information
wesbillman authored Feb 2, 2024
1 parent 73fa3e8 commit 079c6f6
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 50 deletions.
2 changes: 1 addition & 1 deletion integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestHttpIngress(t *testing.T) {
assert.Equal(t, []string{"Header from FTL"}, resp.headers["Get"])

message, ok := resp.body["msg"].(string)
assert.True(t, ok, "message is not a string")
assert.True(t, ok, "msg is not a string")
assert.Equal(t, "UserID: 123, PostID: 456", message)

nested, ok := resp.body["nested"].(map[string]any)
Expand Down
82 changes: 48 additions & 34 deletions integration/testdata/kotlin/httpingress/Echo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,90 +12,104 @@ import xyz.block.ftl.Method
import xyz.block.ftl.Verb

data class GetRequest(
val userID: String,
val postID: String,
@Alias("userId") val userID: String,
@Alias("postId") val postID: String,
)

data class Nested(
@Alias("good_stuff") val goodStuff: String,
)

data class GetResponse(
val message: String,
@Alias("msg") val message: String,
@Alias("nested") val nested: Nested,
)

data class PostRequest(
@Alias("id") val userID: String,
val postID: String,
@Alias("user_id") val userID: Int,
val postID: Int,
)

data class PostResponse(
val message: String,
@Alias("success") val success: Boolean,
)

data class PutRequest(
val userID: String,
val postID: String,
@Alias("userId") val userID: String,
@Alias("postId") val postID: String,
)

data class PutResponse(
val message: String,
)
typealias PutResponse = Unit

data class DeleteRequest(
val userID: String,
@Alias("userId") val userID: String,
)

data class DeleteResponse(
val message: String,
)
typealias DeleteResponse = Unit
typealias HtmlRequest = Unit

class Echo {
@Verb
@Ingress(
Method.GET,
"/echo/users/{userID}/posts/{postID}",
HTTP
)
Method.GET, "/users/{userID}/posts/{postID}", HTTP)
fun `get`(context: Context, req: HttpRequest<GetRequest>): HttpResponse<GetResponse> {
return HttpResponse<GetResponse>(
status = 200,
headers = mapOf("Get" to arrayListOf("Header from FTL")),
body = GetResponse(message = "UserID: ${req.body.userID}, PostID ${req.body.postID}")
body = GetResponse(
message = "UserID: ${req.body.userID}, PostID: ${req.body.postID}",
nested = Nested(goodStuff = "This is good stuff")
)
)
}

@Verb
@Ingress(
Method.POST,
"/echo/users",
HTTP
)
@Ingress(Method.POST, "/users", HTTP)
fun post(context: Context, req: HttpRequest<PostRequest>): HttpResponse<PostResponse> {
return HttpResponse<PostResponse>(
status = 201,
headers = mapOf("Post" to arrayListOf("Header from FTL")),
body = PostResponse(message = "UserID: ${req.body.userID}, PostID ${req.body.postID}")
body = PostResponse(success = true)
)
}

@Verb
@Ingress(
Method.PUT,
"/echo/users/{userID}",
HTTP
)
@Ingress(Method.PUT, "/users/{userId}", HTTP)
fun put(context: Context, req: HttpRequest<PutRequest>): HttpResponse<PutResponse> {
return HttpResponse<PutResponse>(
status = 200,
headers = mapOf("Put" to arrayListOf("Header from FTL")),
body = PutResponse(message = "UserID: ${req.body.userID}, PostID ${req.body.postID}")
body = PutResponse
)
}

@Verb
@Ingress(Method.DELETE, "/echo/users/{userID}", HTTP)
@Ingress(Method.DELETE, "/users/{userId}", HTTP)
fun delete(context: Context, req: HttpRequest<DeleteRequest>): HttpResponse<DeleteResponse> {
return HttpResponse<DeleteResponse>(
status = 200,
headers = mapOf("Delete" to arrayListOf("Header from FTL")),
body = DeleteResponse(message = "UserID: ${req.body.userID}")
body = DeleteResponse
)
}

@Verb
@Ingress(Method.GET, "/html", HTTP)
fun html(context: Context, req: HttpRequest<HtmlRequest>): HttpResponse<String> {
return HttpResponse<String>(
status = 200,
headers = mapOf("Content-Type" to arrayListOf("text/html; charset=utf-8")),
body = "<html><body><h1>HTML Page From FTL 🚀!</h1></body></html>",
)
}

@Verb
@Ingress(Method.POST, "/bytes", HTTP)
fun bytes(context: Context, req: HttpRequest<ByteArray>): HttpResponse<ByteArray> {
return HttpResponse<ByteArray>(
status = 200,
headers = mapOf("Content-Type" to arrayListOf("application/octet-stream")),
body = req.body,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
package xyz.block.ftl.registry

import xyz.block.ftl.Context
import xyz.block.ftl.logging.Logging
import xyz.block.ftl.serializer.makeGson
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.createInstance
import kotlin.reflect.jvm.javaType

internal class VerbHandle<Resp>(
private val verbClass: KClass<*>,
private val verbFunction: KFunction<Resp>,
) {
private val gson = makeGson()

private val logger = Logging.logger(VerbHandle::class)
private val argumentType = findArgumentType(verbFunction.parameters)
val returnType: KClass<*> = verbFunction.returnType.classifier as KClass<*>

fun invokeVerbInternal(context: Context, argument: String): String {
val instance = verbClass.createInstance()

Expand All @@ -26,20 +21,12 @@ internal class VerbHandle<Resp>(
verbClass -> instance
Context::class -> context
else -> {
val req = (parameter.type.classifier as KClass<*>).java
gson.fromJson(argument, req)
gson.fromJson(argument, parameter.type.javaType)
}
}
}

val result = verbFunction.callBy(arguments)
return gson.toJson(result)
}

private fun findArgumentType(parameters: List<KParameter>): KClass<*> {
return parameters.find { param ->
// skip the owning type itself
null != param.name && Context::class != param.type.classifier
}!!.type.classifier as KClass<*>
}
}

0 comments on commit 079c6f6

Please sign in to comment.