Skip to content

Commit

Permalink
feat: redesign kotlin verb interface
Browse files Browse the repository at this point in the history
fixes #803

with this change, verbs in kotlin are defined as top-level functions rather than class member functions. this change also introduces the `Module` file annotation in Kotlin,
which will be used to declare an FTL module as the package of the annotated file.
  • Loading branch information
worstell committed Feb 10, 2024
1 parent 0fb4818 commit 44be7a6
Show file tree
Hide file tree
Showing 23 changed files with 681 additions and 659 deletions.
77 changes: 39 additions & 38 deletions examples/kotlin/ftl-module-ad/src/main/kotlin/ftl/ad/Ad.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Module

package ftl.ad

import com.google.gson.Gson
Expand All @@ -7,55 +9,54 @@ import ftl.builtin.HttpResponse
import xyz.block.ftl.Context
import xyz.block.ftl.HttpIngress
import xyz.block.ftl.Method
import xyz.block.ftl.Module
import xyz.block.ftl.Verb
import java.util.*

data class Ad(val redirectUrl: String, val text: String)
data class AdRequest(val contextKeys: List<String>? = null)
data class AdResponse(val ads: List<Ad>)

class AdModule {
private val database: Map<String, Ad> = loadDatabase()

@Verb
@HttpIngress(Method.GET, "/get")
fun get(context: Context, req: HttpRequest<AdRequest>): HttpResponse<AdResponse, String> {
val ads: List<Ad> = when {
req.body.contextKeys != null -> contextualAds(req.body.contextKeys)
else -> randomAds()
}
private val database: Map<String, Ad> = loadDatabase()

return HttpResponse(status = 200, headers = emptyMap(), body = AdResponse(ads = ads))
@Verb
@HttpIngress(Method.GET, "/get")
fun get(context: Context, req: HttpRequest<AdRequest>): HttpResponse<AdResponse, String> {
val ads: List<Ad> = when {
req.body.contextKeys != null -> contextualAds(req.body.contextKeys)
else -> randomAds()
}

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

private fun randomAds(): List<Ad> {
val ads = mutableListOf<Ad>()
val random = Random()
repeat(MAX_ADS_TO_SERVE) {
ads.add(database.entries.elementAt(random.nextInt(database.size)).value)
}
return ads
}
private fun contextualAds(contextKeys: List<String>): List<Ad> {
return contextKeys.map { database[it] ?: throw Exception("no ad registered for this context key") }
}

companion object {
private const val MAX_ADS_TO_SERVE = 2
private val DATABASE = mapOf(
"hair" to Ad("/product/2ZYFJ3GM2N", "Hairdryer for sale. 50% off."),
"clothing" to Ad("/product/66VCHSJNUP", "Tank top for sale. 20% off."),
"accessories" to Ad("/product/1YMWWN1N4O", "Watch for sale. Buy one, get second kit for free"),
"footwear" to Ad("/product/L9ECAV7KIM", "Loafers for sale. Buy one, get second one for free"),
"decor" to Ad("/product/0PUK6V6EV0", "Candle holder for sale. 30% off."),
"kitchen" to Ad("/product/9SIQT8TOJO", "Bamboo glass jar for sale. 10% off.")
)

private fun loadDatabase(): Map<String, Ad> {
return DATABASE
}

inline fun <reified T> Gson.fromJson(json: String) = fromJson<T>(json, object : TypeToken<T>() {}.type)
private fun randomAds(): List<Ad> {
val ads = mutableListOf<Ad>()
val random = Random()
repeat(MAX_ADS_TO_SERVE) {
ads.add(database.entries.elementAt(random.nextInt(database.size)).value)
}
return ads
}

private const val MAX_ADS_TO_SERVE = 2
private val DATABASE = mapOf(
"hair" to Ad("/product/2ZYFJ3GM2N", "Hairdryer for sale. 50% off."),
"clothing" to Ad("/product/66VCHSJNUP", "Tank top for sale. 20% off."),
"accessories" to Ad("/product/1YMWWN1N4O", "Watch for sale. Buy one, get second kit for free"),
"footwear" to Ad("/product/L9ECAV7KIM", "Loafers for sale. Buy one, get second one for free"),
"decor" to Ad("/product/0PUK6V6EV0", "Candle holder for sale. 30% off."),
"kitchen" to Ad("/product/9SIQT8TOJO", "Bamboo glass jar for sale. 10% off.")
)

private fun loadDatabase(): Map<String, Ad> {
return DATABASE
}

inline fun <reified T> Gson.fromJson(json: String) = fromJson<T>(json, object : TypeToken<T>() {}.type)


77 changes: 39 additions & 38 deletions examples/kotlin/ftl-module-api/src/main/kotlin/ftl/api/Api.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Module

package ftl.api

import com.google.gson.Gson
Expand Down Expand Up @@ -53,48 +55,47 @@ private val todos = ConcurrentHashMap<Int, Todo>()
private val idCounter = AtomicInteger()
private val gson = makeGson()

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

@Verb
@HttpIngress(Method.GET, "/api/status")
fun status(context: Context, req: HttpRequest<Empty>): HttpResponse<GetStatusResponse> {
return HttpResponse<GetStatusResponse>(status = 200, headers = mapOf(), body = GetStatusResponse("OK"))
}
@Verb
@HttpIngress(Method.GET, "/api/status")
fun status(context: Context, req: HttpRequest<Empty>): HttpResponse<GetStatusResponse, String> {
return HttpResponse(status = 200, headers = mapOf(), body = GetStatusResponse("OK"))
}

@Verb
@HttpIngress(Method.GET, "/api/todos/{id}")
fun getTodo(context: Context, req: HttpRequest<GetTodoRequest>): HttpResponse<GetTodoResponse> {
val todoId = req.pathParameters["id"]?.toIntOrNull()
val todo = todos[todoId]

return if (todo != null) {
HttpResponse<GetTodoResponse>(
status = 200,
headers = mapOf(),
body = GetTodoResponse(todo)
)
} else {
HttpResponse<GetTodoResponse>(status = 404, headers = mapOf(), body = GetTodoResponse(null))
}
}
@Verb
@HttpIngress(Method.GET, "/api/todos/{id}")
fun getTodo(context: Context, req: HttpRequest<GetTodoRequest>): HttpResponse<GetTodoResponse, String> {
val todoId = req.pathParameters["id"]?.toIntOrNull()
val todo = todos[todoId]

@Verb
@HttpIngress(Method.POST, "/api/todos")
fun addTodo(context: Context, req: HttpRequest<CreateTodoRequest>): HttpResponse<CreateTodoResponse> {
val todoReq = req.body
val id = idCounter.incrementAndGet()
todos.put(
id, Todo(
id = id,
title = todoReq.title,
)
return if (todo != null) {
HttpResponse(
status = 200,
headers = mapOf(),
body = GetTodoResponse(todo)
)
} else {
HttpResponse(status = 404, headers = mapOf(), error = "Not found")
}
}

return HttpResponse<CreateTodoResponse>(
status = 201,
headers = headers,
body = CreateTodoResponse(id),
@Verb
@HttpIngress(Method.POST, "/api/todos")
fun addTodo(context: Context, req: HttpRequest<CreateTodoRequest>): HttpResponse<CreateTodoResponse, String> {
val todoReq = req.body
val id = idCounter.incrementAndGet()
todos.put(
id, Todo(
id = id,
title = todoReq.title,
)
}
)

return HttpResponse(
status = 201,
headers = headers,
body = CreateTodoResponse(id),
)
}

19 changes: 10 additions & 9 deletions examples/kotlin/ftl-module-echo/src/main/kotlin/ftl/echo/Echo.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
@file:Module

package ftl.echo

import ftl.time.TimeModuleClient
import ftl.time.TimeRequest
import ftl.builtin.Empty
import ftl.time.time
import xyz.block.ftl.Context
import xyz.block.ftl.Method
import xyz.block.ftl.Module
import xyz.block.ftl.Verb

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

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

class Echo {
@Throws(InvalidInput::class)
@Verb
fun echo(context: Context, req: EchoRequest): EchoResponse {
val response = context.call(TimeModuleClient::time, TimeRequest)
return EchoResponse(message = "Hello, ${req.name ?: "anonymous"}! The time is ${response.time}.")
}
@Throws(InvalidInput::class)
@Verb
fun echo(context: Context, req: EchoRequest): EchoResponse {
val response = context.call(::time, Empty())
return EchoResponse(message = "Hello, ${req.name ?: "anonymous"}! The time is ${response.time}.")
}
30 changes: 15 additions & 15 deletions integration/testdata/kotlin/database/Echo.kt
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
@file:Module

package ftl.echo

import ftl.builtin.Empty
import xyz.block.ftl.Context
import xyz.block.ftl.Verb
import xyz.block.ftl.Database
import xyz.block.ftl.Module

data class InsertRequest(val data: String)

val db = Database("testdb")

class Echo {

@Verb
fun insert(context: Context, req: InsertRequest): Empty {
persistRequest(req)
return Empty()
}
@Verb
fun insert(context: Context, req: InsertRequest): Empty {
persistRequest(req)
return Empty()
}

fun persistRequest(req: InsertRequest) {
db.conn {
it.prepareStatement(
"""
fun persistRequest(req: InsertRequest) {
db.conn {
it.prepareStatement(
"""
CREATE TABLE IF NOT EXISTS requests
(
data TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'),
updated_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc')
);
"""
).execute()
it.prepareStatement("INSERT INTO requests (data) VALUES ('${req.data}');")
.execute()
}
).execute()
it.prepareStatement("INSERT INTO requests (data) VALUES ('${req.data}');")
.execute()
}
}
23 changes: 12 additions & 11 deletions integration/testdata/kotlin/externalcalls/Echo.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
@file:Module

package ftl.echo

import ftl.echo2.Echo2ModuleClient
import ftl.echo2.echo as echo2
import xyz.block.ftl.Context
import xyz.block.ftl.Module
import xyz.block.ftl.Verb

data class EchoRequest(val name: String)
data class EchoResponse(val message: String)

class Echo {
@Verb
fun echo(context: Context, req: EchoRequest): EchoResponse {
return EchoResponse(message = "Hello, ${req.name}!")
}
@Verb
fun echo(context: Context, req: EchoRequest): EchoResponse {
return EchoResponse(message = "Hello, ${req.name}!")
}

@Verb
fun call(context: Context, req: EchoRequest): EchoResponse {
val res = context.call(Echo2ModuleClient::echo, ftl.echo2.EchoRequest(name = req.name))
return EchoResponse(message = res.message)
}
@Verb
fun call(context: Context, req: EchoRequest): EchoResponse {
val res = context.call(::echo2, ftl.echo2.EchoRequest(name = req.name))
return EchoResponse(message = res.message)
}
Loading

0 comments on commit 44be7a6

Please sign in to comment.