Skip to content

Commit f9fa7cb

Browse files
authored
feat: redesign kotlin verb interface (#916)
fixes #803 with this change, verbs in kotlin are defined as top-level functions rather than class member functions
1 parent 0fb4818 commit f9fa7cb

File tree

22 files changed

+670
-683
lines changed

22 files changed

+670
-683
lines changed

examples/kotlin/ftl-module-ad/src/main/kotlin/ftl/ad/Ad.kt

+37-39
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,46 @@ data class Ad(val redirectUrl: String, val text: String)
1414
data class AdRequest(val contextKeys: List<String>? = null)
1515
data class AdResponse(val ads: List<Ad>)
1616

17-
class AdModule {
18-
private val database: Map<String, Ad> = loadDatabase()
19-
20-
@Verb
21-
@HttpIngress(Method.GET, "/get")
22-
fun get(context: Context, req: HttpRequest<AdRequest>): HttpResponse<AdResponse, String> {
23-
val ads: List<Ad> = when {
24-
req.body.contextKeys != null -> contextualAds(req.body.contextKeys)
25-
else -> randomAds()
26-
}
27-
28-
return HttpResponse(status = 200, headers = emptyMap(), body = AdResponse(ads = ads))
17+
private val database: Map<String, Ad> = loadDatabase()
18+
19+
@Verb
20+
@HttpIngress(Method.GET, "/get")
21+
fun get(context: Context, req: HttpRequest<AdRequest>): HttpResponse<AdResponse, String> {
22+
val ads: List<Ad> = when {
23+
req.body.contextKeys != null -> contextualAds(req.body.contextKeys)
24+
else -> randomAds()
2925
}
3026

31-
private fun contextualAds(contextKeys: List<String>): List<Ad> {
32-
return contextKeys.map { database[it] ?: throw Exception("no ad registered for this context key") }
33-
}
27+
return HttpResponse(status = 200, headers = emptyMap(), body = AdResponse(ads = ads))
28+
}
3429

35-
private fun randomAds(): List<Ad> {
36-
val ads = mutableListOf<Ad>()
37-
val random = Random()
38-
repeat(MAX_ADS_TO_SERVE) {
39-
ads.add(database.entries.elementAt(random.nextInt(database.size)).value)
40-
}
41-
return ads
42-
}
30+
private fun contextualAds(contextKeys: List<String>): List<Ad> {
31+
return contextKeys.map { database[it] ?: throw Exception("no ad registered for this context key") }
32+
}
4333

44-
companion object {
45-
private const val MAX_ADS_TO_SERVE = 2
46-
private val DATABASE = mapOf(
47-
"hair" to Ad("/product/2ZYFJ3GM2N", "Hairdryer for sale. 50% off."),
48-
"clothing" to Ad("/product/66VCHSJNUP", "Tank top for sale. 20% off."),
49-
"accessories" to Ad("/product/1YMWWN1N4O", "Watch for sale. Buy one, get second kit for free"),
50-
"footwear" to Ad("/product/L9ECAV7KIM", "Loafers for sale. Buy one, get second one for free"),
51-
"decor" to Ad("/product/0PUK6V6EV0", "Candle holder for sale. 30% off."),
52-
"kitchen" to Ad("/product/9SIQT8TOJO", "Bamboo glass jar for sale. 10% off.")
53-
)
54-
55-
private fun loadDatabase(): Map<String, Ad> {
56-
return DATABASE
57-
}
58-
59-
inline fun <reified T> Gson.fromJson(json: String) = fromJson<T>(json, object : TypeToken<T>() {}.type)
34+
private fun randomAds(): List<Ad> {
35+
val ads = mutableListOf<Ad>()
36+
val random = Random()
37+
repeat(MAX_ADS_TO_SERVE) {
38+
ads.add(database.entries.elementAt(random.nextInt(database.size)).value)
6039
}
40+
return ads
6141
}
42+
43+
private const val MAX_ADS_TO_SERVE = 2
44+
private val DATABASE = mapOf(
45+
"hair" to Ad("/product/2ZYFJ3GM2N", "Hairdryer for sale. 50% off."),
46+
"clothing" to Ad("/product/66VCHSJNUP", "Tank top for sale. 20% off."),
47+
"accessories" to Ad("/product/1YMWWN1N4O", "Watch for sale. Buy one, get second kit for free"),
48+
"footwear" to Ad("/product/L9ECAV7KIM", "Loafers for sale. Buy one, get second one for free"),
49+
"decor" to Ad("/product/0PUK6V6EV0", "Candle holder for sale. 30% off."),
50+
"kitchen" to Ad("/product/9SIQT8TOJO", "Bamboo glass jar for sale. 10% off.")
51+
)
52+
53+
private fun loadDatabase(): Map<String, Ad> {
54+
return DATABASE
55+
}
56+
57+
inline fun <reified T> Gson.fromJson(json: String) = fromJson<T>(json, object : TypeToken<T>() {}.type)
58+
59+

examples/kotlin/ftl-module-api/src/main/kotlin/ftl/api/Api.kt

+37-38
Original file line numberDiff line numberDiff line change
@@ -53,48 +53,47 @@ private val todos = ConcurrentHashMap<Int, Todo>()
5353
private val idCounter = AtomicInteger()
5454
private val gson = makeGson()
5555

56-
class Api {
57-
private val headers = mapOf("Content-Type" to arrayListOf("application/json"))
56+
private val headers = mapOf("Content-Type" to arrayListOf("application/json"))
5857

59-
@Verb
60-
@HttpIngress(Method.GET, "/api/status")
61-
fun status(context: Context, req: HttpRequest<Empty>): HttpResponse<GetStatusResponse> {
62-
return HttpResponse<GetStatusResponse>(status = 200, headers = mapOf(), body = GetStatusResponse("OK"))
63-
}
58+
@Verb
59+
@HttpIngress(Method.GET, "/api/status")
60+
fun status(context: Context, req: HttpRequest<Empty>): HttpResponse<GetStatusResponse, String> {
61+
return HttpResponse(status = 200, headers = mapOf(), body = GetStatusResponse("OK"))
62+
}
6463

65-
@Verb
66-
@HttpIngress(Method.GET, "/api/todos/{id}")
67-
fun getTodo(context: Context, req: HttpRequest<GetTodoRequest>): HttpResponse<GetTodoResponse> {
68-
val todoId = req.pathParameters["id"]?.toIntOrNull()
69-
val todo = todos[todoId]
70-
71-
return if (todo != null) {
72-
HttpResponse<GetTodoResponse>(
73-
status = 200,
74-
headers = mapOf(),
75-
body = GetTodoResponse(todo)
76-
)
77-
} else {
78-
HttpResponse<GetTodoResponse>(status = 404, headers = mapOf(), body = GetTodoResponse(null))
79-
}
80-
}
64+
@Verb
65+
@HttpIngress(Method.GET, "/api/todos/{id}")
66+
fun getTodo(context: Context, req: HttpRequest<GetTodoRequest>): HttpResponse<GetTodoResponse, String> {
67+
val todoId = req.pathParameters["id"]?.toIntOrNull()
68+
val todo = todos[todoId]
8169

82-
@Verb
83-
@HttpIngress(Method.POST, "/api/todos")
84-
fun addTodo(context: Context, req: HttpRequest<CreateTodoRequest>): HttpResponse<CreateTodoResponse> {
85-
val todoReq = req.body
86-
val id = idCounter.incrementAndGet()
87-
todos.put(
88-
id, Todo(
89-
id = id,
90-
title = todoReq.title,
91-
)
70+
return if (todo != null) {
71+
HttpResponse(
72+
status = 200,
73+
headers = mapOf(),
74+
body = GetTodoResponse(todo)
9275
)
76+
} else {
77+
HttpResponse(status = 404, headers = mapOf(), error = "Not found")
78+
}
79+
}
9380

94-
return HttpResponse<CreateTodoResponse>(
95-
status = 201,
96-
headers = headers,
97-
body = CreateTodoResponse(id),
81+
@Verb
82+
@HttpIngress(Method.POST, "/api/todos")
83+
fun addTodo(context: Context, req: HttpRequest<CreateTodoRequest>): HttpResponse<CreateTodoResponse, String> {
84+
val todoReq = req.body
85+
val id = idCounter.incrementAndGet()
86+
todos.put(
87+
id, Todo(
88+
id = id,
89+
title = todoReq.title,
9890
)
99-
}
91+
)
92+
93+
return HttpResponse(
94+
status = 201,
95+
headers = headers,
96+
body = CreateTodoResponse(id),
97+
)
10098
}
99+
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
package ftl.echo
22

3-
import ftl.time.TimeModuleClient
4-
import ftl.time.TimeRequest
3+
import ftl.builtin.Empty
4+
import ftl.time.time
55
import xyz.block.ftl.Context
6-
import xyz.block.ftl.Method
76
import xyz.block.ftl.Verb
87

98
class InvalidInput(val field: String) : Exception()
109

1110
data class EchoRequest(val name: String?)
1211
data class EchoResponse(val message: String)
1312

14-
class Echo {
15-
@Throws(InvalidInput::class)
16-
@Verb
17-
fun echo(context: Context, req: EchoRequest): EchoResponse {
18-
val response = context.call(TimeModuleClient::time, TimeRequest)
19-
return EchoResponse(message = "Hello, ${req.name ?: "anonymous"}! The time is ${response.time}.")
20-
}
13+
@Throws(InvalidInput::class)
14+
@Verb
15+
fun echo(context: Context, req: EchoRequest): EchoResponse {
16+
val response = context.call(::time, Empty())
17+
return EchoResponse(message = "Hello, ${req.name ?: "anonymous"}! The time is ${response.time}.")
2118
}

integration/testdata/kotlin/database/Echo.kt

+12-15
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,25 @@ data class InsertRequest(val data: String)
99

1010
val db = Database("testdb")
1111

12-
class Echo {
13-
14-
@Verb
15-
fun insert(context: Context, req: InsertRequest): Empty {
16-
persistRequest(req)
17-
return Empty()
18-
}
12+
@Verb
13+
fun insert(context: Context, req: InsertRequest): Empty {
14+
persistRequest(req)
15+
return Empty()
16+
}
1917

20-
fun persistRequest(req: InsertRequest) {
21-
db.conn {
22-
it.prepareStatement(
23-
"""
18+
fun persistRequest(req: InsertRequest) {
19+
db.conn {
20+
it.prepareStatement(
21+
"""
2422
CREATE TABLE IF NOT EXISTS requests
2523
(
2624
data TEXT,
2725
created_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'),
2826
updated_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc')
2927
);
3028
"""
31-
).execute()
32-
it.prepareStatement("INSERT INTO requests (data) VALUES ('${req.data}');")
33-
.execute()
34-
}
29+
).execute()
30+
it.prepareStatement("INSERT INTO requests (data) VALUES ('${req.data}');")
31+
.execute()
3532
}
3633
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
package ftl.echo
22

3-
import ftl.echo2.Echo2ModuleClient
3+
import ftl.echo2.echo as echo2
44
import xyz.block.ftl.Context
55
import xyz.block.ftl.Verb
66

77
data class EchoRequest(val name: String)
88
data class EchoResponse(val message: String)
99

10-
class Echo {
11-
@Verb
12-
fun echo(context: Context, req: EchoRequest): EchoResponse {
13-
return EchoResponse(message = "Hello, ${req.name}!")
14-
}
10+
@Verb
11+
fun echo(context: Context, req: EchoRequest): EchoResponse {
12+
return EchoResponse(message = "Hello, ${req.name}!")
13+
}
1514

16-
@Verb
17-
fun call(context: Context, req: EchoRequest): EchoResponse {
18-
val res = context.call(Echo2ModuleClient::echo, ftl.echo2.EchoRequest(name = req.name))
19-
return EchoResponse(message = res.message)
20-
}
15+
@Verb
16+
fun call(context: Context, req: EchoRequest): EchoResponse {
17+
val res = context.call(::echo2, ftl.echo2.EchoRequest(name = req.name))
18+
return EchoResponse(message = res.message)
2119
}

0 commit comments

Comments
 (0)