diff --git a/docs/content/docs/getting-started/quick-start/index.md b/docs/content/docs/getting-started/quick-start/index.md
index a35ce52d72..89c0484b2d 100644
--- a/docs/content/docs/getting-started/quick-start/index.md
+++ b/docs/content/docs/getting-started/quick-start/index.md
@@ -66,10 +66,12 @@ This will create an `ftl-project.toml` file, a git repository, and a `bin/` dire
Now that you have an FTL project, create a new module:
+{% code_selector() %}
+
+
```
ftl new go . alice
```
-
This will place the code for the new module `alice` in `myproject/alice/alice.go`:
```go
@@ -98,6 +100,48 @@ func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) {
Each module is its own Go module.
+
+```
+ftl new kotlin . alice
+```
+
+This will create a new Maven `pom.xml` based project in the directory `alice` and create new example code in `alice/src/main/kotlin/com/example/EchoVerb.kt`:
+
+```kotlin
+package com.example
+
+import xyz.block.ftl.Export
+import xyz.block.ftl.Verb
+
+
+@Export
+@Verb
+fun echo(req: String): String = "Hello, $req!"
+```
+
+```
+ftl new java . alice
+```
+This will create a new Maven `pom.xml` based project in the directory `alice` and create new example code in `alice/src/main/java/com/example/EchoVerb.java`:
+
+```java
+package com.example;
+
+import xyz.block.ftl.Export;
+import xyz.block.ftl.Verb;
+
+public class EchoVerb {
+
+ @Export
+ @Verb
+ public String echo(String request) {
+ return "Hello, " + request + "!";
+ }
+}
+```
+{% end %}
+
+
Any number of modules can be added to your project, adjacent to each other.
### Start the FTL cluster
@@ -139,6 +183,9 @@ Or from a terminal use `ftl call` to call your verb:
Create another module and call `alice.echo` from it with by importing the `alice` module and adding the verb client,
`alice.EchoClient`, to the signature of the calling verb. It can be invoked as a function:
+{% code_selector() %}
+
+
```go
//ftl:verb
import "ftl/alice"
@@ -149,6 +196,44 @@ func Other(ctx context.Context, in Request, ec alice.EchoClient) (Response, erro
...
}
```
+
+```kotlin
+package com.example
+
+import xyz.block.ftl.Export
+import xyz.block.ftl.Verb
+import ftl.alice.EchoClient
+
+
+@Export
+@Verb
+fun other(req: String, echo: EchoClient): String = "Hello from Other , ${echo.call(req)}!"
+```
+Note that the `EchoClient` is generated by FTL and must be imported. Unfortunately at the moment JVM based languages have
+a bit of a chicken-and-egg problem with the generated clients. To force a dependency between the modules you need to add
+an import on a class that does not exist yet, and then FTL will generate the client for you. This will be fixed in the future.
+
+```java
+package com.example.client;
+
+import xyz.block.ftl.Export;
+import xyz.block.ftl.Verb;
+import ftl.alice.EchoClient;
+
+public class OtherVerb {
+
+ @Export
+ @Verb
+ public String other(String request, EchoClient echoClient) {
+ return "Hello, " + echoClient.call(request) + "!";
+ }
+}
+```
+Note that the `EchoClient` is generated by FTL and must be imported. Unfortunately at the moment JVM based languages have
+a bit of a chicken-and-egg problem with the generated clients. To force a dependency between the modules you need to add
+an import on a class that does not exist yet, and then FTL will generate the client for you. This will be fixed in the future.
+
+{% end %}
### What next?
diff --git a/docs/content/docs/reference/cron.md b/docs/content/docs/reference/cron.md
index 4ff99588a7..962b9ca44b 100644
--- a/docs/content/docs/reference/cron.md
+++ b/docs/content/docs/reference/cron.md
@@ -21,27 +21,93 @@ You can also use a shorthand syntax for the cron job, supporting seconds (`s`),
The following function will be called hourly:
+{% code_selector() %}
+
```go
//ftl:cron 0 * * * *
func Hourly(ctx context.Context) error {
// ...
}
```
+
+```kotlin
+import xyz.block.ftl.Cron
+@Cron("0 * * * *")
+fun hourly() {
+
+}
+```
+
+```java
+import xyz.block.ftl.Cron;
+
+class MyCron {
+ @Cron("0 * * * *")
+ void hourly() {
+
+ }
+}
+```
+{% end %}
Every 12 hours, starting at UTC midnight:
+{% code_selector() %}
+
```go
//ftl:cron 12h
func TwiceADay(ctx context.Context) error {
// ...
}
```
+
+```kotlin
+import xyz.block.ftl.Cron
+@Cron("12h")
+fun twiceADay() {
+
+}
+```
+
+```java
+import xyz.block.ftl.Cron;
+
+class MyCron {
+ @Cron("12h")
+ void twiceADay() {
+
+ }
+}
+```
+{% end %}
Every Monday at UTC midnight:
+{% code_selector() %}
+
```go
//ftl:cron Mon
func Mondays(ctx context.Context) error {
// ...
}
```
+
+```kotlin
+import xyz.block.ftl.Cron
+@Cron("Mon")
+fun mondays() {
+
+}
+```
+
+```java
+import xyz.block.ftl.Cron;
+
+class MyCron {
+ @Cron("Mon")
+ void mondays() {
+
+ }
+}
+```
+{% end %}
\ No newline at end of file
diff --git a/docs/content/docs/reference/ingress.md b/docs/content/docs/reference/ingress.md
index 2d4557fdb8..353402f2dc 100644
--- a/docs/content/docs/reference/ingress.md
+++ b/docs/content/docs/reference/ingress.md
@@ -17,6 +17,9 @@ Verbs annotated with `ftl:ingress` will be exposed via HTTP (`http` is the defau
The following will be available at `http://localhost:8891/http/users/123/posts?postId=456`.
+{% code_selector() %}
+
+
```go
type GetRequestPathParams struct {
UserID string `json:"userId"`
@@ -164,3 +167,40 @@ Complex query params can also be encoded as JSON using the `@json` query paramet
```bash
curl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D
```
+
+
+
+
+JVM Languages use the `JAX-RS` annotations to define HTTP endpoints. The following example shows how to define an HTTP endpoint in Java. As the underling implementation is based on [Quarkus](https://quarkus.io)
+it is also possible to use the [Quarkus extensions to the JAX-RS annotations](https://quarkus.io/guides/rest#accessing-request-parameters):
+
+In general the difference between the Quarkus annotation and the standard JAX-RS ones is that the Quarkus parameters infer the parameter name from the method parameter name, while the JAX-RS ones require the parameter name to be explicitly defined.
+
+```java
+
+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.QueryParam; // JAX-RS annotation to get the query parameter
+import org.jboss.resteasy.reactive.RestPath; // Quarkus annotation to get the path parameter
+
+@Path("/")
+public class TestHTTP {
+
+ @GET
+ @Path("/http/users/{userId}/posts")
+ public String get(@RestPath String userId, @QueryParam("postId") String post) {
+ //...
+ }
+
+}
+```
+Under the hood these HTTP invocations are being mapped to verbs that take a `builtin.HttpRequest` and return a `builtin.HttpResponse`. This is not exposed directly to the user, but is instead mapped directly to `JAX-RS` annotations.
+
+{% end %}
+
diff --git a/docs/content/docs/reference/start.md b/docs/content/docs/reference/start.md
index 48e2a176ba..5a4cf438dc 100644
--- a/docs/content/docs/reference/start.md
+++ b/docs/content/docs/reference/start.md
@@ -17,22 +17,58 @@ top = false
{% code_selector() %}
-Some aspects of FTL rely on a runtime which must be imported with:
-
+Some aspects of FTL rely on a runtime which must be imported with:
+
```go
import "github.com/TBD54566975/ftl/go-runtime/ftl"
```
-```kotlin
-// Declare a verb you want to use:
-import xyz.block.ftl.Verb
+The easiest way to get started with Kotlin is to use the `ftl-build-parent-kotlin` parent POM. This will automatically include the FTL runtime as a dependency, and setup all required plugins:
+
+```xml
+
+
+ 4.0.0
+ com.myproject
+ myproject
+ 1.0-SNAPSHOT
+
+
+ xyz.block.ftl
+ ftl-build-parent-kotlin
+ ${ftl.version}
+
+
+
+```
+
+If you do not want to use a parent pom then you can copy the plugins and dependencies from the parent pom into your own pom.
+
+
+
+The easiest way to get started with Java is to use the `ftl-build-parent-java` parent POM. This will automatically include the FTL runtime as a dependency, and setup all required plugins:
-// When using the export feature:
-import xyz.block.ftl.Export
+```xml
+
+
+ 4.0.0
+ com.myproject
+ myproject
+ 1.0-SNAPSHOT
+
+
+ xyz.block.ftl
+ ftl-build-parent-java
+ ${ftl.version}
+
+
+
```
+If you do not want to use a parent pom then you can copy the plugins and dependencies from the parent pom into your own pom.
+
{% end %}
diff --git a/docs/content/docs/reference/visibility.md b/docs/content/docs/reference/visibility.md
index ddb0a05cdb..d09975728d 100644
--- a/docs/content/docs/reference/visibility.md
+++ b/docs/content/docs/reference/visibility.md
@@ -21,7 +21,7 @@ Exporting a declaration makes it accessible to other modules. Some declarations
Types that are transitively referenced by an exported declaration will be automatically exported unless they were already defined but unexported. In this case, an error will be raised and the type must be explicitly exported.
-The following table describes the directives used to export the corresponding declaration:
+The following table describes the go directives used to export the corresponding declaration:
| Symbol | Export syntax |
| ------------- | ------------------------ |
@@ -31,8 +31,13 @@ The following table describes the directives used to export the corresponding de
| Typealias | `//ftl:typealias export` |
| Topic | `//ftl:export` [^1] |
+For JVM languages the `@Export` annotation can be used to export a declaration.
+
eg.
+{% code_selector() %}
+
+
```go
//ftl:verb export
func Verb(ctx context.Context, in In) (Out, error)
@@ -40,5 +45,22 @@ func Verb(ctx context.Context, in In) (Out, error)
//ftl:typealias export
type UserID string
```
+
+```kotlin
+@Verb
+@Export
+fun time(): TimeResponse {
+ // ...
+}
+```
+
+```java
+@Verb
+@Export
+TimeResponse time() {
+ // ...
+}
+```
+{% end %}
[^1]: By default, topics do not require any annotations as the declaration itself is sufficient.
diff --git a/frontend/cli/cmd_init.go b/frontend/cli/cmd_init.go
index 69ca63f1ed..427b5564a2 100644
--- a/frontend/cli/cmd_init.go
+++ b/frontend/cli/cmd_init.go
@@ -69,6 +69,10 @@ func (i initCmd) Run(
}
if !i.NoGit {
+ err := maybeGitInit(ctx, i.Dir)
+ if err != nil {
+ return fmt.Errorf("running git init: %w", err)
+ }
logger.Debugf("Updating .gitignore")
if err := updateGitIgnore(ctx, i.Dir); err != nil {
return fmt.Errorf("update .gitignore: %w", err)
@@ -88,6 +92,14 @@ func maybeGitAdd(ctx context.Context, dir string, paths ...string) error {
return nil
}
+func maybeGitInit(ctx context.Context, dir string) error {
+ args := []string{"init"}
+ if err := exec.Command(ctx, log.Debug, dir, "git", args...).RunBuffered(ctx); err != nil {
+ return fmt.Errorf("git init: %w", err)
+ }
+ return nil
+}
+
func updateGitIgnore(ctx context.Context, gitRoot string) error {
f, err := os.OpenFile(path.Join(gitRoot, ".gitignore"), os.O_RDWR|os.O_CREATE, 0644) //nolint:gosec
if err != nil {
diff --git a/internal/lsp/hoveritems.go b/internal/lsp/hoveritems.go
index 6ff560a0cf..7ffc48fd47 100644
--- a/internal/lsp/hoveritems.go
+++ b/internal/lsp/hoveritems.go
@@ -2,9 +2,9 @@
package lsp
var hoverMap = map[string]string{
- "//ftl:cron": "## Cron\n\nA cron job is an Empty verb that will be called on a schedule. The syntax is described [here](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/crontab.html).\n\nYou can also use a shorthand syntax for the cron job, supporting seconds (`s`), minutes (`m`), hours (`h`), and specific days of the week (e.g. `Mon`).\n\n### Examples\n\nThe following function will be called hourly:\n\n```go\n//ftl:cron 0 * * * *\nfunc Hourly(ctx context.Context) error {\n // ...\n}\n```\n\nEvery 12 hours, starting at UTC midnight:\n\n```go\n//ftl:cron 12h\nfunc TwiceADay(ctx context.Context) error {\n // ...\n}\n```\n\nEvery Monday at UTC midnight:\n\n```go\n//ftl:cron Mon\nfunc Mondays(ctx context.Context) error {\n // ...\n}\n```\n",
+ "//ftl:cron": "## Cron\n\nA cron job is an Empty verb that will be called on a schedule. The syntax is described [here](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/crontab.html).\n\nYou can also use a shorthand syntax for the cron job, supporting seconds (`s`), minutes (`m`), hours (`h`), and specific days of the week (e.g. `Mon`).\n\n### Examples\n\nThe following function will be called hourly:\n\n```go\n//ftl:cron 0 * * * *\nfunc Hourly(ctx context.Context) error {\n // ...\n}\n```\nEvery 12 hours, starting at UTC midnight:\n\n```go\n//ftl:cron 12h\nfunc TwiceADay(ctx context.Context) error {\n // ...\n}\n```\n\nEvery Monday at UTC midnight:\n\n```go\n//ftl:cron Mon\nfunc Mondays(ctx context.Context) error {\n // ...\n}\n```",
"//ftl:enum": "## Type enums (sum types)\n\n[Sum types](https://en.wikipedia.org/wiki/Tagged_union) are supported by FTL's type system, but aren't directly supported by Go. However they can be approximated with the use of [sealed interfaces](https://blog.chewxy.com/2018/03/18/golang-interfaces/). To declare a sum type in FTL use the comment directive `//ftl:enum`:\n\n```go\n//ftl:enum\ntype Animal interface { animal() }\n\ntype Cat struct {}\nfunc (Cat) animal() {}\n\ntype Dog struct {}\nfunc (Dog) animal() {}\n```\n## Value enums\n\nA value enum is an enumerated set of string or integer values.\n\n```go\n//ftl:enum\ntype Colour string\n\nconst (\n Red Colour = \"red\"\n Green Colour = \"green\"\n Blue Colour = \"blue\"\n)\n```\n",
- "//ftl:ingress": "## HTTP Ingress\n\nVerbs annotated with `ftl:ingress` will be exposed via HTTP (`http` is the default ingress type). These endpoints will then be available on one of our default `ingress` ports (local development defaults to `http://localhost:8891`).\n\nThe following will be available at `http://localhost:8891/http/users/123/posts?postId=456`.\n\n```go\ntype GetRequestPathParams struct {\n\tUserID string `json:\"userId\"`\n}\n\ntype GetRequestQueryParams struct {\n\tPostID string `json:\"postId\"`\n}\n\ntype GetResponse struct {\n\tMessage string `json:\"msg\"`\n}\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, GetRequestPathParams, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\nBecause the example above only has a single path parameter it can be simplified by just using a scalar such as `string` or `int64` as the path parameter type:\n\n```go\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, int64, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\n> **NOTE!**\n> The `req` and `resp` types of HTTP `ingress` [verbs](../verbs) must be `builtin.HttpRequest` and `builtin.HttpResponse` respectively. These types provide the necessary fields for HTTP `ingress` (`headers`, `statusCode`, etc.)\n>\n> You will need to import `ftl/builtin`.\n\nKey points:\n\n- `ingress` verbs will be automatically exported by default.\n\n## Field mapping\n\nThe `HttpRequest` request object takes 3 type parameters, the body, the path parameters and the query parameters.\n\nGiven the following request verb:\n\n```go\n\ntype PostBody struct{\n\tTitle string `json:\"title\"`\n\tContent string `json:\"content\"`\n\tTag ftl.Option[string] `json:\"tag\"`\n}\ntype PostPathParams struct {\n\tUserID string `json:\"userId\"`\n\tPostID string `json:\"postId\"`\n}\n\ntype PostQueryParams struct {\n\tPublish boolean `json:\"publish\"`\n}\n\n//ftl:ingress http PUT /users/{userId}/posts/{postId}\nfunc Get(ctx context.Context, req builtin.HttpRequest[PostBody, PostPathParams, PostQueryParams]) (builtin.HttpResponse[GetResponse, string], error) {\n\treturn builtin.HttpResponse[GetResponse, string]{\n\t\tHeaders: map[string][]string{\"Get\": {\"Header from FTL\"}},\n\t\tBody: ftl.Some(GetResponse{\n\t\t\tMessage: fmt.Sprintf(\"UserID: %s, PostID: %s, Tag: %s\", req.pathParameters.UserID, req.pathParameters.PostID, req.Body.Tag.Default(\"none\")),\n\t\t}),\n\t}, nil\n}\n```\n\nThe rules for how each element is mapped are slightly different, as they have a different structure:\n\n- The body is mapped directly to the body of the request, generally as a JSON object. Scalars are also supported, as well as []byte to get the raw body. If they type is `any` then it will be assumed to be JSON and mapped to the appropriate types based on the JSON structure.\n- The path parameters can be mapped directly to an object with field names corresponding to the name of the path parameter. If there is only a single path parameter it can be injected directly as a scalar. They can also be injected as a `map[string]string`.\n- The path parameters can also be mapped directly to an object with field names corresponding to the name of the path parameter. They can also be injected directly as a `map[string]string`, or `map[string][]string` for multiple values.\n\n#### Optional fields\n\nOptional fields are represented by the `ftl.Option` type. The `Option` type is a wrapper around the actual type and can be `Some` or `None`. In the example above, the `Tag` field is optional.\n\n```sh\ncurl -i http://localhost:8891/users/123/posts/456\n```\n\nBecause the `tag` query parameter is not provided, the response will be:\n\n```json\n{\n \"msg\": \"UserID: 123, PostID: 456, Tag: none\"\n}\n```\n\n#### Casing\n\nField names use lowerCamelCase by default. You can override this by using the `json` tag.\n\n## SumTypes\n\nGiven the following request verb:\n\n```go\n//ftl:enum export\ntype SumType interface {\n\ttag()\n}\n\ntype A string\n\nfunc (A) tag() {}\n\ntype B []string\n\nfunc (B) tag() {}\n\n//ftl:ingress http POST /typeenum\nfunc TypeEnum(ctx context.Context, req builtin.HttpRequest[SumType, ftl.Unit, ftl.Unit]) (builtin.HttpResponse[SumType, string], error) {\n\treturn builtin.HttpResponse[SumType, string]{Body: ftl.Some(req.Body)}, nil\n}\n```\n\nThe following curl request will map the `SumType` name and value to the `req.Body`:\n\n```sh\ncurl -X POST \"http://localhost:8891/typeenum\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"name\": \"A\", \"value\": \"sample\"}'\n```\n\nThe response will be:\n\n```json\n{\n \"name\": \"A\",\n \"value\": \"sample\"\n}\n```\n\n## Encoding query params as JSON\n\nComplex query params can also be encoded as JSON using the `@json` query parameter. For example:\n\n> `{\"tag\":\"ftl\"}` url-encoded is `%7B%22tag%22%3A%22ftl%22%7D`\n\n```bash\ncurl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D\n```\n",
+ "//ftl:ingress": "## HTTP Ingress\n\nVerbs annotated with `ftl:ingress` will be exposed via HTTP (`http` is the default ingress type). These endpoints will then be available on one of our default `ingress` ports (local development defaults to `http://localhost:8891`).\n\nThe following will be available at `http://localhost:8891/http/users/123/posts?postId=456`.\n\n\n```go\ntype GetRequestPathParams struct {\n\tUserID string `json:\"userId\"`\n}\n\ntype GetRequestQueryParams struct {\n\tPostID string `json:\"postId\"`\n}\n\ntype GetResponse struct {\n\tMessage string `json:\"msg\"`\n}\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, GetRequestPathParams, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\nBecause the example above only has a single path parameter it can be simplified by just using a scalar such as `string` or `int64` as the path parameter type:\n\n```go\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, int64, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\n> **NOTE!**\n> The `req` and `resp` types of HTTP `ingress` [verbs](../verbs) must be `builtin.HttpRequest` and `builtin.HttpResponse` respectively. These types provide the necessary fields for HTTP `ingress` (`headers`, `statusCode`, etc.)\n>\n> You will need to import `ftl/builtin`.\n\nKey points:\n\n- `ingress` verbs will be automatically exported by default.\n\n## Field mapping\n\nThe `HttpRequest` request object takes 3 type parameters, the body, the path parameters and the query parameters.\n\nGiven the following request verb:\n\n```go\n\ntype PostBody struct{\n\tTitle string `json:\"title\"`\n\tContent string `json:\"content\"`\n\tTag ftl.Option[string] `json:\"tag\"`\n}\ntype PostPathParams struct {\n\tUserID string `json:\"userId\"`\n\tPostID string `json:\"postId\"`\n}\n\ntype PostQueryParams struct {\n\tPublish boolean `json:\"publish\"`\n}\n\n//ftl:ingress http PUT /users/{userId}/posts/{postId}\nfunc Get(ctx context.Context, req builtin.HttpRequest[PostBody, PostPathParams, PostQueryParams]) (builtin.HttpResponse[GetResponse, string], error) {\n\treturn builtin.HttpResponse[GetResponse, string]{\n\t\tHeaders: map[string][]string{\"Get\": {\"Header from FTL\"}},\n\t\tBody: ftl.Some(GetResponse{\n\t\t\tMessage: fmt.Sprintf(\"UserID: %s, PostID: %s, Tag: %s\", req.pathParameters.UserID, req.pathParameters.PostID, req.Body.Tag.Default(\"none\")),\n\t\t}),\n\t}, nil\n}\n```\n\nThe rules for how each element is mapped are slightly different, as they have a different structure:\n\n- The body is mapped directly to the body of the request, generally as a JSON object. Scalars are also supported, as well as []byte to get the raw body. If they type is `any` then it will be assumed to be JSON and mapped to the appropriate types based on the JSON structure.\n- The path parameters can be mapped directly to an object with field names corresponding to the name of the path parameter. If there is only a single path parameter it can be injected directly as a scalar. They can also be injected as a `map[string]string`.\n- The path parameters can also be mapped directly to an object with field names corresponding to the name of the path parameter. They can also be injected directly as a `map[string]string`, or `map[string][]string` for multiple values.\n\n#### Optional fields\n\nOptional fields are represented by the `ftl.Option` type. The `Option` type is a wrapper around the actual type and can be `Some` or `None`. In the example above, the `Tag` field is optional.\n\n```sh\ncurl -i http://localhost:8891/users/123/posts/456\n```\n\nBecause the `tag` query parameter is not provided, the response will be:\n\n```json\n{\n \"msg\": \"UserID: 123, PostID: 456, Tag: none\"\n}\n```\n\n#### Casing\n\nField names use lowerCamelCase by default. You can override this by using the `json` tag.\n\n## SumTypes\n\nGiven the following request verb:\n\n```go\n//ftl:enum export\ntype SumType interface {\n\ttag()\n}\n\ntype A string\n\nfunc (A) tag() {}\n\ntype B []string\n\nfunc (B) tag() {}\n\n//ftl:ingress http POST /typeenum\nfunc TypeEnum(ctx context.Context, req builtin.HttpRequest[SumType, ftl.Unit, ftl.Unit]) (builtin.HttpResponse[SumType, string], error) {\n\treturn builtin.HttpResponse[SumType, string]{Body: ftl.Some(req.Body)}, nil\n}\n```\n\nThe following curl request will map the `SumType` name and value to the `req.Body`:\n\n```sh\ncurl -X POST \"http://localhost:8891/typeenum\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"name\": \"A\", \"value\": \"sample\"}'\n```\n\nThe response will be:\n\n```json\n{\n \"name\": \"A\",\n \"value\": \"sample\"\n}\n```\n\n## Encoding query params as JSON\n\nComplex query params can also be encoded as JSON using the `@json` query parameter. For example:\n\n> `{\"tag\":\"ftl\"}` url-encoded is `%7B%22tag%22%3A%22ftl%22%7D`\n\n```bash\ncurl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D\n```\n\n\n\n",
"//ftl:retry": "## Retries\n\nSome FTL features allow specifying a retry policy via a Go comment directive. Retries back off exponentially until the maximum is reached.\n\nThe directive has the following syntax:\n\n```go\n//ftl:retry [] [] [catch ]\n```\n\nFor example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.\n\n```go\n//ftl:retry 10 5s 1m\nfunc Process(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\n### PubSub\n\nSubscribers can have a retry policy. For example:\n```go\n//ftl:subscribe exampleSubscription\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n```\n\n### FSM\n\nRetries can be declared on the FSM or on individual transition verbs. Retries declared on a verb take precedence over ones declared on the FSM. For example:\n```go\n//ftl:retry 10 1s 10s\nvar fsm = ftl.FSM(\"fsm\",\n\tftl.Start(Start),\n\tftl.Transition(Start, End),\n)\n\n//ftl:verb\n//ftl:retry 1 1s 1s\nfunc Start(ctx context.Context, in Event) error {\n\t// Start uses its own retry policy\n}\n\n\n//ftl:verb\nfunc End(ctx context.Context, in Event) error {\n\t// End inherits the default retry policy from the FSM\n}\n```\n\n\n## Catching\nAfter all retries have failed, a catch verb can be used to safely recover.\n\nThese catch verbs have a request type of `builtin.CatchRequest` and no response type. If a catch verb returns an error, it will be retried until it succeeds so it is important to handle errors carefully.\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n\n//ftl:verb\nfunc RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error {\n // safely handle final failure of the payment\n}\n```\n\nFor FSMs, after a catch verb has been successfully called the FSM will moved to the failed state.",
"//ftl:subscribe": "## PubSub\n\nFTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent), subscriptions (a cursor over the topic), and subscribers (functions events are delivered to). Subscribers are, as you would expect, sinks. Each subscription is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each subscription may have multiple subscribers, in which case events will be distributed among them.\n\nFirst, declare a new topic:\n\n```go\nvar Invoices = ftl.Topic[Invoice](\"invoices\")\n```\n\nThen declare each subscription on the topic:\n\n```go\nvar _ = ftl.Subscription(Invoices, \"emailInvoices\")\n```\n\nAnd finally define a Sink to consume from the subscription:\n\n```go\n//ftl:subscribe emailInvoices\nfunc SendInvoiceEmail(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\nEvents can be published to a topic like so:\n\n```go\nInvoices.Publish(ctx, Invoice{...})\n```\n\n> **NOTE!**\n> PubSub topics cannot be published to from outside the module that declared them, they can only be subscribed to. That is, if a topic is declared in module `A`, module `B` cannot publish to it.\n",
"//ftl:typealias": "## Type aliases\n\nA type alias is an alternate name for an existing type. It can be declared like so:\n\n```go\n//ftl:typealias\ntype Alias Target\n```\nor\n```go\n//ftl:typealias\ntype Alias = Target\n```\n\neg.\n\n```go\n//ftl:typealias\ntype UserID string\n\n//ftl:typealias\ntype UserToken = string\n```\n",
diff --git a/jvm-runtime/testdata/kotlin/kotlinmodule/pom.xml b/jvm-runtime/testdata/kotlin/kotlinmodule/pom.xml
index 38115b7723..d82a108a01 100644
--- a/jvm-runtime/testdata/kotlin/kotlinmodule/pom.xml
+++ b/jvm-runtime/testdata/kotlin/kotlinmodule/pom.xml
@@ -19,26 +19,4 @@
-
-
-
- org.codehaus.mojo
- build-helper-maven-plugin
- 3.6.0
-
-
- generate-sources
-
- add-source
-
-
-
-
-
-
-
-
-
-
-