diff --git a/backend/ingress/ingress_integration_test.go b/backend/ingress/ingress_integration_test.go
index d78873935a..cfb9b9c8de 100644
--- a/backend/ingress/ingress_integration_test.go
+++ b/backend/ingress/ingress_integration_test.go
@@ -16,7 +16,7 @@ import (
func TestHttpIngress(t *testing.T) {
in.Run(t,
- in.WithLanguages("go", "java"),
+ in.WithLanguages("go", "java", "kotlin"),
in.CopyModule("httpingress"),
in.Deploy("httpingress"),
in.SubTests(
@@ -132,7 +132,7 @@ func TestHttpIngress(t *testing.T) {
assert.Equal(t, nil, resp.Headers["Access-Control-Allow-Headers"])
})},
),
- in.IfLanguage("go", in.SubTests(
+ in.IfLanguages(in.SubTests(
// Double, Int etc work in java with JSON encoding, but test/plain is not implemented yet
in.SubTest{Name: "PostInt", Action: in.HttpCall(http.MethodPost, "/int", nil, []byte("1234"), func(t testing.TB, resp *in.HTTPResponse) {
assert.Equal(t, 200, resp.Status)
@@ -172,10 +172,10 @@ func TestHttpIngress(t *testing.T) {
in.SubTest{Name: "TestExtraFieldLenient", Action: in.HttpCall(http.MethodPost, "/lenient", nil, in.JsonData(t, in.Obj{"user_id": 123, "postId": 345, "extra": "blah"}), func(t testing.TB, resp *in.HTTPResponse) {
assert.Equal(t, 201, resp.Status)
})},
- )),
+ ), "go", "kotlin"),
in.IfLanguage("java", in.SubTests(
- // Double, Int etc work in java with JSON encoding, but test/plain is not implemented yet
+ // Double, Int etc work in java with JSON encoding, but text/plain is not implemented yet
in.SubTest{Name: "JavaVerbClient", Action: in.HttpCall(http.MethodGet, "/client", nil, nil, func(t testing.TB, resp *in.HTTPResponse) {
assert.Equal(t, 200, resp.Status)
assert.Equal(t, []byte("Hello"), resp.BodyBytes)
diff --git a/backend/ingress/testdata/kotlin/httpingress/ftl.toml b/backend/ingress/testdata/kotlin/httpingress/ftl.toml
new file mode 100644
index 0000000000..81d85fc28b
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/ftl.toml
@@ -0,0 +1,2 @@
+module = "httpingress"
+language = "kotlin"
diff --git a/backend/ingress/testdata/kotlin/httpingress/pom.xml b/backend/ingress/testdata/kotlin/httpingress/pom.xml
new file mode 100644
index 0000000000..c2e710df59
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/pom.xml
@@ -0,0 +1,14 @@
+
+
+ 4.0.0
+ ftl.httpingress
+ httpingress
+ 1.0-SNAPSHOT
+
+
+ xyz.block.ftl
+ ftl-build-parent-kotlin
+ 1.0-SNAPSHOT
+
+
+
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/ArrayType.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/ArrayType.kt
new file mode 100644
index 0000000000..4252874788
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/ArrayType.kt
@@ -0,0 +1,5 @@
+package ftl.httpingress;
+
+data class ArrayType(
+ val item: String
+)
\ No newline at end of file
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/DeleteRequest.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/DeleteRequest.kt
new file mode 100644
index 0000000000..e384ee73d5
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/DeleteRequest.kt
@@ -0,0 +1,5 @@
+package ftl.httpingress;
+
+data class DeleteRequest(
+ val userId: String
+)
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/DeleteResponse.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/DeleteResponse.kt
new file mode 100644
index 0000000000..6741a7803e
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/DeleteResponse.kt
@@ -0,0 +1,4 @@
+package ftl.httpingress;
+
+class DeleteResponse {
+}
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/GetRequest.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/GetRequest.kt
new file mode 100644
index 0000000000..218cfc1941
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/GetRequest.kt
@@ -0,0 +1,6 @@
+package ftl.httpingress;
+
+data class GetRequest(
+ val userId: String,
+ val postId: String
+)
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/GetResponse.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/GetResponse.kt
new file mode 100644
index 0000000000..8f25b90d37
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/GetResponse.kt
@@ -0,0 +1,6 @@
+package ftl.httpingress;
+
+data class GetResponse(
+ val nested: Nested,
+ val msg: String
+)
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/HelloVerb.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/HelloVerb.kt
new file mode 100644
index 0000000000..64fdcf9ad8
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/HelloVerb.kt
@@ -0,0 +1,9 @@
+package ftl.httpingress;
+
+import xyz.block.ftl.SourceVerb
+import xyz.block.ftl.Verb
+
+@Verb
+fun hello(): String {
+ return "Hello"
+}
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/Nested.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/Nested.kt
new file mode 100644
index 0000000000..87677ff8d7
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/Nested.kt
@@ -0,0 +1,8 @@
+package ftl.httpingress;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+
+data class Nested(
+ @field:JsonAlias("good_stuff")
+ val goodStuff: String
+)
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PostRequest.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PostRequest.kt
new file mode 100644
index 0000000000..ff6bbace6a
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PostRequest.kt
@@ -0,0 +1,9 @@
+package ftl.httpingress;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+
+data class PostRequest(
+ @field:JsonAlias("user_id")
+ val userId: Long,
+ val postId: Long
+)
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PostResponse.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PostResponse.kt
new file mode 100644
index 0000000000..ef3924dafa
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PostResponse.kt
@@ -0,0 +1,5 @@
+package ftl.httpingress;
+
+data class PostResponse(
+ val success: Boolean,
+)
\ No newline at end of file
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PutRequest.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PutRequest.kt
new file mode 100644
index 0000000000..a3477fd108
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PutRequest.kt
@@ -0,0 +1,5 @@
+package ftl.httpingress;
+
+data class PutRequest(
+ val postId: String
+)
\ No newline at end of file
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PutResponse.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PutResponse.kt
new file mode 100644
index 0000000000..82e0ea1315
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/PutResponse.kt
@@ -0,0 +1,5 @@
+package ftl.httpingress;
+
+class PutResponse {
+
+}
\ No newline at end of file
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/QueryParamRequest.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/QueryParamRequest.kt
new file mode 100644
index 0000000000..a761332e37
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/QueryParamRequest.kt
@@ -0,0 +1,7 @@
+package ftl.httpingress;
+
+import org.jetbrains.annotations.Nullable;
+
+data class QueryParamRequest(
+ val foo:String?
+)
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/TestHTTP.kt b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/TestHTTP.kt
new file mode 100644
index 0000000000..e23cdbdd29
--- /dev/null
+++ b/backend/ingress/testdata/kotlin/httpingress/src/main/kotlin/ftl/httpingress/TestHTTP.kt
@@ -0,0 +1,144 @@
+package ftl.httpingress;
+
+import java.util.List;
+
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import org.jboss.resteasy.reactive.ResponseHeader;
+import org.jboss.resteasy.reactive.ResponseStatus;
+import org.jboss.resteasy.reactive.RestPath;
+import org.jboss.resteasy.reactive.RestQuery;
+
+@Path("/")
+class TestHTTP {
+
+ @GET
+ @Path("/users/{userId}/posts/{postId}")
+ @ResponseHeader(name = "Get", value = ["Header from FTL"])
+ fun get(
+ @RestPath userId:Long,
+ @RestPath postId:Long
+ ): GetResponse {
+ return GetResponse(
+ nested = Nested(
+ goodStuff = "This is good stuff"
+ ),
+ msg = String.format("UserID: %s, PostID: %s", userId, postId),
+ )
+ }
+
+ @GET
+ @Path("/getquery")
+ @ResponseHeader(name = "Get", value = ["Header from FTL"])
+ fun getquery(@RestQuery userId:Long, @RestQuery postId:Long): GetResponse {
+ return GetResponse(
+ msg = String.format("UserID: %s, PostID: %s", userId, postId),
+ nested = Nested(
+ goodStuff = "This is good stuff"
+ ),
+ )
+ }
+
+ @POST
+ @Path("/users")
+ @ResponseStatus(201)
+ @ResponseHeader(name = "Post", value = ["Header from FTL"])
+ fun post(req: PostRequest): PostResponse {
+ return PostResponse(success = true)
+ }
+
+ @PUT
+ @Path("/users/{userId}")
+ @ResponseHeader(name = "Put", value = ["Header from FTL"])
+ fun put(req:PutRequest): PutResponse {
+ return PutResponse();
+ }
+
+ @DELETE
+ @Path("/users/{userId}")
+ @ResponseHeader(name = "Delete", value = ["Header from FTL"])
+ @ResponseStatus(200)
+ fun delete(@RestPath userId:String): DeleteResponse {
+ System.out.println("delete");
+ return DeleteResponse();
+ }
+
+ @GET
+ @Path("/queryparams")
+ fun query(@RestQuery foo:String): String {
+ if (foo == null) {
+ return "No value";
+ }
+ return foo;
+ }
+
+ @GET
+ @Path("/html")
+ @Produces("text/html; charset=utf-8")
+ fun html() : String {
+ return "
HTML Page From FTL 🚀!
";
+ }
+
+ @POST
+ @Path("/bytes")
+ fun bytes(b: ByteArray): ByteArray {
+ return b;
+ }
+
+ @GET
+ @Path("/empty")
+ @ResponseStatus(200)
+ fun empty() {
+ }
+
+ @POST
+ @Path("/string")
+ fun string(value:String): String {
+ return value;
+ }
+
+ @POST
+ @Path("/int")
+ @Produces(MediaType.APPLICATION_JSON)
+ fun intMethod(value:Int): Int {
+ return value;
+ }
+
+ @POST
+ @Path("/float")
+ @Produces(MediaType.APPLICATION_JSON)
+ fun floatVerb(value:Float): Float {
+ return value;
+ }
+
+ @POST
+ @Path("/bool")
+ @Produces(MediaType.APPLICATION_JSON)
+ fun bool(value:Boolean): Boolean {
+ return value;
+ }
+
+ @GET
+ @Path("/error")
+ fun error():String {
+ throw RuntimeException("Error from FTL");
+ }
+
+ @POST
+ @Path("/array/string")
+ fun arrayString(array:List): List {
+ return array;
+ }
+
+ @POST
+ @Path("/array/data")
+ fun arrayData(array:List): List {
+ return array;
+ }
+}
diff --git a/backend/ingress/testdata/kotlin/httpingress/src/main/resources/application.properties b/backend/ingress/testdata/kotlin/httpingress/src/main/resources/application.properties
new file mode 100644
index 0000000000..e69de29bb2