diff --git a/docs/content/docs/reference/ingress.md b/docs/content/docs/reference/ingress.md index 353402f2d..1c1bf24e1 100644 --- a/docs/content/docs/reference/ingress.md +++ b/docs/content/docs/reference/ingress.md @@ -168,6 +168,50 @@ Complex query params can also be encoded as JSON using the `@json` query paramet curl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D ``` + +Kotlin uses the `@Ingress` annotation to define HTTP endpoints. These endpoints will be exposed on the default ingress port (local development defaults to `http://localhost:8891`). + +```kotlin +import xyz.block.ftl.Ingress +import xyz.block.ftl.Option + +// Simple GET endpoint with path and query parameters +@Ingress("GET /users/{userId}/posts") +fun getPost(request: Request): Response { + val userId = request.pathParams["userId"] + val postId = request.queryParams["postId"] + return Response.ok(Post(userId, postId)) +} + +// POST endpoint with request body +@Ingress("POST /users/{userId}/posts") +fun createPost(request: Request): Response { + val userId = request.pathParams["userId"] + val body = request.body() + return Response.created(Post(userId, body.title)) +} + +// Request body data class +data class PostBody( + val title: String, + val content: String, + val tag: Option // Optional field using Option type +) + +// Response data class +data class Post( + val userId: String, + val title: String +) +``` + +Key features: +- The `@Ingress` annotation takes a string parameter combining the HTTP method and path +- Path parameters are accessed via `request.pathParams` +- Query parameters are accessed via `request.queryParams` +- Request bodies can be automatically deserialized using `request.body()` +- Optional fields are represented using the `Option` type +- Response helpers like `Response.ok()` and `Response.created()` for common status codes diff --git a/docs/content/docs/reference/secretsconfig.md b/docs/content/docs/reference/secretsconfig.md index 7354056bd..49cde9fbf 100644 --- a/docs/content/docs/reference/secretsconfig.md +++ b/docs/content/docs/reference/secretsconfig.md @@ -21,16 +21,19 @@ Configuration values are named, typed values. They are managed by the `ftl confi - To declare a configuration value use the following syntax: ```go +// Simple string configuration +type ApiUrl = ftl.Config[string] + +// Type-safe configuration type DefaultUser = ftl.Config[Username] ``` -Note that the name of the configuration value as represented in the FTL schema is the lower camel case version of the type name. +Note that the name of the configuration value as represented in the FTL schema is the lower camel case version of the type name (e.g., `ApiUrl` becomes `apiUrl`). -Configuration values can be injected into FTL methods, such as `@Verb`, HTTP ingress, Cron etc. To inject a configuration value, use the following syntax: +Configuration values can be injected into FTL methods, such as //ftl:verb, HTTP ingress, Cron etc. To inject a configuration value, use the following syntax: ```go //ftl:verb @@ -76,12 +79,16 @@ Secrets are encrypted, named, typed values. They are managed by the `ftl secret` Declare a secret with the following: ```go +// Simple string secret +type ApiToken = ftl.Secret[string] + +// Type-safe secret type ApiKey = ftl.Secret[Credentials] ``` -Like configuration values, the name of the secret as represented in the FTL schema is the lower camel case version of the type name. +Like configuration values, the name of the secret as represented in the FTL schema is the lower camel case version of the type name (e.g., `ApiToken` becomes `apiToken`). -Configuration values can be injected into FTL methods, such as `@Verb`, HTTP ingress, Cron etc. To inject a configuration value, use the following syntax: +Secrets can be injected into FTL methods, such as //ftl:verb, HTTP ingress, Cron etc. To inject a secret value, use the following syntax: ```go //ftl:verb @@ -93,7 +100,7 @@ func CallApi(ctx context.Context, req Request, apiKey ApiKey) error { -Configuration values can be injected into FTL methods, such as `@Verb`, HTTP ingress, Cron etc. To inject a configuration value, use the following syntax: +Secrets can be injected into FTL methods, such as `@Verb`, HTTP ingress, Cron etc. To inject a secret value, use the following syntax: ```kotlin @Export @@ -103,7 +110,7 @@ fun hello(helloRequest: HelloRequest, @Secret("apiKey") apiKey: String): HelloRe } ``` -Configuration values can be injected into FTL methods, such as `@Verb`, HTTP ingress, Cron etc. To inject a configuration value, use the following syntax: +Secrets can be injected into FTL methods, such as `@Verb`, HTTP ingress, Cron etc. To inject a secret value, use the following syntax: ```java @Export diff --git a/docs/content/docs/reference/verbs.md b/docs/content/docs/reference/verbs.md index ecf250097..01d8b1dcf 100644 --- a/docs/content/docs/reference/verbs.md +++ b/docs/content/docs/reference/verbs.md @@ -58,6 +58,27 @@ fun echo(request: EchoRequest): EchoResponse { } ``` + +To declare a Verb, write a normal Java method with the following signature, annotated with the `@Verb` annotation: + +```java +@Verb +public Output f(Input input) { } +``` + +eg. + +```java +public class EchoRequest {} + +public class EchoResponse {} + +@Verb +public EchoResponse echo(EchoRequest request) { + // ... +} +``` + {% end %} By default verbs are only [visible](../visibility) to other verbs in the same module. @@ -103,4 +124,27 @@ interface TimeClient { fun call(): TimeResponse } ``` + + +To call a verb, import the module's verb client, add it to your verb's signature, then call it. eg. + +```java +import ftl.time.TimeClient; + +@Verb +public EchoResponse echo(EchoRequest request, TimeClient time) { + TimeResponse response = time.call(); + // ... +} +``` + +Verb clients are generated by FTL. If the callee verb belongs to the same module as the caller, you must manually define your +own client: + +```java +@VerbClient(name="time") +public interface TimeClient { + TimeResponse call(); +} +``` {% end %} diff --git a/frontend/vscode/src/client.ts b/frontend/vscode/src/client.ts index 05250ca76..b673850c9 100644 --- a/frontend/vscode/src/client.ts +++ b/frontend/vscode/src/client.ts @@ -43,6 +43,7 @@ export class FTLClient { documentSelector: [ { scheme: 'file', language: 'kotlin' }, { scheme: 'file', language: 'go' }, + { scheme: 'file', language: 'java' }, ], outputChannel: this.outputChannel, } diff --git a/internal/lsp/hoveritems.go b/internal/lsp/hoveritems.go index 6c1468227..e8f9b38cf 100644 --- a/internal/lsp/hoveritems.go +++ b/internal/lsp/hoveritems.go @@ -4,7 +4,7 @@ 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```\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\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: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", "//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\n```go\n//ftl:retry [] [] [catch ]\n```\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\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\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n...\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\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", "//ftl:subscribe": "## PubSub\n\nFTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent) and subscribers (a verb which consumes events). Subscribers are, as you would expect, sinks. Each subscriber is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each published event has an at least once delivery guarantee for each subscription.\n\nA topic can be exported to allow other modules to subscribe to it. Subscriptions are always private to their module.\n\nWhen a subscription is first created in an environment, it can start consuming from the beginning of the topic or only consume events published afterwards.\n\nTopics allow configuring the number of partitions and how each event should be mapped to a partition, allowing for greater throughput. Subscriptions will consume in order within each partition. There are cases where a small amount of progress on a subscription will be lost, so subscriptions should be able to handle receiving some events that have already been consumed.\n\n\nFirst, declare a new topic:\n\n```go\npackage payments\n\nimport (\n \"github.com/block/ftl/go-runtime/ftl\"\n)\n\n// Define an event type\ntype Invoice struct {\n InvoiceNo string\n}\n\n//ftl:topic partitions=1\ntype Invoices = ftl.TopicHandle[Invoice, ftl.SinglePartitionMap[Invoice]]\n```\n\nIf you want multiple partitions in the topic, you'll also need to write a partition mapper:\n\n```go\npackage payments\n\nimport (\n \"github.com/block/ftl/go-runtime/ftl\"\n)\n\n// Define an event type\ntype Invoice struct {\n InvoiceNo string\n}\n\ntype PartitionMapper struct{}\n\nvar _ ftl.TopicPartitionMap[PubSubEvent] = PartitionMapper{}\n\nfunc (PartitionMapper) PartitionKey(event PubSubEvent) string {\n\treturn event.Time.String()\n}\n\n//ftl:topic partitions=10\ntype Invoices = ftl.TopicHandle[Invoice, PartitionMapper]\n```\n\nNote that the name of the topic as represented in the FTL schema is the lower camel case version of the type name.\n\nThe `Invoices` type is a handle to the topic. It is a generic type that takes two arguments: the event type and the partition map type. The partition map type is used to map events to partitions.\n\nThen define a Sink to consume from the topic:\n\n```go\n// Configure initial event consumption with either from=beginning or from=latest\n//\n//ftl:subscribe payments.invoices from=beginning\nfunc SendInvoiceEmail(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\nEvents can be published to a topic by injecting the topic type into a verb:\n\n```go\n//ftl:verb\nfunc PublishInvoice(ctx context.Context, topic Invoices) error {\n topic.Publish(ctx, Invoice{...})\n // ...\n}\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/internal/lsp/markdown/completion/go/retry.md b/internal/lsp/markdown/completion/go/retry.md index 325ab5020..1e6770c39 100644 --- a/internal/lsp/markdown/completion/go/retry.md +++ b/internal/lsp/markdown/completion/go/retry.md @@ -1,20 +1,42 @@ Directive for retrying an async operation. -Any verb called asynchronously (specifically, PubSub subscribers and cron jobs) may specify a basic exponential backoff retry policy. +Any verb called asynchronously (specifically, PubSub subscribers and cron jobs) may specify a basic exponential backoff retry policy. You can optionally specify a catch verb to handle final failures. ```go +// Basic retry //ftl:retry 10 5s 1m -func Process(ctx context.Context, in Invoice) error { - // Process with retries - return nil +func processPayment(ctx context.Context, payment Payment) error { + // Process with retries + return nil +} + +// Retry with catch handler +//ftl:retry 5 1s catch recoverPaymentProcessing +func processPayment(ctx context.Context, payment Payment) error { + // Process with retries, failures will be sent to recoverPaymentProcessing verb + return nil +} + +// The catch verb that handles final failures +//ftl:verb +func recoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error { + // Safely handle final failure of the payment + return nil } ``` See https://block.github.io/ftl/docs/reference/retries/ --- -//ftl:retry ${1:attempts} ${2:minBackoff} ${3:maxBackoff} -func ${4:Process}(ctx context.Context, in ${5:Type}) error { - ${6:// TODO: Implement} +//ftl:retry ${1:attempts} ${2:minBackoff} ${3:maxBackoff}${4: catch ${5:catchVerb}} +func ${6:process}(ctx context.Context, in ${7:Type}) error { + ${8:// TODO: Implement} + return nil +} + +// Optional catch verb handler +//ftl:verb +func ${5:catchVerb}(ctx context.Context, request builtin.CatchRequest[${7:Type}]) error { + ${9:// Safely handle final failure} return nil } diff --git a/internal/lsp/markdown/completion/go/retryWithCatch.md b/internal/lsp/markdown/completion/go/retryWithCatch.md deleted file mode 100644 index ac916f4c3..000000000 --- a/internal/lsp/markdown/completion/go/retryWithCatch.md +++ /dev/null @@ -1,32 +0,0 @@ -Directive for retrying an async operation with a catch handler. - -Specify a catch handler verb to safely handle final failures after all retries are exhausted. - -```go -//ftl:retry 5 1s catch recoverPaymentProcessing -func ProcessPayment(ctx context.Context, payment Payment) error { - // Process payment with retries - return nil -} - -//ftl:verb -func RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error { - // Safely handle final failure of the payment - return nil -} -``` - -See https://block.github.io/ftl/docs/reference/retries/ ---- - -//ftl:retry ${1:5} ${2:1s} catch ${3:RecoverProcessing} -func ${4:Process}(ctx context.Context, in ${5:Type}) error { - ${6:// TODO: Implement} - return nil -} - -//ftl:verb -func ${3:RecoverProcessing}(ctx context.Context, request builtin.CatchRequest[${5:Type}]) error { - ${7:// Handle final failure} - return nil -} diff --git a/internal/lsp/markdown/completion/java/config.md b/internal/lsp/markdown/completion/java/config.md index adc8ae569..4770b6a73 100644 --- a/internal/lsp/markdown/completion/java/config.md +++ b/internal/lsp/markdown/completion/java/config.md @@ -1,27 +1,15 @@ -Declare a config variable. +Inject a configuration value into a method. -Configuration values are named, typed values. They are managed by the `ftl config` command-line. +Configuration values can be injected into FTL methods such as @Verb, HTTP ingress, Cron etc. To inject a configuration value, use the following syntax: ```java -// Will create a config value called "myConfig" in the FTL schema -@Config -public class MyConfig { - private String value; - - public String getValue() { - return value; - } +@Verb +HelloResponse hello(HelloRequest helloRequest, @Config("defaultUser") String defaultUser) { + return new HelloResponse("Hello, " + defaultUser); } ``` See https://block.github.io/ftl/docs/reference/secretsconfig/ --- -@Config -public class ${1:Name} { - private ${2:Type} value; - - public ${2:Type} getValue() { - return value; - } -} +@Config("${5:configName}") diff --git a/internal/lsp/markdown/completion/java/cron.md b/internal/lsp/markdown/completion/java/cron.md index b76f15c0e..e4a72e4b6 100644 --- a/internal/lsp/markdown/completion/java/cron.md +++ b/internal/lsp/markdown/completion/java/cron.md @@ -23,9 +23,7 @@ class MyCron { See https://block.github.io/ftl/docs/reference/cron/ --- -class ${1:Name} { - @Cron("${2:schedule}") - void ${3:name}() { - ${4:// Add your cron job logic here} - } +@Cron("${1:schedule}") +void ${2:name}() { + ${3:// Add your cron job logic here} } diff --git a/internal/lsp/markdown/completion/java/retry.md b/internal/lsp/markdown/completion/java/retry.md index 6b21e529e..dfd795dcd 100644 --- a/internal/lsp/markdown/completion/java/retry.md +++ b/internal/lsp/markdown/completion/java/retry.md @@ -1,22 +1,37 @@ Directive for retrying an async operation. -Any verb called asynchronously (specifically, PubSub subscribers and cron jobs) may specify a basic exponential backoff retry policy. +Any verb called asynchronously (specifically, PubSub subscribers and cron jobs) may specify a basic exponential backoff retry policy. You can optionally specify a catch verb to handle final failures. ```java +// Basic retry @Retry(attempts = 10, minBackoff = "5s", maxBackoff = "1m") -public class InvoiceProcessor { - public void process(Context ctx, Invoice invoice) throws Exception { - // Process with retries - } +public void processPayment(Payment payment) throws Exception { + // Process with retries +} + +// Retry with catch handler +@Retry(attempts = 5, minBackoff = "1s", catchVerb = "recoverPaymentProcessing") +public void processPayment(Payment payment) throws Exception { + // Process with retries, failures will be sent to recoverPaymentProcessing verb +} + +// The catch verb that handles final failures +@Verb +public void recoverPaymentProcessing(CatchRequest req) { + // Safely handle final failure of the payment } ``` See https://block.github.io/ftl/docs/reference/retries/ --- -@Retry(attempts = ${1:10}, minBackoff = "${2:5s}", maxBackoff = "${3:1m}") -public class ${4:Processor} { - public void ${5:process}(Context ctx, ${6:Type} input) throws Exception { - ${7:// TODO: Implement} - } +@Retry(attempts = ${1:10}, minBackoff = "${2:5s}", maxBackoff = "${3:1m}"${4:, catchVerb = "${5:catchVerb}"}) +public void ${6:process}(${7:Type} input) throws Exception { + ${8:// TODO: Implement} +} + +// Optional catch verb handler +@Verb +public void ${5:catchVerb}(CatchRequest<${7:Type}> req) { + ${9:// Safely handle final failure} } diff --git a/internal/lsp/markdown/completion/java/secret.md b/internal/lsp/markdown/completion/java/secret.md new file mode 100644 index 000000000..82e372ea8 --- /dev/null +++ b/internal/lsp/markdown/completion/java/secret.md @@ -0,0 +1,15 @@ +Inject a secret value into a method. + +Secrets are encrypted, named, typed values. They are managed by the `ftl secret` command-line. To inject a secret value, use the following syntax: + +```java +@Verb +HelloResponse hello(HelloRequest helloRequest, @Secret("apiKey") String apiKey) { + return new HelloResponse("Hello from API: " + apiKey); +} +``` + +See https://block.github.io/ftl/docs/reference/secretsconfig/ +--- + +@Secret("${5:secretName}") diff --git a/internal/lsp/markdown/completion/java/verb.md b/internal/lsp/markdown/completion/java/verb.md index 4f0e69673..7edb8cbdd 100644 --- a/internal/lsp/markdown/completion/java/verb.md +++ b/internal/lsp/markdown/completion/java/verb.md @@ -3,16 +3,26 @@ Declare a verb. A verb is a method that can be called by other modules. It must be public and have a request parameter. ```java -// Define request/response types -record MyRequest(String name) {} -record MyResponse(String message) {} - -// Will create a verb called "myVerb" in the FTL schema -public class MyVerb { - @Verb - public MyResponse myVerb(MyRequest request) { - // Verb implementation - } +// Basic verb declaration +@Verb +public Response verb(Request request) { + // Verb implementation +} + +// Example with request/response types +record EchoRequest(String message) {} +record EchoResponse(String message) {} + +@Verb +public EchoResponse echo(EchoRequest request) { + return new EchoResponse("Echo: " + request.message()); +} + +// Example calling another verb +@Verb +public EchoResponse echo(EchoRequest request, TimeClient timeClient) { + TimeResponse time = timeClient.call(); + return new EchoResponse("Echo at " + time.time() + ": " + request.message()); } ``` @@ -23,9 +33,8 @@ See https://block.github.io/ftl/docs/reference/verbs/ record ${1:Name}Request(String data) {} record ${1:Name}Response(String result) {} -public class ${1:Name} { - @Verb - public ${1:Name}Response ${2:name}(${1:Name}Request request) { - ${3:// TODO: Implement} - } +@Verb +public ${1:Name}Response ${2:name}(${1:Name}Request request) { + ${3:// TODO: Implement} + return new ${1:Name}Response("result"); } diff --git a/internal/lsp/markdown/completion/kotlin/retry.md b/internal/lsp/markdown/completion/kotlin/retry.md index 857d803af..1d2063ec4 100644 --- a/internal/lsp/markdown/completion/kotlin/retry.md +++ b/internal/lsp/markdown/completion/kotlin/retry.md @@ -1,22 +1,37 @@ Directive for retrying an async operation. -Any verb called asynchronously (specifically, PubSub subscribers and cron jobs) may specify a basic exponential backoff retry policy. +Any verb called asynchronously (specifically, PubSub subscribers and cron jobs) may specify a basic exponential backoff retry policy. You can optionally specify a catch verb to handle final failures. ```kotlin +// Basic retry @Retry(attempts = 10, minBackoff = "5s", maxBackoff = "1m") -class InvoiceProcessor { - suspend fun process(ctx: Context, invoice: Invoice) { - // Process with retries - } +fun processPayment(payment: Payment) { + // Process with retries +} + +// Retry with catch handler +@Retry(attempts = 5, minBackoff = "1s", catchVerb = "recoverPaymentProcessing") +fun processPayment(payment: Payment) { + // Process with retries, failures will be sent to recoverPaymentProcessing verb +} + +// The catch verb that handles final failures +@Verb +fun recoverPaymentProcessing(req: CatchRequest) { + // Safely handle final failure of the payment } ``` See https://block.github.io/ftl/docs/reference/retries/ --- -@Retry(attempts = ${1:10}, minBackoff = "${2:5s}", maxBackoff = "${3:1m}") -class ${4:Processor} { - suspend fun ${5:process}(ctx: Context, input: ${6:Type}) { - ${7:// TODO: Implement} - } +@Retry(attempts = ${1:10}, minBackoff = "${2:5s}", maxBackoff = "${3:1m}"${4:, catchVerb = "${5:catchVerb}"}) +fun ${6:process}(${7:input}: ${8:Type}) { + ${9:// TODO: Implement} +} + +// Optional catch verb handler +@Verb +fun ${5:catchVerb}(req: CatchRequest<${8:Type}>) { + ${10:// Safely handle final failure} } diff --git a/internal/lsp/markdown/completion/kotlin/secret.md b/internal/lsp/markdown/completion/kotlin/secret.md new file mode 100644 index 000000000..d93b0ad38 --- /dev/null +++ b/internal/lsp/markdown/completion/kotlin/secret.md @@ -0,0 +1,17 @@ +Declare a secret variable. + +Secrets are encrypted, named, typed values. They are managed by the `ftl secret` command-line. + +```kotlin +// Example usage of a secret in a verb +@Export +@Verb +fun processPayment(@Secret("apiKey") apiKey: String) { + // Use the secret apiKey value +} +``` + +See https://block.github.io/ftl/docs/reference/secretsconfig/ +--- + +@Secret("${1:secretName}") diff --git a/internal/lsp/templates.go b/internal/lsp/templates.go index caaf93431..66d016160 100644 --- a/internal/lsp/templates.go +++ b/internal/lsp/templates.go @@ -32,9 +32,6 @@ var cronExpressionCompletionDocs string //go:embed markdown/completion/go/retry.md var retryCompletionDocs string -//go:embed markdown/completion/go/retryWithCatch.md -var retryWithCatchCompletionDocs string - //go:embed markdown/completion/go/config.md var configCompletionDocs string @@ -61,7 +58,6 @@ var goCompletionItems = []protocol.CompletionItem{ completionItem("ftl:pubsub:subscription", "Create a PubSub subscription", pubSubSubscriptionCompletionDocs), completionItem("ftl:pubsub:topic", "Create a PubSub topic", pubSubTopicCompletionDocs), completionItem("ftl:retry", "FTL Retry", retryCompletionDocs), - completionItem("ftl:retry:catch", "FTL Retry with catch", retryWithCatchCompletionDocs), completionItem("ftl:secret", "Create a new secret value", secretCompletionDocs), completionItem("ftl:typealias", "FTL Type Alias", typeAliasCompletionDocs), completionItem("ftl:verb", "FTL Verb", verbCompletionDocs), @@ -86,12 +82,16 @@ var pubSubTopicCompletionDocsKotlin string //go:embed markdown/completion/kotlin/pubSubSubscription.md var pubSubSubscriptionCompletionDocsKotlin string +//go:embed markdown/completion/kotlin/secret.md +var secretCompletionDocsKotlin string + var kotlinCompletionItems = []protocol.CompletionItem{ completionItem("ftl:config", "Create a new configuration value", configCompletionDocsKotlin), completionItem("ftl:cron", "FTL Cron", cronCompletionDocsKotlin), completionItem("ftl:ingress", "FTL Ingress", ingressCompletionDocsKotlin), completionItem("ftl:pubsub:subscription", "Create a PubSub subscription", pubSubSubscriptionCompletionDocsKotlin), completionItem("ftl:pubsub:topic", "Create a PubSub topic", pubSubTopicCompletionDocsKotlin), + completionItem("ftl:secret", "Create a new secret value", secretCompletionDocsKotlin), completionItem("ftl:verb", "FTL Verb", verbCompletionDocsKotlin), } @@ -113,12 +113,16 @@ var pubSubTopicCompletionDocsJava string //go:embed markdown/completion/java/pubSubSubscription.md var pubSubSubscriptionCompletionDocsJava string +//go:embed markdown/completion/java/secret.md +var secretCompletionDocsJava string + var javaCompletionItems = []protocol.CompletionItem{ completionItem("ftl:config", "Create a new configuration value", configCompletionDocsJava), completionItem("ftl:cron", "FTL Cron", cronCompletionDocsJava), completionItem("ftl:ingress", "FTL Ingress", ingressCompletionDocsJava), completionItem("ftl:pubsub:subscription", "Create a PubSub subscription", pubSubSubscriptionCompletionDocsJava), completionItem("ftl:pubsub:topic", "Create a PubSub topic", pubSubTopicCompletionDocsJava), + completionItem("ftl:secret", "Create a new secret value", secretCompletionDocsJava), completionItem("ftl:verb", "FTL Verb", verbCompletionDocsJava), }