From 307c7e0e6c12dc1a3f1c01a4dc0bc241ff158b33 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Mon, 12 Jan 2026 19:23:31 +0100 Subject: [PATCH 1/2] Feedback on new Reflection based API: * Move the client proxy gen top level in Restate and rename ServiceReference to ServiceHandle * Move Restate.context() to Context.current() * Move state and promises access inside Restate * Same as above for ingress client --- .../main/java/dev/restate/client/Client.java | 219 +++++-- ...eference.java => ClientServiceHandle.java} | 51 +- ...Impl.java => ClientServiceHandleImpl.java} | 37 +- .../java/my/restate/sdk/examples/Counter.java | 17 +- .../my/restate/sdk/examples/LoanWorkflow.java | 45 +- .../main/java/dev/restate/sdk/Context.java | 9 + .../main/java/dev/restate/sdk/Restate.java | 561 ++++++++++++------ ...rviceReference.java => ServiceHandle.java} | 185 +++--- ...erenceImpl.java => ServiceHandleImpl.java} | 87 +-- 9 files changed, 749 insertions(+), 462 deletions(-) rename client/src/main/java/dev/restate/client/{ClientServiceReference.java => ClientServiceHandle.java} (94%) rename client/src/main/java/dev/restate/client/{ClientServiceReferenceImpl.java => ClientServiceHandleImpl.java} (80%) rename sdk-api/src/main/java/dev/restate/sdk/{ServiceReference.java => ServiceHandle.java} (61%) rename sdk-api/src/main/java/dev/restate/sdk/{ServiceReferenceImpl.java => ServiceHandleImpl.java} (61%) diff --git a/client/src/main/java/dev/restate/client/Client.java b/client/src/main/java/dev/restate/client/Client.java index 28df265b..ecd7555c 100644 --- a/client/src/main/java/dev/restate/client/Client.java +++ b/client/src/main/java/dev/restate/client/Client.java @@ -14,6 +14,10 @@ import dev.restate.common.Request; import dev.restate.common.Target; import dev.restate.common.WorkflowRequest; +import dev.restate.common.reflections.MethodInfo; +import dev.restate.common.reflections.ProxySupport; +import dev.restate.common.reflections.ReflectionUtils; +import dev.restate.common.reflections.RestateUtils; import dev.restate.sdk.annotation.Service; import dev.restate.sdk.annotation.VirtualObject; import dev.restate.sdk.annotation.Workflow; @@ -531,98 +535,241 @@ default Response> getOutput() throws IngressException { } /** - * EXPERIMENTAL API: Create a reference to invoke a Restate service from the ingress. This - * API may change in future releases. + * EXPERIMENTAL API: Simple API to invoke a Restate service from the ingress. * - *

You can invoke the service in three ways: + *

Create a proxy client that allows calling service methods directly and synchronously, + * returning just the output (not wrapped in {@link Response}). This is the recommended approach + * for straightforward request-response interactions. * *

{@code
    * Client client = Client.connect("http://localhost:8080");
    *
-   * // 1. Create a client proxy and call it directly (returns output directly)
-   * var greeterProxy = client.service(Greeter.class).client();
+   * var greeterProxy = client.service(Greeter.class);
    * GreetingResponse output = greeterProxy.greet(new Greeting("Alice"));
+   * }
+ * + *

For advanced use cases requiring asynchronous request handling, access to {@link Response} + * metadata, or invocation options (such as idempotency keys), use {@link #serviceHandle(Class)} + * instead. * - * // 2. Use call() with method reference and wait for the result - * Response response = client.service(Greeter.class) + * @param clazz the service class annotated with {@link Service} + * @return a proxy client to invoke the service + */ + @org.jetbrains.annotations.ApiStatus.Experimental + default SVC service(Class clazz) { + mustHaveAnnotation(clazz, Service.class); + var serviceName = ReflectionUtils.extractServiceName(clazz); + return ProxySupport.createProxy( + clazz, + invocation -> { + var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); + + //noinspection unchecked + return this.call( + Request.of( + Target.virtualObject(serviceName, null, methodInfo.getHandlerName()), + (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) + .response(); + }); + } + + /** + * EXPERIMENTAL API: Advanced API to invoke a Restate service from the ingress with full + * control. + * + *

Create a handle that provides advanced invocation capabilities including: + * + *

+ * + *
{@code
+   * Client client = Client.connect("http://localhost:8080");
+   *
+   * // Use call() with method reference and wait for the result
+   * Response response = client.serviceHandle(Greeter.class)
    *   .call(Greeter::greet, new Greeting("Alice"));
    *
-   * // 3. Use send() for one-way invocation without waiting
-   * SendResponse sendResponse = client.service(Greeter.class)
+   * // Use send() for one-way invocation without waiting
+   * SendResponse sendResponse = client.serviceHandle(Greeter.class)
    *   .send(Greeter::greet, new Greeting("Alice"));
    * }
* + *

For simple synchronous request-response interactions, consider using {@link #service(Class)} + * instead. + * * @param clazz the service class annotated with {@link Service} - * @return a reference to invoke the service + * @return a handle to invoke the service with advanced options */ @org.jetbrains.annotations.ApiStatus.Experimental - default ClientServiceReference service(Class clazz) { + default ClientServiceHandle serviceHandle(Class clazz) { mustHaveAnnotation(clazz, Service.class); - return new ClientServiceReferenceImpl<>(this, clazz, null); + return new ClientServiceHandleImpl<>(this, clazz, null); } /** - * EXPERIMENTAL API: Create a reference to invoke a Restate Virtual Object from the - * ingress. This API may change in future releases. + * EXPERIMENTAL API: Simple API to invoke a Restate Virtual Object from the ingress. * - *

You can invoke the virtual object in three ways: + *

Create a proxy client that allows calling virtual object methods directly and synchronously, + * returning just the output (not wrapped in {@link Response}). This is the recommended approach + * for straightforward request-response interactions. * *

{@code
    * Client client = Client.connect("http://localhost:8080");
    *
-   * // 1. Create a client proxy and call it directly (returns output directly)
-   * var counterProxy = client.virtualObject(Counter.class, "my-counter").client();
+   * var counterProxy = client.virtualObject(Counter.class, "my-counter");
    * int count = counterProxy.increment();
+   * }
+ * + *

For advanced use cases requiring asynchronous request handling, access to {@link Response} + * metadata, or invocation options (such as idempotency keys), use {@link + * #virtualObjectHandle(Class, String)} instead. + * + * @param clazz the virtual object class annotated with {@link VirtualObject} + * @param key the key identifying the specific virtual object instance + * @return a proxy client to invoke the virtual object + */ + @org.jetbrains.annotations.ApiStatus.Experimental + default SVC virtualObject(Class clazz, String key) { + mustHaveAnnotation(clazz, VirtualObject.class); + var serviceName = ReflectionUtils.extractServiceName(clazz); + return ProxySupport.createProxy( + clazz, + invocation -> { + var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); + + //noinspection unchecked + return this.call( + Request.of( + Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), + (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) + .response(); + }); + } + + /** + * EXPERIMENTAL API: Advanced API to invoke a Restate Virtual Object from the ingress with + * full control. + * + *

Create a handle that provides advanced invocation capabilities including: + * + *

+ * + *
{@code
+   * Client client = Client.connect("http://localhost:8080");
    *
-   * // 2. Use call() with method reference and wait for the result
-   * Response response = client.virtualObject(Counter.class, "my-counter")
+   * // Use call() with method reference and wait for the result
+   * Response response = client.virtualObjectHandle(Counter.class, "my-counter")
    *   .call(Counter::increment);
    *
-   * // 3. Use send() for one-way invocation without waiting
-   * SendResponse sendResponse = client.virtualObject(Counter.class, "my-counter")
+   * // Use send() for one-way invocation without waiting
+   * SendResponse sendResponse = client.virtualObjectHandle(Counter.class, "my-counter")
    *   .send(Counter::increment);
    * }
* + *

For simple synchronous request-response interactions, consider using {@link + * #virtualObject(Class, String)} instead. + * * @param clazz the virtual object class annotated with {@link VirtualObject} * @param key the key identifying the specific virtual object instance - * @return a reference to invoke the virtual object + * @return a handle to invoke the virtual object with advanced options */ @org.jetbrains.annotations.ApiStatus.Experimental - default ClientServiceReference virtualObject(Class clazz, String key) { + default ClientServiceHandle virtualObjectHandle(Class clazz, String key) { mustHaveAnnotation(clazz, VirtualObject.class); - return new ClientServiceReferenceImpl<>(this, clazz, key); + return new ClientServiceHandleImpl<>(this, clazz, key); } /** - * EXPERIMENTAL API: Create a reference to invoke a Restate Workflow from the ingress. This - * API may change in future releases. + * EXPERIMENTAL API: Simple API to invoke a Restate Workflow from the ingress. * - *

You can invoke the workflow in three ways: + *

Create a proxy client that allows calling workflow methods directly and synchronously, + * returning just the output (not wrapped in {@link Response}). This is the recommended approach + * for straightforward request-response interactions. * *

{@code
    * Client client = Client.connect("http://localhost:8080");
    *
-   * // 1. Create a client proxy and call it directly (returns output directly)
-   * var workflowProxy = client.workflow(OrderWorkflow.class, "order-123").client();
+   * var workflowProxy = client.workflow(OrderWorkflow.class, "order-123");
    * OrderResult result = workflowProxy.start(new OrderRequest(...));
+   * }
+ * + *

For advanced use cases requiring asynchronous request handling, access to {@link Response} + * metadata, or invocation options (such as idempotency keys), use {@link #workflowHandle(Class, + * String)} instead. * - * // 2. Use call() with method reference and wait for the result - * Response response = client.workflow(OrderWorkflow.class, "order-123") + * @param clazz the workflow class annotated with {@link Workflow} + * @param key the key identifying the specific workflow instance + * @return a proxy client to invoke the workflow + */ + @org.jetbrains.annotations.ApiStatus.Experimental + default SVC workflow(Class clazz, String key) { + mustHaveAnnotation(clazz, Workflow.class); + var serviceName = ReflectionUtils.extractServiceName(clazz); + return ProxySupport.createProxy( + clazz, + invocation -> { + var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); + + //noinspection unchecked + return this.call( + Request.of( + Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), + (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) + .response(); + }); + } + + /** + * EXPERIMENTAL API: Advanced API to invoke a Restate Workflow from the ingress with full + * control. + * + *

Create a handle that provides advanced invocation capabilities including: + * + *

+ * + *
{@code
+   * Client client = Client.connect("http://localhost:8080");
+   *
+   * // Use call() with method reference and wait for the result
+   * Response response = client.workflowHandle(OrderWorkflow.class, "order-123")
    *   .call(OrderWorkflow::start, new OrderRequest(...));
    *
-   * // 3. Use send() for one-way invocation without waiting
-   * SendResponse sendResponse = client.workflow(OrderWorkflow.class, "order-123")
+   * // Use send() for one-way invocation without waiting
+   * SendResponse sendResponse = client.workflowHandle(OrderWorkflow.class, "order-123")
    *   .send(OrderWorkflow::start, new OrderRequest(...));
    * }
* + *

For simple synchronous request-response interactions, consider using {@link #workflow(Class, + * String)} instead. + * * @param clazz the workflow class annotated with {@link Workflow} * @param key the key identifying the specific workflow instance - * @return a reference to invoke the workflow + * @return a handle to invoke the workflow with advanced options */ @org.jetbrains.annotations.ApiStatus.Experimental - default ClientServiceReference workflow(Class clazz, String key) { + default ClientServiceHandle workflowHandle(Class clazz, String key) { mustHaveAnnotation(clazz, Workflow.class); - return new ClientServiceReferenceImpl<>(this, clazz, key); + return new ClientServiceHandleImpl<>(this, clazz, key); } /** diff --git a/client/src/main/java/dev/restate/client/ClientServiceReference.java b/client/src/main/java/dev/restate/client/ClientServiceHandle.java similarity index 94% rename from client/src/main/java/dev/restate/client/ClientServiceReference.java rename to client/src/main/java/dev/restate/client/ClientServiceHandle.java index 5e2c4ae3..dba5e153 100644 --- a/client/src/main/java/dev/restate/client/ClientServiceReference.java +++ b/client/src/main/java/dev/restate/client/ClientServiceHandle.java @@ -21,50 +21,41 @@ * EXPERIMENTAL API: This interface is part of the new reflection-based API and may change in * future releases. * - *

A reference to a Restate service, virtual object, or workflow that can be invoked from the - * ingress (outside of a handler). Provides three ways to invoke methods: + *

Advanced API handle for invoking Restate services, virtual objects, or workflows from the + * ingress (outside of a handler). This handle provides advanced invocation capabilities including: + * + *

+ * + *

Use this handle to perform requests with method references: * *

{@code
  * Client client = Client.connect("http://localhost:8080");
  *
- * // 1. Create a client proxy and call it directly (returns output directly)
- * var greeterProxy = client.service(Greeter.class).client();
- * GreetingResponse output = greeterProxy.greet(new Greeting("Alice"));
- *
- * // 2. Use call() with method reference and wait for the result
- * Response response = client.service(Greeter.class)
+ * // 1. Use call() with method reference and wait for the result
+ * Response response = client.serviceHandle(Greeter.class)
  *   .call(Greeter::greet, new Greeting("Alice"));
  *
- * // 3. Use send() for one-way invocation without waiting
- * SendResponse sendResponse = client.service(Greeter.class)
+ * // 2. Use send() for one-way invocation without waiting
+ * SendResponse sendResponse = client.serviceHandle(Greeter.class)
  *   .send(Greeter::greet, new Greeting("Alice"));
  * }
* - *

Create instances using {@link Client#service(Class)}, {@link Client#virtualObject(Class, + *

Create instances using {@link Client#serviceHandle(Class)}, {@link + * Client#virtualObjectHandle(Class, String)}, or {@link Client#workflowHandle(Class, String)}. + * + *

For simple synchronous request-response interactions returning just the output, consider using + * the simple proxy API instead: {@link Client#service(Class)}, {@link Client#virtualObject(Class, * String)}, or {@link Client#workflow(Class, String)}. * * @param the service interface type */ @org.jetbrains.annotations.ApiStatus.Experimental -public interface ClientServiceReference { - /** - * EXPERIMENTAL API: Get a client proxy to call methods directly (returns output directly, - * not wrapped in Response). - * - *

{@code
-   * Client client = Client.connect("http://localhost:8080");
-   *
-   * // Get a proxy and call methods on it (returns output directly)
-   * var greeterProxy = client.service(Greeter.class).client();
-   * GreetingResponse output = greeterProxy.greet(new Greeting("Alice"));
-   * }
- * - * @return a proxy instance of the service interface - */ - @org.jetbrains.annotations.ApiStatus.Experimental - SVC client(); - - // call - BiFunction variants +public interface ClientServiceHandle { /** * EXPERIMENTAL API: Invoke a service method with input and wait for the response. * diff --git a/client/src/main/java/dev/restate/client/ClientServiceReferenceImpl.java b/client/src/main/java/dev/restate/client/ClientServiceHandleImpl.java similarity index 80% rename from client/src/main/java/dev/restate/client/ClientServiceReferenceImpl.java rename to client/src/main/java/dev/restate/client/ClientServiceHandleImpl.java index 7730a4b6..67cf3ce2 100644 --- a/client/src/main/java/dev/restate/client/ClientServiceReferenceImpl.java +++ b/client/src/main/java/dev/restate/client/ClientServiceHandleImpl.java @@ -11,8 +11,6 @@ import static dev.restate.common.reflections.RestateUtils.toRequest; import dev.restate.common.InvocationOptions; -import dev.restate.common.Request; -import dev.restate.common.Target; import dev.restate.common.reflections.*; import dev.restate.serde.Serde; import dev.restate.serde.TypeTag; @@ -24,7 +22,7 @@ import java.util.function.Function; import org.jspecify.annotations.Nullable; -final class ClientServiceReferenceImpl implements ClientServiceReference { +final class ClientServiceHandleImpl implements ClientServiceHandle { private final Client innerClient; @@ -32,46 +30,15 @@ final class ClientServiceReferenceImpl implements ClientServiceReference methodInfoCollector; - ClientServiceReferenceImpl(Client innerClient, Class clazz, @Nullable String key) { + ClientServiceHandleImpl(Client innerClient, Class clazz, @Nullable String key) { this.innerClient = innerClient; this.clazz = clazz; this.serviceName = ReflectionUtils.extractServiceName(clazz); this.key = key; } - @Override - public SVC client() { - if (proxyClient == null) { - this.proxyClient = - ProxySupport.createProxy( - clazz, - invocation -> { - var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); - - //noinspection unchecked - return innerClient - .call( - Request.of( - Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), - (TypeTag) - RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) - RestateUtils.typeTag(methodInfo.getOutputType()), - invocation.getArguments().length == 0 - ? null - : invocation.getArguments()[0])) - .response(); - }); - } - return this.proxyClient; - } - @SuppressWarnings("unchecked") @Override public CompletableFuture> callAsync( diff --git a/examples/src/main/java/my/restate/sdk/examples/Counter.java b/examples/src/main/java/my/restate/sdk/examples/Counter.java index c6090f00..8eaaeffc 100644 --- a/examples/src/main/java/my/restate/sdk/examples/Counter.java +++ b/examples/src/main/java/my/restate/sdk/examples/Counter.java @@ -31,37 +31,36 @@ public class Counter { /** Reset the counter. */ @Handler public void reset() { - Restate.objectContext().clearAll(); + Restate.state().clearAll(); } /** Add the given value to the count. */ @Handler public void add(long request) { - var ctx = Restate.objectContext(); + var state = Restate.state(); - long currentValue = ctx.get(TOTAL).orElse(0L); + long currentValue = state.get(TOTAL).orElse(0L); long newValue = currentValue + request; - ctx.set(TOTAL, newValue); + state.set(TOTAL, newValue); } /** Get the current counter value. */ @Shared @Handler public long get() { - var ctx = Restate.sharedObjectContext(); - return ctx.get(TOTAL).orElse(0L); + return Restate.state().get(TOTAL).orElse(0L); } /** Add a value, and get both the previous value and the new value. */ @Handler public CounterUpdateResult getAndAdd(long request) { - var ctx = Restate.objectContext(); + var state = Restate.state(); LOG.info("Invoked get and add with {}", request); - long currentValue = ctx.get(TOTAL).orElse(0L); + long currentValue = state.get(TOTAL).orElse(0L); long newValue = currentValue + request; - ctx.set(TOTAL, newValue); + state.set(TOTAL, newValue); return new CounterUpdateResult(newValue, currentValue); } diff --git a/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java b/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java index f1a6012d..a8a3677b 100644 --- a/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java +++ b/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java @@ -58,48 +58,49 @@ public record LoanRequest( @Workflow public String run(LoanRequest loanRequest) { - var ctx = (WorkflowContext) Restate.context(); + var state = Restate.state(); // 1. Set status - ctx.set(STATUS, Status.SUBMITTED); - ctx.set(LOAN_REQUEST, loanRequest); + state.set(STATUS, Status.SUBMITTED); + state.set(LOAN_REQUEST, loanRequest); LOG.info("Loan request submitted"); // 2. Ask human approval - ctx.run(() -> askHumanApproval(ctx.key())); - ctx.set(STATUS, Status.WAITING_HUMAN_APPROVAL); + var key = Restate.key(); + Restate.run("human-approval", () -> askHumanApproval(key)); + state.set(STATUS, Status.WAITING_HUMAN_APPROVAL); // 3. Wait human approval - boolean approved = ctx.promise(HUMAN_APPROVAL).future().await(); + boolean approved = Restate.promise(HUMAN_APPROVAL).future().await(); if (!approved) { LOG.info("Not approved"); - ctx.set(STATUS, Status.NOT_APPROVED); + state.set(STATUS, Status.NOT_APPROVED); return "Not approved"; } LOG.info("Approved"); - ctx.set(STATUS, Status.APPROVED); + state.set(STATUS, Status.APPROVED); // 4. Request money transaction to the bank Instant executionTime; try { executionTime = - Restate.service(MockBank.class) + Restate.serviceHandle(MockBank.class) .call( MockBank::transfer, new TransferRequest(loanRequest.customerBankAccount(), loanRequest.amount())) .await(Duration.ofDays(7)); } catch (TerminalException e) { LOG.warn("Transaction failed", e); - ctx.set(STATUS, Status.TRANSFER_FAILED); + state.set(STATUS, Status.TRANSFER_FAILED); return "Failed"; } LOG.info("Transfer complete"); // 5. Transfer complete! - ctx.set(TRANSFER_EXECUTION_TIME, executionTime.toString()); - ctx.set(STATUS, Status.TRANSFER_SUCCEEDED); + state.set(TRANSFER_EXECUTION_TIME, executionTime.toString()); + state.set(STATUS, Status.TRANSFER_SUCCEEDED); return "Transfer succeeded"; } @@ -108,24 +109,18 @@ public String run(LoanRequest loanRequest) { @Shared public String approveLoan() { - var ctx = (SharedWorkflowContext) Restate.context(); - - ctx.promiseHandle(HUMAN_APPROVAL).resolve(true); + Restate.promiseHandle(HUMAN_APPROVAL).resolve(true); return "Approved"; } @Shared public void rejectLoan() { - var ctx = (SharedWorkflowContext) Restate.context(); - - ctx.promiseHandle(HUMAN_APPROVAL).resolve(false); + Restate.promiseHandle(HUMAN_APPROVAL).resolve(false); } @Shared public Status getStatus() { - var ctx = (SharedWorkflowContext) Restate.context(); - - return ctx.get(STATUS).orElse(Status.UNKNOWN); + return Restate.state().get(STATUS).orElse(Status.UNKNOWN); } public static void main(String[] args) { @@ -148,10 +143,10 @@ public static void main(String[] args) { // To invoke the workflow: Client restateClient = Client.connect("http://127.0.0.1:8080"); - ClientServiceReference loanWorkflow = + LoanWorkflow loanWorkflow = restateClient.workflow(LoanWorkflow.class, "my-loan"); var handle = - loanWorkflow.send( + restateClient.workflowHandle(LoanWorkflow.class, "my-loan").send( LoanWorkflow::run, new LoanRequest( "Francesco", "slinkydeveloper", "DE1234", new BigDecimal("1000000000"))); @@ -168,12 +163,12 @@ public static void main(String[] args) { LOG.info("We took the decision to approve your loan! You can now achieve your dreams!"); // Now approve it - loanWorkflow.client().approveLoan(); + loanWorkflow.approveLoan(); // Wait for output handle.attach(); - LOG.info("Loan workflow completed, now in status {}", loanWorkflow.client().getStatus()); + LOG.info("Loan workflow completed, now in status {}", loanWorkflow.getStatus()); } // -- Some mocks diff --git a/sdk-api/src/main/java/dev/restate/sdk/Context.java b/sdk-api/src/main/java/dev/restate/sdk/Context.java index da955b56..1ee55912 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/Context.java +++ b/sdk-api/src/main/java/dev/restate/sdk/Context.java @@ -16,6 +16,7 @@ import dev.restate.sdk.common.HandlerRequest; import dev.restate.sdk.common.RetryPolicy; import dev.restate.sdk.common.TerminalException; +import dev.restate.sdk.internal.ContextThreadLocal; import dev.restate.serde.Serde; import dev.restate.serde.TypeTag; import java.time.Duration; @@ -482,4 +483,12 @@ default Awakeable awakeable(Class clazz) { * @see RestateRandom */ RestateRandom random(); + + /** + * @return the current context + * @throws NullPointerException if called outside a Restate Handler + */ + static Context current() { + return ContextThreadLocal.getContext(); + } } diff --git a/sdk-api/src/main/java/dev/restate/sdk/Restate.java b/sdk-api/src/main/java/dev/restate/sdk/Restate.java index 9132de99..f8e623a5 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/Restate.java +++ b/sdk-api/src/main/java/dev/restate/sdk/Restate.java @@ -10,19 +10,25 @@ import static dev.restate.common.reflections.ReflectionUtils.mustHaveAnnotation; +import dev.restate.common.Request; import dev.restate.common.Slice; +import dev.restate.common.Target; import dev.restate.common.function.ThrowingRunnable; import dev.restate.common.function.ThrowingSupplier; +import dev.restate.common.reflections.MethodInfo; +import dev.restate.common.reflections.ProxySupport; +import dev.restate.common.reflections.ReflectionUtils; +import dev.restate.common.reflections.RestateUtils; import dev.restate.sdk.annotation.Service; import dev.restate.sdk.annotation.VirtualObject; import dev.restate.sdk.annotation.Workflow; -import dev.restate.sdk.common.AbortedExecutionException; -import dev.restate.sdk.common.HandlerRequest; -import dev.restate.sdk.common.RetryPolicy; -import dev.restate.sdk.common.TerminalException; -import dev.restate.sdk.internal.ContextThreadLocal; +import dev.restate.sdk.common.*; +import dev.restate.serde.Serde; import dev.restate.serde.TypeTag; import java.time.Duration; +import java.util.Collection; +import java.util.Optional; +import org.jspecify.annotations.NonNull; /** * This class exposes the Restate functionalities to Restate services using the reflection-based @@ -76,134 +82,12 @@ */ @org.jetbrains.annotations.ApiStatus.Experimental public final class Restate { - /** - * Get the base {@link Context} for the current handler invocation. - * - *

This method is safe to call from any Restate handler (Service, Virtual Object, or Workflow). - * - *

For handlers requiring access to state or promises, prefer using the specialized context - * getters: {@link #objectContext()}, {@link #sharedObjectContext()}, {@link #workflowContext()}, - * or {@link #sharedWorkflowContext()}. - * - * @return the current context - * @throws IllegalStateException if called outside a Restate handler - */ - @org.jetbrains.annotations.ApiStatus.Experimental - public static Context context() { - return ContextThreadLocal.getContext(); - } - - /** - * Get the {@link ObjectContext} for the current Virtual Object handler invocation. - * - *

This context provides access to read and write state operations for Virtual Objects. It is - * safe to call this method only from exclusive Virtual Object handlers (non-shared handlers). - * - * @return the current object context - * @throws IllegalStateException if called from a shared Virtual Object handler (use {@link - * #sharedObjectContext()} instead) or from a non-Virtual Object handler - */ - @org.jetbrains.annotations.ApiStatus.Experimental - public static ObjectContext objectContext() { - var handlerContext = HandlerRunner.getHandlerContext(); - - if (handlerContext.canReadState() && handlerContext.canWriteState()) { - return (ObjectContext) context(); - } - if (handlerContext.canReadState()) { - throw new IllegalStateException( - "Calling objectContext() from a Virtual object shared handler. You must use Restate.sharedObjectContext() instead."); - } - - throw new IllegalStateException( - "Calling objectContext() from a non Virtual object handler. You can use Restate.objectContext() only inside a Restate Virtual Object handler."); - } - - /** - * Get the {@link SharedObjectContext} for the current Virtual Object shared handler invocation. - * - *

This context provides read-only access to state operations for Virtual Objects. It is safe - * to call this method from shared Virtual Object handlers that need to read state but not modify - * it. - * - * @return the current shared object context - * @throws IllegalStateException if called from a non-Virtual Object handler - */ - @org.jetbrains.annotations.ApiStatus.Experimental - public static SharedObjectContext sharedObjectContext() { - var handlerContext = HandlerRunner.getHandlerContext(); - - if (handlerContext.canReadState()) { - return (SharedObjectContext) context(); - } - - throw new IllegalStateException( - "Calling objectContext() from a non Virtual object handler. You can use Restate.objectContext() only inside a Restate Virtual Object handler."); - } - - /** - * Get the {@link WorkflowContext} for the current Workflow handler invocation. - * - *

This context provides access to read and write promise operations for Workflows. It is safe - * to call this method only from exclusive Workflow handlers (non-shared handlers). - * - * @return the current workflow context - * @throws IllegalStateException if called from a shared Workflow handler (use {@link - * #sharedWorkflowContext()} instead) or from a non-Workflow handler - */ - @org.jetbrains.annotations.ApiStatus.Experimental - public static WorkflowContext workflowContext() { - var handlerContext = HandlerRunner.getHandlerContext(); - - if (handlerContext.canReadPromises() && handlerContext.canWritePromises()) { - return (WorkflowContext) context(); - } - if (handlerContext.canReadPromises()) { - throw new IllegalStateException( - "Calling workflowContext() from a Workflow shared handler. You must use Restate.sharedWorkflowContext() instead."); - } - - throw new IllegalStateException( - "Calling workflowContext() from a non Workflow handler. You can use Restate.workflowContext() only inside a Restate Workflow handler."); - } - - /** - * Get the {@link SharedWorkflowContext} for the current Workflow shared handler invocation. - * - *

This context provides read-only access to promise operations for Workflows. It is safe to - * call this method from shared Workflow handlers that need to read promises but not modify them. - * - * @return the current shared workflow context - * @throws IllegalStateException if called from a non-Workflow handler - */ - @org.jetbrains.annotations.ApiStatus.Experimental - public static SharedWorkflowContext sharedWorkflowContext() { - var handlerContext = HandlerRunner.getHandlerContext(); - - if (handlerContext.canReadPromises()) { - return (SharedWorkflowContext) context(); - } - - throw new IllegalStateException( - "Calling workflowContext() from a non Workflow handler. You can use Restate.workflowContext() only inside a Restate Workflow handler."); - } - - /** - * Check if the current code is executing inside a Restate handler. - * - * @return true if currently inside a handler, false otherwise - */ - @org.jetbrains.annotations.ApiStatus.Experimental - public static boolean isInsideHandler() { - return ContextThreadLocal.CONTEXT_THREAD_LOCAL.get() != null; - } - /** * @see Context#request() */ @org.jetbrains.annotations.ApiStatus.Experimental public static HandlerRequest request() { - return context().request(); + return Context.current().request(); } /** @@ -214,7 +98,7 @@ public static HandlerRequest request() { */ @org.jetbrains.annotations.ApiStatus.Experimental public static RestateRandom random() { - return context().random(); + return Context.current().random(); } /** @@ -223,7 +107,7 @@ public static RestateRandom random() { @org.jetbrains.annotations.ApiStatus.Experimental public static InvocationHandle invocationHandle( String invocationId, TypeTag responseTypeTag) { - return context().invocationHandle(invocationId, responseTypeTag); + return Context.current().invocationHandle(invocationId, responseTypeTag); } /** @@ -237,7 +121,7 @@ public static InvocationHandle invocationHandle( @org.jetbrains.annotations.ApiStatus.Experimental public static InvocationHandle invocationHandle( String invocationId, Class responseClazz) { - return context().invocationHandle(invocationId, responseClazz); + return Context.current().invocationHandle(invocationId, responseClazz); } /** @@ -247,7 +131,7 @@ public static InvocationHandle invocationHandle( */ @org.jetbrains.annotations.ApiStatus.Experimental public static InvocationHandle invocationHandle(String invocationId) { - return context().invocationHandle(invocationId); + return Context.current().invocationHandle(invocationId); } /** @@ -258,7 +142,7 @@ public static InvocationHandle invocationHandle(String invocationId) { */ @org.jetbrains.annotations.ApiStatus.Experimental public static void sleep(Duration duration) { - context().sleep(duration); + Context.current().sleep(duration); } /** @@ -271,7 +155,7 @@ public static void sleep(Duration duration) { */ @org.jetbrains.annotations.ApiStatus.Experimental public static DurableFuture timer(String name, Duration duration) { - return context().timer(name, duration); + return Context.current().timer(name, duration); } /** @@ -304,7 +188,7 @@ public static DurableFuture timer(String name, Duration duration) { @org.jetbrains.annotations.ApiStatus.Experimental public static T run(String name, Class clazz, ThrowingSupplier action) throws TerminalException { - return context().run(name, clazz, action); + return Context.current().run(name, clazz, action); } /** @@ -322,7 +206,7 @@ public static T run(String name, Class clazz, ThrowingSupplier action) public static T run( String name, TypeTag typeTag, RetryPolicy retryPolicy, ThrowingSupplier action) throws TerminalException { - return context().run(name, typeTag, retryPolicy, action); + return Context.current().run(name, typeTag, retryPolicy, action); } /** @@ -340,7 +224,7 @@ public static T run( public static T run( String name, Class clazz, RetryPolicy retryPolicy, ThrowingSupplier action) throws TerminalException { - return context().run(name, clazz, retryPolicy, action); + return Context.current().run(name, clazz, retryPolicy, action); } /** @@ -354,7 +238,7 @@ public static T run( @org.jetbrains.annotations.ApiStatus.Experimental public static T run(String name, TypeTag typeTag, ThrowingSupplier action) throws TerminalException { - return context().run(name, typeTag, action); + return Context.current().run(name, typeTag, action); } /** @@ -372,7 +256,7 @@ public static T run(String name, TypeTag typeTag, ThrowingSupplier act @org.jetbrains.annotations.ApiStatus.Experimental public static void run(String name, RetryPolicy retryPolicy, ThrowingRunnable runnable) throws TerminalException { - context().run(name, retryPolicy, runnable); + Context.current().run(name, retryPolicy, runnable); } /** @@ -383,7 +267,7 @@ public static void run(String name, RetryPolicy retryPolicy, ThrowingRunnable ru */ @org.jetbrains.annotations.ApiStatus.Experimental public static void run(String name, ThrowingRunnable runnable) throws TerminalException { - context().run(name, runnable); + Context.current().run(name, runnable); } /** @@ -396,7 +280,7 @@ public static void run(String name, ThrowingRunnable runnable) throws TerminalEx @org.jetbrains.annotations.ApiStatus.Experimental public static DurableFuture runAsync( String name, Class clazz, ThrowingSupplier action) throws TerminalException { - return context().runAsync(name, clazz, action); + return Context.current().runAsync(name, clazz, action); } /** @@ -410,7 +294,7 @@ public static DurableFuture runAsync( @org.jetbrains.annotations.ApiStatus.Experimental public static DurableFuture runAsync( String name, TypeTag typeTag, ThrowingSupplier action) throws TerminalException { - return context().runAsync(name, typeTag, action); + return Context.current().runAsync(name, typeTag, action); } /** @@ -428,7 +312,7 @@ public static DurableFuture runAsync( public static DurableFuture runAsync( String name, Class clazz, RetryPolicy retryPolicy, ThrowingSupplier action) throws TerminalException { - return context().runAsync(name, clazz, retryPolicy, action); + return Context.current().runAsync(name, clazz, retryPolicy, action); } /** @@ -446,7 +330,7 @@ public static DurableFuture runAsync( public static DurableFuture runAsync( String name, TypeTag typeTag, RetryPolicy retryPolicy, ThrowingSupplier action) throws TerminalException { - return context().runAsync(name, typeTag, retryPolicy, action); + return Context.current().runAsync(name, typeTag, retryPolicy, action); } /** @@ -464,7 +348,7 @@ public static DurableFuture runAsync( @org.jetbrains.annotations.ApiStatus.Experimental public static DurableFuture runAsync( String name, RetryPolicy retryPolicy, ThrowingRunnable runnable) throws TerminalException { - return context().runAsync(name, retryPolicy, runnable); + return Context.current().runAsync(name, retryPolicy, runnable); } /** @@ -475,7 +359,7 @@ public static DurableFuture runAsync( @org.jetbrains.annotations.ApiStatus.Experimental public static DurableFuture runAsync(String name, ThrowingRunnable runnable) throws TerminalException { - return context().runAsync(name, runnable); + return Context.current().runAsync(name, runnable); } /** @@ -494,7 +378,7 @@ public static DurableFuture runAsync(String name, ThrowingRunnable runnabl */ @org.jetbrains.annotations.ApiStatus.Experimental public static Awakeable awakeable(Class clazz) { - return context().awakeable(clazz); + return Context.current().awakeable(clazz); } /** @@ -512,7 +396,7 @@ public static Awakeable awakeable(Class clazz) { */ @org.jetbrains.annotations.ApiStatus.Experimental public static Awakeable awakeable(TypeTag typeTag) { - return context().awakeable(typeTag); + return Context.current().awakeable(typeTag); } /** @@ -525,95 +409,408 @@ public static Awakeable awakeable(TypeTag typeTag) { */ @org.jetbrains.annotations.ApiStatus.Experimental public static AwakeableHandle awakeableHandle(String id) { - return context().awakeableHandle(id); + return Context.current().awakeableHandle(id); } /** - * EXPERIMENTAL API: Create a reference to invoke a Restate service. + * EXPERIMENTAL API: Simple API to invoke a Restate service. * - *

You can invoke the service in three ways: + *

Create a proxy client that allows calling service methods directly and synchronously. This + * is the recommended approach for straightforward request-response interactions. * *

{@code
-   * // 1. Create a client proxy and call it directly
-   * var greeterProxy = Restate.service(Greeter.class).client();
+   * var greeterProxy = Restate.service(Greeter.class);
    * GreetingResponse response = greeterProxy.greet(new Greeting("Alice"));
+   * }
+ * + *

For advanced use cases requiring asynchronous request handling, composable futures, or + * invocation options (such as idempotency keys), use {@link #serviceHandle(Class)} instead. * - * // 2. Use call() with method reference and await the result - * GreetingResponse response = Restate.service(Greeter.class) + * @param clazz the service class annotated with {@link Service} + * @return a proxy client to invoke the service + */ + @org.jetbrains.annotations.ApiStatus.Experimental + public static SVC service(Class clazz) { + mustHaveAnnotation(clazz, Service.class); + String serviceName = ReflectionUtils.extractServiceName(clazz); + return ProxySupport.createProxy( + clazz, + invocation -> { + var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); + + //noinspection unchecked + return Context.current() + .call( + Request.of( + Target.virtualObject(serviceName, null, methodInfo.getHandlerName()), + (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) + .await(); + }); + } + + /** + * EXPERIMENTAL API: Advanced API to invoke a Restate service with full control. + * + *

Create a handle that provides advanced invocation capabilities including: + * + *

    + *
  • Composable futures for asynchronous request handling + *
  • Invocation options such as {@link + * dev.restate.common.InvocationOptions#idempotencyKey(String)} + *
  • Fire-and-forget requests via {@code send()} + *
  • Deferred response handling + *
+ * + *
{@code
+   * // 1. Use call() with method reference and await the result
+   * GreetingResponse response = Restate.serviceHandle(Greeter.class)
    *   .call(Greeter::greet, new Greeting("Alice"))
    *   .await();
    *
-   * // 3. Use send() for one-way invocation without waiting
-   * InvocationHandle handle = Restate.service(Greeter.class)
+   * // 2. Use send() for one-way invocation without waiting
+   * InvocationHandle handle = Restate.serviceHandle(Greeter.class)
    *   .send(Greeter::greet, new Greeting("Alice"));
    * }
* + *

For simple synchronous request-response interactions, consider using {@link #service(Class)} + * instead. + * * @param clazz the service class annotated with {@link Service} - * @return a reference to invoke the service + * @return a handle to invoke the service with advanced options */ @org.jetbrains.annotations.ApiStatus.Experimental - public static ServiceReference service(Class clazz) { + public static ServiceHandle serviceHandle(Class clazz) { mustHaveAnnotation(clazz, Service.class); - return new ServiceReferenceImpl<>(clazz, null); + return new ServiceHandleImpl<>(clazz, null); } /** - * EXPERIMENTAL API: Create a reference to invoke a Restate Virtual Object. + * EXPERIMENTAL API: Simple API to invoke a Restate Virtual Object. * - *

You can invoke the virtual object in three ways: + *

Create a proxy client that allows calling virtual object methods directly and synchronously. + * This is the recommended approach for straightforward request-response interactions. * *

{@code
-   * // 1. Create a client proxy and call it directly
-   * var counterProxy = Restate.virtualObject(Counter.class, "my-counter").client();
+   * var counterProxy = Restate.virtualObject(Counter.class, "my-counter");
    * int count = counterProxy.increment();
+   * }
+ * + *

For advanced use cases requiring asynchronous request handling, composable futures, or + * invocation options (such as idempotency keys), use {@link #virtualObjectHandle(Class, String)} + * instead. * - * // 2. Use call() with method reference and await the result - * int count = Restate.virtualObject(Counter.class, "my-counter") + * @param clazz the virtual object class annotated with {@link VirtualObject} + * @param key the key identifying the specific virtual object instance + * @return a proxy client to invoke the virtual object + */ + @org.jetbrains.annotations.ApiStatus.Experimental + public static SVC virtualObject(Class clazz, String key) { + mustHaveAnnotation(clazz, VirtualObject.class); + String serviceName = ReflectionUtils.extractServiceName(clazz); + return ProxySupport.createProxy( + clazz, + invocation -> { + var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); + + //noinspection unchecked + return Context.current() + .call( + Request.of( + Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), + (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) + .await(); + }); + } + + /** + * EXPERIMENTAL API: Advanced API to invoke a Restate Virtual Object with full control. + * + *

Create a handle that provides advanced invocation capabilities including: + * + *

    + *
  • Composable futures for asynchronous request handling + *
  • Invocation options such as {@link + * dev.restate.common.InvocationOptions#idempotencyKey(String)} + *
  • Fire-and-forget requests via {@code send()} + *
  • Deferred response handling + *
+ * + *
{@code
+   * // 1. Use call() with method reference and await the result
+   * int count = Restate.virtualObjectHandle(Counter.class, "my-counter")
    *   .call(Counter::increment)
    *   .await();
    *
-   * // 3. Use send() for one-way invocation without waiting
-   * InvocationHandle handle = Restate.virtualObject(Counter.class, "my-counter")
+   * // 2. Use send() for one-way invocation without waiting
+   * InvocationHandle handle = Restate.virtualObjectHandle(Counter.class, "my-counter")
    *   .send(Counter::increment);
    * }
* + *

For simple synchronous request-response interactions, consider using {@link + * #virtualObject(Class, String)} instead. + * * @param clazz the virtual object class annotated with {@link VirtualObject} * @param key the key identifying the specific virtual object instance - * @return a reference to invoke the virtual object + * @return a handle to invoke the virtual object with advanced options */ @org.jetbrains.annotations.ApiStatus.Experimental - public static ServiceReference virtualObject(Class clazz, String key) { + public static ServiceHandle virtualObjectHandle(Class clazz, String key) { mustHaveAnnotation(clazz, VirtualObject.class); - return new ServiceReferenceImpl<>(clazz, key); + return new ServiceHandleImpl<>(clazz, key); } /** - * EXPERIMENTAL API: Create a reference to invoke a Restate Workflow. + * EXPERIMENTAL API: Simple API to invoke a Restate Workflow. * - *

You can invoke the workflow in three ways: + *

Create a proxy client that allows calling workflow methods directly and synchronously. This + * is the recommended approach for straightforward request-response interactions. * *

{@code
-   * // 1. Create a client proxy and call it directly
-   * var workflowProxy = Restate.workflow(OrderWorkflow.class, "order-123").client();
+   * var workflowProxy = Restate.workflow(OrderWorkflow.class, "order-123");
    * workflowProxy.start(new OrderRequest(...));
+   * }
+ * + *

For advanced use cases requiring asynchronous request handling, composable futures, or + * invocation options (such as idempotency keys), use {@link #workflowHandle(Class, String)} + * instead. * - * // 2. Use call() with method reference and await the result - * Restate.workflow(OrderWorkflow.class, "order-123") + * @param clazz the workflow class annotated with {@link Workflow} + * @param key the key identifying the specific workflow instance + * @return a proxy client to invoke the workflow + */ + @org.jetbrains.annotations.ApiStatus.Experimental + public static SVC workflow(Class clazz, String key) { + mustHaveAnnotation(clazz, Workflow.class); + String serviceName = ReflectionUtils.extractServiceName(clazz); + return ProxySupport.createProxy( + clazz, + invocation -> { + var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); + + //noinspection unchecked + return Context.current() + .call( + Request.of( + Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), + (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) + .await(); + }); + } + + /** + * EXPERIMENTAL API: Advanced API to invoke a Restate Workflow with full control. + * + *

Create a handle that provides advanced invocation capabilities including: + * + *

    + *
  • Composable futures for asynchronous request handling + *
  • Invocation options such as {@link + * dev.restate.common.InvocationOptions#idempotencyKey(String)} + *
  • Fire-and-forget requests via {@code send()} + *
  • Deferred response handling + *
+ * + *
{@code
+   * // 1. Use call() with method reference and await the result
+   * Restate.workflowHandle(OrderWorkflow.class, "order-123")
    *   .call(OrderWorkflow::start, new OrderRequest(...))
    *   .await();
    *
-   * // 3. Use send() for one-way invocation without waiting
-   * InvocationHandle handle = Restate.workflow(OrderWorkflow.class, "order-123")
+   * // 2. Use send() for one-way invocation without waiting
+   * InvocationHandle handle = Restate.workflowHandle(OrderWorkflow.class, "order-123")
    *   .send(OrderWorkflow::start, new OrderRequest(...));
    * }
* + *

For simple synchronous request-response interactions, consider using {@link #workflow(Class, + * String)} instead. + * * @param clazz the workflow class annotated with {@link Workflow} * @param key the key identifying the specific workflow instance - * @return a reference to invoke the workflow + * @return a handle to invoke the workflow with advanced options */ @org.jetbrains.annotations.ApiStatus.Experimental - public static ServiceReference workflow(Class clazz, String key) { + public static ServiceHandle workflowHandle(Class clazz, String key) { mustHaveAnnotation(clazz, Workflow.class); - return new ServiceReferenceImpl<>(clazz, key); + return new ServiceHandleImpl<>(clazz, key); + } + + /** EXPERIMENTAL API: Interface to interact with this Virtual Object/Workflow state. */ + @org.jetbrains.annotations.ApiStatus.Experimental + public interface State { + + /** + * EXPERIMENTAL API: Gets the state stored under key, deserializing the raw value using + * the {@link Serde} in the {@link StateKey}. + * + * @param key identifying the state to get and its type. + * @return an {@link Optional} containing the stored state deserialized or an empty {@link + * Optional} if not set yet. + * @throws RuntimeException when the state cannot be deserialized. + */ + @org.jetbrains.annotations.ApiStatus.Experimental + Optional get(StateKey key); + + /** + * EXPERIMENTAL API: Sets the given value under the given key, serializing the value + * using the {@link Serde} in the {@link StateKey}. + * + * @param key identifying the value to store and its type. + * @param value to store under the given key. MUST NOT be null. + * @throws IllegalStateException if called from a Shared handler + */ + @org.jetbrains.annotations.ApiStatus.Experimental + void set(StateKey key, @NonNull T value); + + /** + * EXPERIMENTAL API: Clears the state stored under key. + * + * @param key identifying the state to clear. + * @throws IllegalStateException if called from a Shared handler + */ + @org.jetbrains.annotations.ApiStatus.Experimental + void clear(StateKey key); + + /** + * EXPERIMENTAL API: Gets all the known state keys for this virtual object instance. + * + * @return the immutable collection of known state keys. + */ + @org.jetbrains.annotations.ApiStatus.Experimental + Collection getAllKeys(); + + /** + * EXPERIMENTAL API: Clears all the state of this virtual object instance key-value state + * storage + * + * @throws IllegalStateException if called from a Shared handler + */ + @org.jetbrains.annotations.ApiStatus.Experimental + void clearAll(); + } + + private static final State STATE_INSTANCE = + new State() { + @Override + public Optional get(StateKey key) { + return ((SharedObjectContext) Context.current()).get(key); + } + + @Override + public void set(StateKey key, @NonNull T value) { + checkCanWriteState("set"); + ((ObjectContext) Context.current()).set(key, value); + } + + @Override + public void clear(StateKey key) { + checkCanWriteState("clear"); + ((ObjectContext) Context.current()).clear(key); + } + + @Override + public Collection getAllKeys() { + return ((SharedObjectContext) Context.current()).stateKeys(); + } + + @Override + public void clearAll() { + checkCanWriteState("clearAll"); + ((ObjectContext) Context.current()).clearAll(); + } + + private void checkCanWriteState(String opName) { + var handlerContext = HandlerRunner.getHandlerContext(); + if (!handlerContext.canWriteState()) { + throw new IllegalStateException( + "State." + + opName + + "() cannot be used in shared handlers. Check https://docs.restate.dev/develop/java/state for more details."); + } + } + }; + + /** + * EXPERIMENTAL API + * + * @return this Virtual Object/Workflow key + * @throws IllegalStateException if called from a regular Service handler. + */ + @org.jetbrains.annotations.ApiStatus.Experimental + public static String key() { + var handlerContext = HandlerRunner.getHandlerContext(); + + if (!handlerContext.canReadState()) { + throw new IllegalStateException( + "Restate.key() can be used only within Virtual Object or Workflow handlers. Check https://docs.restate.dev/develop/java/state for more details."); + } + + return ((SharedObjectContext) Context.current()).key(); + } + + /** + * EXPERIMENTAL API: Access to this Virtual Object/Workflow state. + * + * @return {@link State} for this Virtual Object/Workflow + * @throws IllegalStateException if called from a regular Service handler. + */ + @org.jetbrains.annotations.ApiStatus.Experimental + public static State state() { + var handlerContext = HandlerRunner.getHandlerContext(); + + if (!handlerContext.canReadState()) { + throw new IllegalStateException( + "Restate.state() can be used only within Virtual Object or Workflow handlers. Check https://docs.restate.dev/develop/java/state for more details."); + } + return STATE_INSTANCE; + } + + /** + * EXPERIMENTAL API: Create a {@link DurablePromise} for the given key. + * + *

You can use this feature to implement interaction between different workflow handlers, e.g. + * to send a signal from a shared handler to the workflow handler. + * + * @return the {@link DurablePromise}. + * @see DurablePromise + * @throws IllegalStateException if called from a non-Workflow handler + */ + @org.jetbrains.annotations.ApiStatus.Experimental + public static DurablePromise promise(DurablePromiseKey key) { + var handlerContext = HandlerRunner.getHandlerContext(); + + if (!handlerContext.canReadPromises() || !handlerContext.canWritePromises()) { + throw new IllegalStateException( + "Restate.promise(key) can be used only within Workflow handlers. Check https://docs.restate.dev/develop/java/external-events#durable-promises for more details."); + } + + SharedWorkflowContext ctx = (SharedWorkflowContext) Context.current(); + return ctx.promise(key); + } + + /** + * EXPERIMENTAL API: Create a new {@link DurablePromiseHandle} for the provided key. You + * can use it to {@link DurablePromiseHandle#resolve(Object)} or {@link + * DurablePromiseHandle#reject(String)} the given {@link DurablePromise}. + * + * @see DurablePromise + * @throws IllegalStateException if called from a non-Workflow handler + */ + @org.jetbrains.annotations.ApiStatus.Experimental + public static DurablePromiseHandle promiseHandle(DurablePromiseKey key) { + var handlerContext = HandlerRunner.getHandlerContext(); + + if (!handlerContext.canReadPromises() || !handlerContext.canWritePromises()) { + throw new IllegalStateException( + "Restate.promiseHandle(key) can be used only within Workflow handlers. Check https://docs.restate.dev/develop/java/external-events#durable-promises for more details."); + } + + SharedWorkflowContext ctx = (SharedWorkflowContext) Context.current(); + return ctx.promiseHandle(key); } } diff --git a/sdk-api/src/main/java/dev/restate/sdk/ServiceReference.java b/sdk-api/src/main/java/dev/restate/sdk/ServiceHandle.java similarity index 61% rename from sdk-api/src/main/java/dev/restate/sdk/ServiceReference.java rename to sdk-api/src/main/java/dev/restate/sdk/ServiceHandle.java index b43a6cbb..08d63fc9 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/ServiceReference.java +++ b/sdk-api/src/main/java/dev/restate/sdk/ServiceHandle.java @@ -19,45 +19,40 @@ * EXPERIMENTAL API: This interface is part of the new reflection-based API and may change in * future releases. * - *

A reference to a Restate service, virtual object, or workflow that can be invoked from within - * a handler. Provides three ways to invoke methods: + *

Advanced API handle for invoking Restate services, virtual objects, or workflows with full + * control. This handle provides advanced invocation capabilities including: * - *

{@code
- * // 1. Create a client proxy and call it directly
- * var greeterProxy = Restate.service(Greeter.class).client();
- * GreetingResponse response = greeterProxy.greet(new Greeting("Alice"));
+ * 
    + *
  • Composable futures for asynchronous request handling + *
  • Invocation options such as idempotency keys + *
  • Fire-and-forget requests via {@code send()} + *
  • Deferred response handling + *
+ * + *

Use this handle to perform requests with method references: * - * // 2. Use call() with method reference and await the result - * GreetingResponse response = Restate.service(Greeter.class) + *

{@code
+ * // 1. Use call() with method reference and await the result
+ * GreetingResponse response = Restate.serviceHandle(Greeter.class)
  *   .call(Greeter::greet, new Greeting("Alice"))
  *   .await();
  *
- * // 3. Use send() for one-way invocation without waiting
- * InvocationHandle handle = Restate.service(Greeter.class)
+ * // 2. Use send() for one-way invocation without waiting
+ * InvocationHandle handle = Restate.serviceHandle(Greeter.class)
  *   .send(Greeter::greet, new Greeting("Alice"));
  * }
* - *

Create instances using {@link Restate#service(Class)}, {@link Restate#virtualObject(Class, - * String)}, or {@link Restate#workflow(Class, String)}. + *

Create instances using {@link Restate#serviceHandle(Class)}, {@link + * Restate#virtualObjectHandle(Class, String)}, or {@link Restate#workflowHandle(Class, String)}. + * + *

For simple synchronous request-response interactions, consider using the simple proxy API + * instead: {@link Restate#service(Class)}, {@link Restate#virtualObject(Class, String)}, or {@link + * Restate#workflow(Class, String)}. * * @param the service interface type */ @org.jetbrains.annotations.ApiStatus.Experimental -public interface ServiceReference { - /** - * EXPERIMENTAL API: Get a client proxy to call methods directly. - * - *

{@code
-   * // Get a proxy and call methods on it
-   * var greeterProxy = Restate.service(Greeter.class).client();
-   * GreetingResponse response = greeterProxy.greet(new Greeting("Alice"));
-   * }
- * - * @return a proxy instance of the service interface - */ - @org.jetbrains.annotations.ApiStatus.Experimental - SVC client(); - +public interface ServiceHandle { /** * EXPERIMENTAL API: Invoke a service method with input and return a future for the result. * @@ -68,13 +63,13 @@ public interface ServiceReference { * .await(); * }
* - * @param s method reference (e.g., {@code Greeter::greet}) + * @param methodReference method reference (e.g., {@code Greeter::greet}) * @param input the input parameter to pass to the method * @return a {@link DurableFuture} wrapping the result */ @org.jetbrains.annotations.ApiStatus.Experimental - default DurableFuture call(BiFunction s, I input) { - return call(s, input, InvocationOptions.DEFAULT); + default DurableFuture call(BiFunction methodReference, I input) { + return call(methodReference, input, InvocationOptions.DEFAULT); } /** @@ -92,33 +87,35 @@ default DurableFuture call(BiFunction s, I input) { */ @org.jetbrains.annotations.ApiStatus.Experimental default DurableFuture call( - BiFunction s, I input, InvocationOptions.Builder options) { - return call(s, input, options.build()); + BiFunction methodReference, I input, InvocationOptions.Builder options) { + return call(methodReference, input, options.build()); } /** EXPERIMENTAL API: Like {@link #call(BiFunction, Object)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - DurableFuture call(BiFunction s, I input, InvocationOptions options); + DurableFuture call( + BiFunction methodReference, I input, InvocationOptions options); /** * EXPERIMENTAL API: Like {@link #call(BiFunction, Object)}, for methods without a return * value. */ @org.jetbrains.annotations.ApiStatus.Experimental - default DurableFuture call(BiConsumer s, I input) { - return call(s, input, InvocationOptions.DEFAULT); + default DurableFuture call(BiConsumer methodReference, I input) { + return call(methodReference, input, InvocationOptions.DEFAULT); } /** EXPERIMENTAL API: Like {@link #call(BiConsumer, Object)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental default DurableFuture call( - BiConsumer s, I input, InvocationOptions.Builder options) { - return call(s, input, options.build()); + BiConsumer methodReference, I input, InvocationOptions.Builder options) { + return call(methodReference, input, options.build()); } /** EXPERIMENTAL API: Like {@link #call(BiConsumer, Object)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - DurableFuture call(BiConsumer s, I input, InvocationOptions options); + DurableFuture call( + BiConsumer methodReference, I input, InvocationOptions options); /** * EXPERIMENTAL API: Invoke a service method without input and return a future for the @@ -132,38 +129,40 @@ default DurableFuture call( * } */ @org.jetbrains.annotations.ApiStatus.Experimental - default DurableFuture call(Function s) { - return call(s, InvocationOptions.DEFAULT); + default DurableFuture call(Function methodReference) { + return call(methodReference, InvocationOptions.DEFAULT); } /** EXPERIMENTAL API: Like {@link #call(Function)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - default DurableFuture call(Function s, InvocationOptions.Builder options) { - return call(s, options.build()); + default DurableFuture call( + Function methodReference, InvocationOptions.Builder options) { + return call(methodReference, options.build()); } /** EXPERIMENTAL API: Like {@link #call(Function)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - DurableFuture call(Function s, InvocationOptions options); + DurableFuture call(Function methodReference, InvocationOptions options); /** * EXPERIMENTAL API: Like {@link #call(BiFunction, Object)}, for methods without input or * return value. */ @org.jetbrains.annotations.ApiStatus.Experimental - default DurableFuture call(Consumer s) { - return call(s, InvocationOptions.DEFAULT); + default DurableFuture call(Consumer methodReference) { + return call(methodReference, InvocationOptions.DEFAULT); } /** EXPERIMENTAL API: Like {@link #call(Consumer)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - default DurableFuture call(Consumer s, InvocationOptions.Builder options) { - return call(s, options.build()); + default DurableFuture call( + Consumer methodReference, InvocationOptions.Builder options) { + return call(methodReference, options.build()); } /** EXPERIMENTAL API: Like {@link #call(Consumer)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - DurableFuture call(Consumer s, InvocationOptions options); + DurableFuture call(Consumer methodReference, InvocationOptions options); /** * EXPERIMENTAL API: Send a one-way invocation without waiting for the response. @@ -179,33 +178,34 @@ default DurableFuture call(Consumer s, InvocationOptions.Builder opti * .send(Greeter::greet, new Greeting("Alice"), Duration.ofMinutes(5)); * } * - * @param s method reference (e.g., {@code Greeter::greet}) + * @param methodReference method reference (e.g., {@code Greeter::greet}) * @param input the input parameter to pass to the method * @return an {@link InvocationHandle} for the invocation */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(BiFunction s, I input) { - return send(s, input, InvocationOptions.DEFAULT); + default InvocationHandle send(BiFunction methodReference, I input) { + return send(methodReference, input, InvocationOptions.DEFAULT); } /** EXPERIMENTAL API: Like {@link #send(BiFunction, Object)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - BiFunction s, I input, InvocationOptions.Builder options) { - return send(s, input, options.build()); + BiFunction methodReference, I input, InvocationOptions.Builder options) { + return send(methodReference, input, options.build()); } /** EXPERIMENTAL API: Like {@link #send(BiFunction, Object)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - BiFunction s, I input, InvocationOptions options) { - return send(s, input, null, options); + BiFunction methodReference, I input, InvocationOptions options) { + return send(methodReference, input, null, options); } /** EXPERIMENTAL API: Like {@link #send(BiFunction, Object)}, with a delay. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(BiFunction s, I input, Duration delay) { - return send(s, input, delay, InvocationOptions.DEFAULT); + default InvocationHandle send( + BiFunction methodReference, I input, Duration delay) { + return send(methodReference, input, delay, InvocationOptions.DEFAULT); } /** @@ -214,8 +214,11 @@ default InvocationHandle send(BiFunction s, I input, Durati */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - BiFunction s, I input, Duration delay, InvocationOptions.Builder options) { - return send(s, input, delay, options.build()); + BiFunction methodReference, + I input, + Duration delay, + InvocationOptions.Builder options) { + return send(methodReference, input, delay, options.build()); } /** @@ -224,35 +227,36 @@ default InvocationHandle send( */ @org.jetbrains.annotations.ApiStatus.Experimental InvocationHandle send( - BiFunction s, I input, Duration delay, InvocationOptions options); + BiFunction methodReference, I input, Duration delay, InvocationOptions options); /** * EXPERIMENTAL API: Like {@link #send(BiFunction, Object)}, for methods without a return * value. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(BiConsumer s, I input) { - return send(s, input, InvocationOptions.DEFAULT); + default InvocationHandle send(BiConsumer methodReference, I input) { + return send(methodReference, input, InvocationOptions.DEFAULT); } /** EXPERIMENTAL API: Like {@link #send(BiConsumer, Object)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - BiConsumer s, I input, InvocationOptions.Builder options) { - return send(s, input, options.build()); + BiConsumer methodReference, I input, InvocationOptions.Builder options) { + return send(methodReference, input, options.build()); } /** EXPERIMENTAL API: Like {@link #send(BiConsumer, Object)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - BiConsumer s, I input, InvocationOptions options) { - return send(s, input, null, options); + BiConsumer methodReference, I input, InvocationOptions options) { + return send(methodReference, input, null, options); } /** EXPERIMENTAL API: Like {@link #send(BiConsumer, Object)}, with a delay. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(BiConsumer s, I input, Duration delay) { - return send(s, input, delay, InvocationOptions.DEFAULT); + default InvocationHandle send( + BiConsumer methodReference, I input, Duration delay) { + return send(methodReference, input, delay, InvocationOptions.DEFAULT); } /** @@ -261,8 +265,11 @@ default InvocationHandle send(BiConsumer s, I input, Duration */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - BiConsumer s, I input, Duration delay, InvocationOptions.Builder options) { - return send(s, input, delay, options.build()); + BiConsumer methodReference, + I input, + Duration delay, + InvocationOptions.Builder options) { + return send(methodReference, input, delay, options.build()); } /** @@ -271,7 +278,7 @@ default InvocationHandle send( */ @org.jetbrains.annotations.ApiStatus.Experimental InvocationHandle send( - BiConsumer s, I input, Duration delay, InvocationOptions options); + BiConsumer methodReference, I input, Duration delay, InvocationOptions options); /** EXPERIMENTAL API: Like {@link #send(BiFunction, Object)}, for methods without input. */ @org.jetbrains.annotations.ApiStatus.Experimental @@ -281,8 +288,9 @@ default InvocationHandle send(Function s) { /** EXPERIMENTAL API: Like {@link #send(Function)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(Function s, InvocationOptions.Builder options) { - return send(s, options.build()); + default InvocationHandle send( + Function methodReference, InvocationOptions.Builder options) { + return send(methodReference, options.build()); } /** EXPERIMENTAL API: Like {@link #send(Function)}, with invocation options. */ @@ -300,49 +308,52 @@ default InvocationHandle send(Function s, Duration delay) { /** EXPERIMENTAL API: Like {@link #send(Function)}, with a delay and invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - Function s, Duration delay, InvocationOptions.Builder options) { - return send(s, delay, options.build()); + Function methodReference, Duration delay, InvocationOptions.Builder options) { + return send(methodReference, delay, options.build()); } /** EXPERIMENTAL API: Like {@link #send(Function)}, with a delay and invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - InvocationHandle send(Function s, Duration delay, InvocationOptions options); + InvocationHandle send( + Function methodReference, Duration delay, InvocationOptions options); /** * EXPERIMENTAL API: Like {@link #send(BiFunction, Object)}, for methods without input or * return value. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(Consumer s) { - return send(s, InvocationOptions.DEFAULT); + default InvocationHandle send(Consumer methodReference) { + return send(methodReference, InvocationOptions.DEFAULT); } /** EXPERIMENTAL API: Like {@link #send(Consumer)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(Consumer s, InvocationOptions.Builder options) { - return send(s, options.build()); + default InvocationHandle send( + Consumer methodReference, InvocationOptions.Builder options) { + return send(methodReference, options.build()); } /** EXPERIMENTAL API: Like {@link #send(Consumer)}, with invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(Consumer s, InvocationOptions options) { - return send(s, null, options); + default InvocationHandle send(Consumer methodReference, InvocationOptions options) { + return send(methodReference, null, options); } /** EXPERIMENTAL API: Like {@link #send(Consumer)}, with a delay. */ @org.jetbrains.annotations.ApiStatus.Experimental - default InvocationHandle send(Consumer s, Duration delay) { - return send(s, delay, InvocationOptions.DEFAULT); + default InvocationHandle send(Consumer methodReference, Duration delay) { + return send(methodReference, delay, InvocationOptions.DEFAULT); } /** EXPERIMENTAL API: Like {@link #send(Consumer)}, with a delay and invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental default InvocationHandle send( - Consumer s, Duration delay, InvocationOptions.Builder options) { - return send(s, delay, options.build()); + Consumer methodReference, Duration delay, InvocationOptions.Builder options) { + return send(methodReference, delay, options.build()); } /** EXPERIMENTAL API: Like {@link #send(Consumer)}, with a delay and invocation options. */ @org.jetbrains.annotations.ApiStatus.Experimental - InvocationHandle send(Consumer s, Duration delay, InvocationOptions options); + InvocationHandle send( + Consumer methodReference, Duration delay, InvocationOptions options); } diff --git a/sdk-api/src/main/java/dev/restate/sdk/ServiceReferenceImpl.java b/sdk-api/src/main/java/dev/restate/sdk/ServiceHandleImpl.java similarity index 61% rename from sdk-api/src/main/java/dev/restate/sdk/ServiceReferenceImpl.java rename to sdk-api/src/main/java/dev/restate/sdk/ServiceHandleImpl.java index bada53cc..7cf480e2 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/ServiceReferenceImpl.java +++ b/sdk-api/src/main/java/dev/restate/sdk/ServiceHandleImpl.java @@ -11,8 +11,6 @@ import static dev.restate.common.reflections.RestateUtils.toRequest; import dev.restate.common.InvocationOptions; -import dev.restate.common.Request; -import dev.restate.common.Target; import dev.restate.common.reflections.*; import dev.restate.serde.Serde; import dev.restate.serde.TypeTag; @@ -23,56 +21,27 @@ import java.util.function.Function; import org.jspecify.annotations.Nullable; -final class ServiceReferenceImpl implements ServiceReference { +final class ServiceHandleImpl implements ServiceHandle { private final Class clazz; private final String serviceName; private final @Nullable String key; - // The simple proxy for users - private SVC proxyClient; - // To use call/send private MethodInfoCollector methodInfoCollector; - ServiceReferenceImpl(Class clazz, @Nullable String key) { + ServiceHandleImpl(Class clazz, @Nullable String key) { this.clazz = clazz; this.serviceName = ReflectionUtils.extractServiceName(clazz); this.key = key; } - @Override - public SVC client() { - if (proxyClient == null) { - this.proxyClient = - ProxySupport.createProxy( - clazz, - invocation -> { - var methodInfo = MethodInfo.fromMethod(invocation.getMethod()); - - //noinspection unchecked - return Restate.context() - .call( - Request.of( - Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), - (TypeTag) - RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) - RestateUtils.typeTag(methodInfo.getOutputType()), - invocation.getArguments().length == 0 - ? null - : invocation.getArguments()[0])) - .await(); - }); - } - return this.proxyClient; - } - @SuppressWarnings("unchecked") @Override - public DurableFuture call(BiFunction s, I input, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s, input); - return Restate.context() + public DurableFuture call( + BiFunction methodReference, I input, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference, input); + return Context.current() .call( toRequest( serviceName, @@ -86,9 +55,10 @@ public DurableFuture call(BiFunction s, I input, Invocation @SuppressWarnings("unchecked") @Override - public DurableFuture call(BiConsumer s, I input, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s, input); - return Restate.context() + public DurableFuture call( + BiConsumer methodReference, I input, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference, input); + return Context.current() .call( toRequest( serviceName, @@ -102,9 +72,9 @@ public DurableFuture call(BiConsumer s, I input, InvocationOpt @SuppressWarnings("unchecked") @Override - public DurableFuture call(Function s, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s); - return Restate.context() + public DurableFuture call(Function methodReference, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference); + return Context.current() .call( toRequest( serviceName, @@ -117,9 +87,9 @@ public DurableFuture call(Function s, InvocationOptions options) } @Override - public DurableFuture call(Consumer s, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s); - return Restate.context() + public DurableFuture call(Consumer methodReference, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference); + return Context.current() .call( toRequest( serviceName, @@ -134,9 +104,9 @@ public DurableFuture call(Consumer s, InvocationOptions options) { @SuppressWarnings("unchecked") @Override public InvocationHandle send( - BiFunction s, I input, Duration delay, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s, input); - return Restate.context() + BiFunction methodReference, I input, Duration delay, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference, input); + return Context.current() .send( toRequest( serviceName, @@ -152,9 +122,9 @@ public InvocationHandle send( @SuppressWarnings("unchecked") @Override public InvocationHandle send( - BiConsumer s, I input, Duration delay, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s, input); - return Restate.context() + BiConsumer methodReference, I input, Duration delay, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference, input); + return Context.current() .send( toRequest( serviceName, @@ -170,9 +140,9 @@ public InvocationHandle send( @SuppressWarnings("unchecked") @Override public InvocationHandle send( - Function s, Duration delay, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s); - return Restate.context() + Function methodReference, Duration delay, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference); + return Context.current() .send( toRequest( serviceName, @@ -186,9 +156,10 @@ public InvocationHandle send( } @Override - public InvocationHandle send(Consumer s, Duration delay, InvocationOptions options) { - MethodInfo methodInfo = getMethodInfoCollector().resolve(s); - return Restate.context() + public InvocationHandle send( + Consumer methodReference, Duration delay, InvocationOptions options) { + MethodInfo methodInfo = getMethodInfoCollector().resolve(methodReference); + return Context.current() .send( toRequest( serviceName, From 8999d6e558e406458437b58ac6a0977a6634f09c Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Mon, 12 Jan 2026 19:53:23 +0100 Subject: [PATCH 2/2] Add some initial tests, fix @Raw annotation usage with reflection api spotless --- .../main/java/dev/restate/client/Client.java | 13 +- .../client/ClientServiceHandleImpl.java | 16 +- .../common/reflections/MethodInfo.java | 39 ++++- .../reflections/MethodInfoCollector.java | 16 +- .../common/reflections/ReflectionUtils.java | 13 +- .../my/restate/sdk/examples/LoanWorkflow.java | 14 +- .../main/java/dev/restate/sdk/Restate.java | 13 +- .../dev/restate/sdk/ServiceHandleImpl.java | 16 +- .../ReflectionServiceDefinitionFactory.java | 128 ++++++++++++++-- sdk-core/build.gradle.kts | 23 ++- .../sdk/core/javaapi/JavaAPITests.java | 4 +- .../sdk/core/javaapi/MySerdeFactory.java | 2 +- .../javaapi/reflections/CheckedException.java | 21 +++ .../core/javaapi/reflections/CustomSerde.java | 26 ++++ .../sdk/core/javaapi/reflections/Empty.java | 34 +++++ .../javaapi/reflections/GreeterInterface.java | 20 +++ .../core/javaapi/reflections/MyWorkflow.java | 30 ++++ .../javaapi/reflections/ObjectGreeter.java | 26 ++++ ...ObjectGreeterImplementedFromInterface.java | 16 ++ .../javaapi/reflections/PrimitiveTypes.java | 29 ++++ .../javaapi/reflections/RawInputOutput.java | 45 ++++++ .../reflections/ReflectionDiscoveryTest.java | 88 +++++++++++ .../javaapi/reflections/ReflectionTest.java | 143 ++++++++++++++++++ .../javaapi/reflections/ServiceGreeter.java | 20 +++ .../java/SdkTestingIntegrationTest.java | 2 +- 25 files changed, 727 insertions(+), 70 deletions(-) create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CheckedException.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CustomSerde.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/Empty.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/GreeterInterface.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/MyWorkflow.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeter.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeterImplementedFromInterface.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/PrimitiveTypes.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/RawInputOutput.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionDiscoveryTest.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionTest.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ServiceGreeter.java diff --git a/client/src/main/java/dev/restate/client/Client.java b/client/src/main/java/dev/restate/client/Client.java index ecd7555c..cf3145b7 100644 --- a/client/src/main/java/dev/restate/client/Client.java +++ b/client/src/main/java/dev/restate/client/Client.java @@ -17,7 +17,6 @@ import dev.restate.common.reflections.MethodInfo; import dev.restate.common.reflections.ProxySupport; import dev.restate.common.reflections.ReflectionUtils; -import dev.restate.common.reflections.RestateUtils; import dev.restate.sdk.annotation.Service; import dev.restate.sdk.annotation.VirtualObject; import dev.restate.sdk.annotation.Workflow; @@ -568,8 +567,8 @@ default SVC service(Class clazz) { return this.call( Request.of( Target.virtualObject(serviceName, null, methodInfo.getHandlerName()), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) .response(); }); @@ -647,8 +646,8 @@ default SVC virtualObject(Class clazz, String key) { return this.call( Request.of( Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) .response(); }); @@ -727,8 +726,8 @@ default SVC workflow(Class clazz, String key) { return this.call( Request.of( Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) .response(); }); diff --git a/client/src/main/java/dev/restate/client/ClientServiceHandleImpl.java b/client/src/main/java/dev/restate/client/ClientServiceHandleImpl.java index 67cf3ce2..6fc12237 100644 --- a/client/src/main/java/dev/restate/client/ClientServiceHandleImpl.java +++ b/client/src/main/java/dev/restate/client/ClientServiceHandleImpl.java @@ -49,8 +49,8 @@ public CompletableFuture> callAsync( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), input, invocationOptions)); } @@ -65,7 +65,7 @@ public CompletableFuture> callAsync( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) methodInfo.getInputType(), Serde.VOID, input, invocationOptions)); @@ -82,7 +82,7 @@ public CompletableFuture> callAsync( key, methodInfo.getHandlerName(), Serde.VOID, - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getOutputType(), null, invocationOptions)); } @@ -112,8 +112,8 @@ public CompletableFuture> sendAsync( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), input, invocationOptions), delay); @@ -129,7 +129,7 @@ public CompletableFuture> sendAsync( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) methodInfo.getInputType(), Serde.VOID, input, invocationOptions), @@ -147,7 +147,7 @@ public CompletableFuture> sendAsync( key, methodInfo.getHandlerName(), Serde.VOID, - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getOutputType(), null, invocationOptions), delay); diff --git a/common/src/main/java/dev/restate/common/reflections/MethodInfo.java b/common/src/main/java/dev/restate/common/reflections/MethodInfo.java index e2598e2f..26cb8c98 100644 --- a/common/src/main/java/dev/restate/common/reflections/MethodInfo.java +++ b/common/src/main/java/dev/restate/common/reflections/MethodInfo.java @@ -8,15 +8,19 @@ // https://github.com/restatedev/sdk-java/blob/main/LICENSE package dev.restate.common.reflections; +import dev.restate.sdk.annotation.Raw; +import dev.restate.serde.Serde; +import dev.restate.serde.TypeTag; import java.lang.reflect.Method; import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; public class MethodInfo extends RuntimeException { private final String handlerName; - private final Type inputType; - private final Type outputType; + private final TypeTag inputType; + private final TypeTag outputType; - private MethodInfo(String handlerName, Type inputType, Type outputType) { + private MethodInfo(String handlerName, TypeTag inputType, TypeTag outputType) { this.inputType = inputType; this.outputType = outputType; this.handlerName = handlerName; @@ -26,21 +30,40 @@ public String getHandlerName() { return handlerName; } - public Type getInputType() { + public TypeTag getInputType() { return inputType; } - public Type getOutputType() { + public TypeTag getOutputType() { return outputType; } public static MethodInfo fromMethod(Method method) { var handlerInfo = ReflectionUtils.mustHaveHandlerAnnotation(method); var genericParameters = method.getGenericParameterTypes(); - var inputType = genericParameters.length == 0 ? Void.TYPE : genericParameters[0]; - var outputType = method.getGenericReturnType(); var handlerName = handlerInfo.name(); + TypeTag inputTypeTag = + genericParameters.length == 0 + ? Serde.VOID + : resolveTypeTag( + genericParameters[0], method.getParameters()[0].getAnnotation(Raw.class)); + TypeTag outputTypeTag = + resolveTypeTag(method.getGenericReturnType(), method.getAnnotation(Raw.class)); - return new MethodInfo(handlerName, inputType, outputType); + return new MethodInfo(handlerName, inputTypeTag, outputTypeTag); + } + + private static TypeTag resolveTypeTag(@Nullable Type type, @Nullable Raw rawAnnotation) { + if (type == null) { + return Serde.VOID; + } + + if (rawAnnotation != null && !rawAnnotation.contentType().equals("application/octet-stream")) { + return Serde.withContentType(rawAnnotation.contentType(), Serde.RAW); + } else if (rawAnnotation != null) { + return Serde.RAW; + } else { + return RestateUtils.typeTag(type); + } } } diff --git a/common/src/main/java/dev/restate/common/reflections/MethodInfoCollector.java b/common/src/main/java/dev/restate/common/reflections/MethodInfoCollector.java index 797e92a7..de2384c3 100644 --- a/common/src/main/java/dev/restate/common/reflections/MethodInfoCollector.java +++ b/common/src/main/java/dev/restate/common/reflections/MethodInfoCollector.java @@ -21,9 +21,9 @@ public MethodInfoCollector(Class svcClass) { this.infoCollectorProxy = ProxySupport.createProxy(svcClass, METHOD_INFO_COLLECTOR_INTERCEPTOR); } - public MethodInfo resolve(Function s) { + public MethodInfo resolve(Function methodReference) { try { - s.apply(this.infoCollectorProxy); + methodReference.apply(this.infoCollectorProxy); throw new UnsupportedOperationException( "The provided lambda MUST contain ONLY a method reference to the service method"); } catch (MethodInfo e) { @@ -31,9 +31,9 @@ public MethodInfo resolve(Function s) { } } - public MethodInfo resolve(BiFunction s, I input) { + public MethodInfo resolve(BiFunction methodReference, I input) { try { - s.apply(this.infoCollectorProxy, input); + methodReference.apply(this.infoCollectorProxy, input); throw new UnsupportedOperationException( "The provided lambda MUST contain ONLY a method reference to the service method"); } catch (MethodInfo e) { @@ -41,9 +41,9 @@ public MethodInfo resolve(BiFunction s, I input) { } } - public MethodInfo resolve(BiConsumer s, I input) { + public MethodInfo resolve(BiConsumer methodReference, I input) { try { - s.accept(this.infoCollectorProxy, input); + methodReference.accept(this.infoCollectorProxy, input); throw new UnsupportedOperationException( "The provided lambda MUST contain ONLY a method reference to a service method"); } catch (MethodInfo e) { @@ -51,9 +51,9 @@ public MethodInfo resolve(BiConsumer s, I input) { } } - public MethodInfo resolve(Consumer s) { + public MethodInfo resolve(Consumer methodReference) { try { - s.accept(this.infoCollectorProxy); + methodReference.accept(this.infoCollectorProxy); throw new UnsupportedOperationException( "The provided lambda MUST contain ONLY a method reference to a service method"); } catch (MethodInfo e) { diff --git a/common/src/main/java/dev/restate/common/reflections/ReflectionUtils.java b/common/src/main/java/dev/restate/common/reflections/ReflectionUtils.java index 1270a61a..8c118b63 100644 --- a/common/src/main/java/dev/restate/common/reflections/ReflectionUtils.java +++ b/common/src/main/java/dev/restate/common/reflections/ReflectionUtils.java @@ -217,15 +217,24 @@ public static HandlerInfo mustHaveHandlerAnnotation(@NonNull Method method) { // Check for @Handler or @Shared annotation (Shared implies Handler) var handlerAnnotation = findAnnotation(method, Handler.class); var sharedAnnotation = findAnnotation(method, Shared.class); + var exclusiveAnnotation = findAnnotation(method, Exclusive.class); + var workflowAnnotation = findAnnotation(method, Workflow.class); - if (handlerAnnotation == null && sharedAnnotation == null) { + if (handlerAnnotation == null + && sharedAnnotation == null + && workflowAnnotation == null + && exclusiveAnnotation == null) { throw new IllegalArgumentException( "The invoked method '" + method.getName() + "' is not annotated with @" + Handler.class.getSimpleName() + " or @" - + Shared.class.getSimpleName()); + + Shared.class.getSimpleName() + + " or @" + + Exclusive.class.getSimpleName() + + " or @" + + Workflow.class.getSimpleName()); } // Extract the name from @Name annotation, or default to method name diff --git a/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java b/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java index a8a3677b..ab8f0a8c 100644 --- a/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java +++ b/examples/src/main/java/my/restate/sdk/examples/LoanWorkflow.java @@ -9,7 +9,6 @@ package my.restate.sdk.examples; import dev.restate.client.Client; -import dev.restate.client.ClientServiceReference; import dev.restate.sdk.*; import dev.restate.sdk.annotation.Handler; import dev.restate.sdk.annotation.Service; @@ -143,13 +142,14 @@ public static void main(String[] args) { // To invoke the workflow: Client restateClient = Client.connect("http://127.0.0.1:8080"); - LoanWorkflow loanWorkflow = - restateClient.workflow(LoanWorkflow.class, "my-loan"); + LoanWorkflow loanWorkflow = restateClient.workflow(LoanWorkflow.class, "my-loan"); var handle = - restateClient.workflowHandle(LoanWorkflow.class, "my-loan").send( - LoanWorkflow::run, - new LoanRequest( - "Francesco", "slinkydeveloper", "DE1234", new BigDecimal("1000000000"))); + restateClient + .workflowHandle(LoanWorkflow.class, "my-loan") + .send( + LoanWorkflow::run, + new LoanRequest( + "Francesco", "slinkydeveloper", "DE1234", new BigDecimal("1000000000"))); LOG.info("Started loan workflow"); diff --git a/sdk-api/src/main/java/dev/restate/sdk/Restate.java b/sdk-api/src/main/java/dev/restate/sdk/Restate.java index f8e623a5..6286ecd8 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/Restate.java +++ b/sdk-api/src/main/java/dev/restate/sdk/Restate.java @@ -18,7 +18,6 @@ import dev.restate.common.reflections.MethodInfo; import dev.restate.common.reflections.ProxySupport; import dev.restate.common.reflections.ReflectionUtils; -import dev.restate.common.reflections.RestateUtils; import dev.restate.sdk.annotation.Service; import dev.restate.sdk.annotation.VirtualObject; import dev.restate.sdk.annotation.Workflow; @@ -443,8 +442,8 @@ public static SVC service(Class clazz) { .call( Request.of( Target.virtualObject(serviceName, null, methodInfo.getHandlerName()), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) .await(); }); @@ -519,8 +518,8 @@ public static SVC virtualObject(Class clazz, String key) { .call( Request.of( Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) .await(); }); @@ -596,8 +595,8 @@ public static SVC workflow(Class clazz, String key) { .call( Request.of( Target.virtualObject(serviceName, key, methodInfo.getHandlerName()), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), invocation.getArguments().length == 0 ? null : invocation.getArguments()[0])) .await(); }); diff --git a/sdk-api/src/main/java/dev/restate/sdk/ServiceHandleImpl.java b/sdk-api/src/main/java/dev/restate/sdk/ServiceHandleImpl.java index 7cf480e2..f4d8eccd 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/ServiceHandleImpl.java +++ b/sdk-api/src/main/java/dev/restate/sdk/ServiceHandleImpl.java @@ -47,8 +47,8 @@ public DurableFuture call( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), input, options)); } @@ -64,7 +64,7 @@ public DurableFuture call( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) methodInfo.getInputType(), Serde.VOID, input, options)); @@ -81,7 +81,7 @@ public DurableFuture call(Function methodReference, InvocationOpt key, methodInfo.getHandlerName(), Serde.VOID, - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getOutputType(), null, options)); } @@ -112,8 +112,8 @@ public InvocationHandle send( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getInputType(), + (TypeTag) methodInfo.getOutputType(), input, options), delay); @@ -130,7 +130,7 @@ public InvocationHandle send( serviceName, key, methodInfo.getHandlerName(), - (TypeTag) RestateUtils.typeTag(methodInfo.getInputType()), + (TypeTag) methodInfo.getInputType(), Serde.VOID, input, options), @@ -149,7 +149,7 @@ public InvocationHandle send( key, methodInfo.getHandlerName(), Serde.VOID, - (TypeTag) RestateUtils.typeTag(methodInfo.getOutputType()), + (TypeTag) methodInfo.getOutputType(), null, options), delay); diff --git a/sdk-api/src/main/java/dev/restate/sdk/internal/ReflectionServiceDefinitionFactory.java b/sdk-api/src/main/java/dev/restate/sdk/internal/ReflectionServiceDefinitionFactory.java index 537adffc..6b716b36 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/internal/ReflectionServiceDefinitionFactory.java +++ b/sdk-api/src/main/java/dev/restate/sdk/internal/ReflectionServiceDefinitionFactory.java @@ -85,7 +85,9 @@ public ServiceDefinition create( serviceClazz, method -> ReflectionUtils.findAnnotation(method, Handler.class) != null - || ReflectionUtils.findAnnotation(method, Shared.class) != null); + || ReflectionUtils.findAnnotation(method, Shared.class) != null + || ReflectionUtils.findAnnotation(method, Workflow.class) != null + || ReflectionUtils.findAnnotation(method, Exclusive.class) != null); if (methods.length == 0) { throw new MalformedRestateServiceException(serviceName, "No @Handler method found"); } @@ -95,7 +97,7 @@ public ServiceDefinition create( Arrays.stream(methods) .map( method -> - createHandlerDefinition( + this.createHandlerDefinition( serviceInstance, method, serviceName, @@ -172,8 +174,6 @@ public ServiceDefinition create( + method.getName() + ". Only zero or one parameter is supported."); } - var inputType = genericParameterTypes.length == 0 ? Void.TYPE : genericParameterTypes[0]; - var outputType = method.getGenericReturnType(); if (serviceType == ServiceType.SERVICE && handlerInfo.shared()) { throw new MalformedRestateServiceException( @@ -186,6 +186,9 @@ public ServiceDefinition create( ? HandlerType.EXCLUSIVE : serviceType == ServiceType.WORKFLOW ? HandlerType.WORKFLOW : null; + Serde inputSerde = resolveInputSerde(method, serdeFactory, serviceName); + Serde outputSerde = resolveOutputSerde(method, serdeFactory, serviceName); + var runner = dev.restate.sdk.HandlerRunner.of( (ctx, in) -> { @@ -202,13 +205,116 @@ public ServiceDefinition create( serdeFactory, overrideHandlerOptions); - //noinspection unchecked - return HandlerDefinition.of( - handlerName, - handlerType, - (Serde) serdeFactory.create(RestateUtils.typeTag(inputType)), - (Serde) serdeFactory.create(RestateUtils.typeTag(outputType)), - runner); + var handlerDefinition = + HandlerDefinition.of(handlerName, handlerType, inputSerde, outputSerde, runner); + + // Look for the accept annotation + if (parameterCount > 0) { + Accept acceptAnnotation = method.getParameters()[0].getAnnotation(Accept.class); + if (acceptAnnotation != null) { + handlerDefinition = handlerDefinition.withAcceptContentType(acceptAnnotation.value()); + } + } + + return handlerDefinition; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Serde resolveInputSerde( + Method method, SerdeFactory serdeFactory, String serviceName) { + if (method.getParameterCount() == 0) { + return (Serde) Serde.VOID; + } + + var inputType = method.getGenericParameterTypes()[0]; + + var parameter = method.getParameters()[0]; + Raw rawAnnotation = parameter.getAnnotation(Raw.class); + Json jsonAnnotation = parameter.getAnnotation(Json.class); + + // Validate annotations + if (rawAnnotation != null && jsonAnnotation != null) { + throw new MalformedRestateServiceException( + serviceName, + "Parameter in method " + + method.getName() + + " cannot be annotated with both @Raw and @Json"); + } + + if (rawAnnotation != null) { + // Validate parameter type is byte[] + if (!inputType.equals(byte[].class)) { + throw new MalformedRestateServiceException( + serviceName, + "Parameter annotated with @Raw in method " + + method.getName() + + " MUST be of type byte[], was " + + inputType); + } + Serde serde = Serde.RAW; + // Apply content type if not default + if (!rawAnnotation.contentType().equals("application/octet-stream")) { + serde = Serde.withContentType(rawAnnotation.contentType(), serde); + } + return serde; + } + + // Use serdeFactory to create serde + Serde serde = (Serde) serdeFactory.create(RestateUtils.typeTag(inputType)); + + // Apply custom content-type from @Json if present + if (jsonAnnotation != null && !jsonAnnotation.contentType().equals("application/json")) { + serde = Serde.withContentType(jsonAnnotation.contentType(), serde); + } + + return serde; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Serde resolveOutputSerde( + Method method, SerdeFactory serdeFactory, String serviceName) { + var outputType = method.getGenericReturnType(); + if (outputType.equals(Void.TYPE)) { + return (Serde) Serde.VOID; + } + + Raw rawAnnotation = method.getAnnotation(Raw.class); + Json jsonAnnotation = method.getAnnotation(Json.class); + + // Validate annotations + if (rawAnnotation != null && jsonAnnotation != null) { + throw new MalformedRestateServiceException( + serviceName, + "Method " + method.getName() + " cannot be annotated with both @Raw and @Json"); + } + + if (rawAnnotation != null) { + // Validate return type is byte[] + if (!outputType.equals(byte[].class)) { + throw new MalformedRestateServiceException( + serviceName, + "Method " + + method.getName() + + " annotated with @Raw MUST return byte[], was " + + outputType); + } + Serde serde = Serde.RAW; + // Apply content type if not default + if (!rawAnnotation.contentType().equals("application/octet-stream")) { + serde = Serde.withContentType(rawAnnotation.contentType(), serde); + } + return serde; + } + + // Use serdeFactory to create serde + Serde serde = (Serde) serdeFactory.create(RestateUtils.typeTag(outputType)); + + // Apply custom content-type from @Json if present + if (jsonAnnotation != null && !jsonAnnotation.contentType().equals("application/json")) { + serde = Serde.withContentType(jsonAnnotation.contentType(), serde); + } + + return serde; } private SerdeFactory resolveSerdeFactory(Class serviceClazz) { diff --git a/sdk-core/build.gradle.kts b/sdk-core/build.gradle.kts index 23935795..07a98f3f 100644 --- a/sdk-core/build.gradle.kts +++ b/sdk-core/build.gradle.kts @@ -101,7 +101,28 @@ protobuf { protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" } } // Make sure task dependencies are correct tasks { - withType { dependsOn(generateJsonSchema2Pojo, generateProto) } + withType { + dependsOn(generateJsonSchema2Pojo, generateProto) + + val disabledClassesCodegen = + listOf( + "dev.restate.sdk.core.javaapi.reflections.CheckedException", + "dev.restate.sdk.core.javaapi.reflections.CustomSerde", + "dev.restate.sdk.core.javaapi.reflections.Empty", + "dev.restate.sdk.core.javaapi.reflections.GreeterInterface", + "dev.restate.sdk.core.javaapi.reflections.MyWorkflow", + "dev.restate.sdk.core.javaapi.reflections.ObjectGreeter", + "dev.restate.sdk.core.javaapi.reflections.ObjectGreeterImplementedFromInterface", + "dev.restate.sdk.core.javaapi.reflections.PrimitiveTypes", + "dev.restate.sdk.core.javaapi.reflections.RawInputOutput", + "dev.restate.sdk.core.javaapi.reflections.ServiceGreeter") + + options.compilerArgs.addAll( + listOf( + "-parameters", + "-Adev.restate.codegen.disabledClasses=${disabledClassesCodegen.joinToString(",")}", + )) + } withType().configureEach { dependsOn(generateJsonSchema2Pojo, generateProto) } withType().configureEach { dependsOn(generateJsonSchema2Pojo, generateProto) diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/JavaAPITests.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/JavaAPITests.java index 37bc2ee2..1d6ef41c 100644 --- a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/JavaAPITests.java +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/JavaAPITests.java @@ -17,6 +17,7 @@ import dev.restate.sdk.core.TestDefinitions.TestExecutor; import dev.restate.sdk.core.TestDefinitions.TestInvocationBuilder; import dev.restate.sdk.core.TestDefinitions.TestSuite; +import dev.restate.sdk.core.javaapi.reflections.ReflectionTest; import dev.restate.sdk.endpoint.definition.HandlerDefinition; import dev.restate.sdk.endpoint.definition.HandlerType; import dev.restate.sdk.endpoint.definition.ServiceDefinition; @@ -49,7 +50,8 @@ public Stream definitions() { new StateMachineFailuresTest(), new UserFailuresTest(), new RandomTest(), - new CodegenTest()); + new CodegenTest(), + new ReflectionTest()); } public static TestInvocationBuilder testDefinitionForService( diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/MySerdeFactory.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/MySerdeFactory.java index 9bdef03a..8d7dbd53 100644 --- a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/MySerdeFactory.java +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/MySerdeFactory.java @@ -18,7 +18,7 @@ @SuppressWarnings("unchecked") public class MySerdeFactory implements SerdeFactory { - static Serde SERDE = + public static Serde SERDE = Serde.using( "mycontent/type", s -> s.toUpperCase().getBytes(), diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CheckedException.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CheckedException.java new file mode 100644 index 00000000..ecc486c9 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CheckedException.java @@ -0,0 +1,21 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Service; +import java.io.IOException; + +@Service +public class CheckedException { + @Handler + public String greet(String request) throws IOException { + return request; + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CustomSerde.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CustomSerde.java new file mode 100644 index 00000000..d0b41081 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/CustomSerde.java @@ -0,0 +1,26 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.restate.sdk.annotation.CustomSerdeFactory; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Service; +import dev.restate.sdk.core.javaapi.MySerdeFactory; + +@Service +@CustomSerdeFactory(MySerdeFactory.class) +public class CustomSerde { + @Handler + public String greet(String request) { + assertThat(request).isEqualTo("INPUT"); + return "output"; + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/Empty.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/Empty.java new file mode 100644 index 00000000..8585b404 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/Empty.java @@ -0,0 +1,34 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.Restate; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Name; +import dev.restate.sdk.annotation.Service; + +@Service +@Name("Empty") +public class Empty { + + @Handler + public String emptyInput() { + return Restate.service(Empty.class).emptyInput(); + } + + @Handler + public void emptyOutput(String request) { + Restate.service(Empty.class).emptyOutput(request); + } + + @Handler + public void emptyInputOutput() { + Restate.service(Empty.class).emptyInputOutput(); + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/GreeterInterface.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/GreeterInterface.java new file mode 100644 index 00000000..8f9d267b --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/GreeterInterface.java @@ -0,0 +1,20 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Name; +import dev.restate.sdk.annotation.VirtualObject; + +@VirtualObject +@Name("GreeterInterface") +public interface GreeterInterface { + @Handler + String greet(String request); +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/MyWorkflow.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/MyWorkflow.java new file mode 100644 index 00000000..605c931c --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/MyWorkflow.java @@ -0,0 +1,30 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.Restate; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Name; +import dev.restate.sdk.annotation.Workflow; + +@Workflow +@Name("MyWorkflow") +public class MyWorkflow { + + @Workflow + public void run(String myInput) { + Restate.workflowHandle(MyWorkflow.class, Restate.key()) + .send(MyWorkflow::sharedHandler, myInput); + } + + @Handler + public String sharedHandler(String myInput) { + return Restate.workflow(MyWorkflow.class, Restate.key()).sharedHandler(myInput); + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeter.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeter.java new file mode 100644 index 00000000..5317ad32 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeter.java @@ -0,0 +1,26 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.annotation.Exclusive; +import dev.restate.sdk.annotation.Shared; +import dev.restate.sdk.annotation.VirtualObject; + +@VirtualObject +public class ObjectGreeter { + @Exclusive + public String greet(String request) { + return request; + } + + @Shared + public String sharedGreet(String request) { + return request; + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeterImplementedFromInterface.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeterImplementedFromInterface.java new file mode 100644 index 00000000..ca49a003 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ObjectGreeterImplementedFromInterface.java @@ -0,0 +1,16 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +public class ObjectGreeterImplementedFromInterface implements GreeterInterface { + @Override + public String greet(String request) { + return request; + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/PrimitiveTypes.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/PrimitiveTypes.java new file mode 100644 index 00000000..f747434e --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/PrimitiveTypes.java @@ -0,0 +1,29 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.Restate; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Name; +import dev.restate.sdk.annotation.Service; + +@Service +@Name("PrimitiveTypes") +public class PrimitiveTypes { + + @Handler + public int primitiveOutput() { + return Restate.service(PrimitiveTypes.class).primitiveOutput(); + } + + @Handler + public void primitiveInput(int input) { + Restate.service(PrimitiveTypes.class).primitiveInput(input); + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/RawInputOutput.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/RawInputOutput.java new file mode 100644 index 00000000..03e9ec77 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/RawInputOutput.java @@ -0,0 +1,45 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.Restate; +import dev.restate.sdk.annotation.*; + +@Service +@Name("RawInputOutput") +public class RawInputOutput { + + @Handler + @Raw + public byte[] rawOutput() { + return Restate.service(RawInputOutput.class).rawOutput(); + } + + @Handler + @Raw(contentType = "application/vnd.my.custom") + public byte[] rawOutputWithCustomCT() { + return Restate.service(RawInputOutput.class).rawOutputWithCustomCT(); + } + + @Handler + public void rawInput(@Raw byte[] input) { + Restate.service(RawInputOutput.class).rawInput(input); + } + + @Handler + public void rawInputWithCustomCt(@Raw(contentType = "application/vnd.my.custom") byte[] input) { + Restate.service(RawInputOutput.class).rawInputWithCustomCt(input); + } + + @Handler + public void rawInputWithCustomAccept( + @Accept("application/*") @Raw(contentType = "application/vnd.my.custom") byte[] input) { + Restate.service(RawInputOutput.class).rawInputWithCustomAccept(input); + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionDiscoveryTest.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionDiscoveryTest.java new file mode 100644 index 00000000..d80c566e --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionDiscoveryTest.java @@ -0,0 +1,88 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import static dev.restate.sdk.core.AssertUtils.assertThatDiscovery; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; + +import dev.restate.sdk.core.generated.manifest.Handler; +import dev.restate.sdk.core.generated.manifest.Input; +import dev.restate.sdk.core.generated.manifest.Output; +import dev.restate.sdk.core.generated.manifest.Service; +import dev.restate.sdk.core.javaapi.GreeterWithExplicitName; +import dev.restate.sdk.core.javaapi.GreeterWithExplicitNameHandlers; +import dev.restate.sdk.endpoint.Endpoint; +import org.junit.jupiter.api.Test; + +public class ReflectionDiscoveryTest { + + @Test + void checkCustomInputContentType() { + assertThatDiscovery(new RawInputOutput()) + .extractingService("RawInputOutput") + .extractingHandler("rawInputWithCustomCt") + .extracting(Handler::getInput, type(Input.class)) + .extracting(Input::getContentType) + .isEqualTo("application/vnd.my.custom"); + } + + @Test + void checkCustomInputAcceptContentType() { + assertThatDiscovery(new RawInputOutput()) + .extractingService("RawInputOutput") + .extractingHandler("rawInputWithCustomAccept") + .extracting(Handler::getInput, type(Input.class)) + .extracting(Input::getContentType) + .isEqualTo("application/*"); + } + + @Test + void checkCustomOutputContentType() { + assertThatDiscovery(new RawInputOutput()) + .extractingService("RawInputOutput") + .extractingHandler("rawOutputWithCustomCT") + .extracting(Handler::getOutput, type(Output.class)) + .extracting(Output::getContentType) + .isEqualTo("application/vnd.my.custom"); + } + + @Test + void explicitNames() { + assertThatDiscovery((GreeterWithExplicitName) (context, request) -> "") + .extractingService("MyExplicitName") + .extractingHandler("my_greeter"); + assertThat(GreeterWithExplicitNameHandlers.Metadata.SERVICE_NAME).isEqualTo("MyExplicitName"); + } + + @Test + void workflowType() { + assertThatDiscovery(new MyWorkflow()) + .extractingService("MyWorkflow") + .returns(Service.Ty.WORKFLOW, Service::getTy) + .extractingHandler("run") + .returns(Handler.Ty.WORKFLOW, Handler::getTy); + } + + @Test + void usingTransformer() { + assertThatDiscovery( + Endpoint.bind( + new RawInputOutput(), + sd -> + sd.documentation("My service documentation") + .configureHandler( + "rawInputWithCustomCt", + hd -> hd.documentation("My handler documentation")))) + .extractingService("RawInputOutput") + .returns("My service documentation", Service::getDocumentation) + .extractingHandler("rawInputWithCustomCt") + .returns("My handler documentation", Handler::getDocumentation); + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionTest.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionTest.java new file mode 100644 index 00000000..0ca8e276 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ReflectionTest.java @@ -0,0 +1,143 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import static dev.restate.sdk.core.TestDefinitions.testInvocation; +import static dev.restate.sdk.core.statemachine.ProtoUtils.*; + +import dev.restate.common.Target; +import dev.restate.sdk.core.TestDefinitions; +import dev.restate.sdk.core.TestDefinitions.TestSuite; +import dev.restate.sdk.core.TestSerdes; +import dev.restate.sdk.core.javaapi.MySerdeFactory; +import dev.restate.serde.Serde; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +public class ReflectionTest implements TestSuite { + + @Override + public Stream definitions() { + return Stream.of( + testInvocation(ServiceGreeter::new, "greet") + .withInput(startMessage(1), inputCmd("Francesco")) + .onlyBidiStream() + .expectingOutput(outputCmd("Francesco"), END_MESSAGE), + testInvocation(ObjectGreeter::new, "greet") + .withInput(startMessage(1, "slinkydeveloper"), inputCmd("Francesco")) + .onlyBidiStream() + .expectingOutput(outputCmd("Francesco"), END_MESSAGE), + testInvocation(ObjectGreeter::new, "sharedGreet") + .withInput(startMessage(1, "slinkydeveloper"), inputCmd("Francesco")) + .onlyBidiStream() + .expectingOutput(outputCmd("Francesco"), END_MESSAGE), + testInvocation(ObjectGreeterImplementedFromInterface::new, "greet") + .withInput(startMessage(1, "slinkydeveloper"), inputCmd("Francesco")) + .onlyBidiStream() + .expectingOutput(outputCmd("Francesco"), END_MESSAGE), + testInvocation(Empty::new, "emptyInput") + .withInput(startMessage(1), inputCmd(), callCompletion(2, "Till")) + .onlyBidiStream() + .expectingOutput( + callCmd(1, 2, Target.service("Empty", "emptyInput")), + outputCmd("Till"), + END_MESSAGE) + .named("empty output"), + testInvocation(Empty::new, "emptyOutput") + .withInput(startMessage(1), inputCmd("Francesco"), callCompletion(2, Serde.VOID, null)) + .onlyBidiStream() + .expectingOutput( + callCmd(1, 2, Target.service("Empty", "emptyOutput"), "Francesco"), + outputCmd(), + END_MESSAGE) + .named("empty output"), + testInvocation(Empty::new, "emptyInputOutput") + .withInput(startMessage(1), inputCmd("Francesco"), callCompletion(2, Serde.VOID, null)) + .onlyBidiStream() + .expectingOutput( + callCmd(1, 2, Target.service("Empty", "emptyInputOutput")), + outputCmd(), + END_MESSAGE) + .named("empty input and empty output"), + testInvocation(PrimitiveTypes::new, "primitiveOutput") + .withInput(startMessage(1), inputCmd(), callCompletion(2, TestSerdes.INT, 10)) + .onlyBidiStream() + .expectingOutput( + callCmd( + 1, 2, Target.service("PrimitiveTypes", "primitiveOutput"), Serde.VOID, null), + outputCmd(TestSerdes.INT, 10), + END_MESSAGE) + .named("primitive output"), + testInvocation(PrimitiveTypes::new, "primitiveInput") + .withInput(startMessage(1), inputCmd(10), callCompletion(2, Serde.VOID, null)) + .onlyBidiStream() + .expectingOutput( + callCmd( + 1, 2, Target.service("PrimitiveTypes", "primitiveInput"), TestSerdes.INT, 10), + outputCmd(), + END_MESSAGE) + .named("primitive input"), + testInvocation(RawInputOutput::new, "rawInput") + .withInput( + startMessage(1), + inputCmd("{{".getBytes(StandardCharsets.UTF_8)), + callCompletion(2, Serde.VOID, null)) + .onlyBidiStream() + .expectingOutput( + callCmd( + 1, + 2, + Target.service("RawInputOutput", "rawInput"), + "{{".getBytes(StandardCharsets.UTF_8)), + outputCmd(), + END_MESSAGE), + testInvocation(RawInputOutput::new, "rawInputWithCustomCt") + .withInput( + startMessage(1), + inputCmd("{{".getBytes(StandardCharsets.UTF_8)), + callCompletion(2, Serde.VOID, null)) + .onlyBidiStream() + .expectingOutput( + callCmd( + 1, + 2, + Target.service("RawInputOutput", "rawInputWithCustomCt"), + "{{".getBytes(StandardCharsets.UTF_8)), + outputCmd(), + END_MESSAGE), + testInvocation(RawInputOutput::new, "rawOutput") + .withInput( + startMessage(1), + inputCmd(), + callCompletion(2, Serde.RAW, "{{".getBytes(StandardCharsets.UTF_8))) + .onlyBidiStream() + .expectingOutput( + callCmd(1, 2, Target.service("RawInputOutput", "rawOutput"), Serde.VOID, null), + outputCmd("{{".getBytes(StandardCharsets.UTF_8)), + END_MESSAGE), + testInvocation(RawInputOutput::new, "rawOutputWithCustomCT") + .withInput( + startMessage(1), + inputCmd(), + callCompletion(2, Serde.RAW, "{{".getBytes(StandardCharsets.UTF_8))) + .onlyBidiStream() + .expectingOutput( + callCmd( + 1, + 2, + Target.service("RawInputOutput", "rawOutputWithCustomCT"), + Serde.VOID, + null), + outputCmd("{{".getBytes(StandardCharsets.UTF_8)), + END_MESSAGE), + testInvocation(CustomSerde::new, "greet") + .withInput(startMessage(1), inputCmd(MySerdeFactory.SERDE, "input")) + .expectingOutput(outputCmd(MySerdeFactory.SERDE, "OUTPUT"), END_MESSAGE)); + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ServiceGreeter.java b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ServiceGreeter.java new file mode 100644 index 00000000..5357a10b --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/javaapi/reflections/ServiceGreeter.java @@ -0,0 +1,20 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core.javaapi.reflections; + +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Service; + +@Service +public class ServiceGreeter { + @Handler + public String greet(String request) { + return request; + } +} diff --git a/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/SdkTestingIntegrationTest.java b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/SdkTestingIntegrationTest.java index 20e2d86c..9b4f1f0b 100644 --- a/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/SdkTestingIntegrationTest.java +++ b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/SdkTestingIntegrationTest.java @@ -37,7 +37,7 @@ void greet(@RestateClient Client ingressClient) { @Test @Timeout(value = 10) void greetNewApi(@RestateClient Client ingressClient) { - var client = ingressClient.service(GreeterNewApi.class).client(); + var client = ingressClient.service(GreeterNewApi.class); assertThat(client.greet("Francesco")).isEqualTo("Something something Francesco"); }