From c397f416aa76c28e17c7af9d465b23f08a73c6b0 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Mon, 1 Jun 2026 21:23:09 -0700 Subject: [PATCH] Add Temporal Operation --- .../io/nexusrpc/handler/MethodExtension.java | 52 +++++++++++++++++++ .../nexusrpc/handler/ServiceImplInstance.java | 50 +++++++++++++++--- 2 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 nexus-sdk/src/main/java/io/nexusrpc/handler/MethodExtension.java diff --git a/nexus-sdk/src/main/java/io/nexusrpc/handler/MethodExtension.java b/nexus-sdk/src/main/java/io/nexusrpc/handler/MethodExtension.java new file mode 100644 index 0000000..4775500 --- /dev/null +++ b/nexus-sdk/src/main/java/io/nexusrpc/handler/MethodExtension.java @@ -0,0 +1,52 @@ +package io.nexusrpc.handler; + +import io.nexusrpc.ServiceDefinition; +import java.lang.reflect.Method; +import java.util.Objects; +import org.jspecify.annotations.Nullable; + +/** + * Extension point for {@link ServiceImplInstance#fromInstance(Object, java.util.List)} that lets a + * framework recognize its own operation-handler annotation alongside {@link OperationImpl}. + * + *

For each method discovered during scanning of a {@link ServiceImpl}-annotated class, the + * extension's {@link #extract} is called once. If the method belongs to the extension's annotation, + * the extension returns a {@link Result} describing the operation name and handler; otherwise it + * returns {@code null}. Duplicate operation registrations (across both {@link OperationImpl} and + * any extension) are caught by the builder. + * + *

Extensions are responsible for their own validation of method shape (parameter types, return + * type, modifiers) and for translating the method into an {@link OperationHandler}. + */ +public interface MethodExtension { + + /** + * Inspect {@code method} on {@code instance}. Return a {@link Result} to register an operation + * handler, or {@code null} if the method is not claimed by this extension. + * + * @param instance the service implementation instance + * @param method the method being scanned + * @param serviceDefinition the resolved service definition for the {@code @ServiceImpl} + * @return a result describing the operation, or {@code null} to skip + */ + @Nullable Result extract(Object instance, Method method, ServiceDefinition serviceDefinition); + + /** Result returned by an extension that claims a method. */ + final class Result { + private final String operationName; + private final OperationHandler handler; + + public Result(String operationName, OperationHandler handler) { + this.operationName = Objects.requireNonNull(operationName, "operationName"); + this.handler = Objects.requireNonNull(handler, "handler"); + } + + public String getOperationName() { + return operationName; + } + + public OperationHandler getHandler() { + return handler; + } + } +} diff --git a/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceImplInstance.java b/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceImplInstance.java index 0ca25fb..eabb84c 100644 --- a/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceImplInstance.java +++ b/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceImplInstance.java @@ -18,6 +18,20 @@ public class ServiceImplInstance { * operations. */ public static ServiceImplInstance fromInstance(Object instance) { + return fromInstance(instance, Collections.emptyList()); + } + + /** + * Create a service instance from the given object instance, with additional {@link + * MethodExtension}s consulted for each scanned method. Extensions let frameworks recognize their + * own operation-handler annotations alongside {@link OperationImpl}. + * + *

For every method on the service class, {@link OperationImpl} is processed first; then each + * extension is invoked. Duplicate operation registrations across {@link OperationImpl} and any + * extension fail the build. + */ + public static ServiceImplInstance fromInstance( + Object instance, List extensions) { // Expect the annotation on the class of the instance itself, do not expect it to be inherited // and go searching through superclasses ServiceImpl serviceImpl = instance.getClass().getDeclaredAnnotation(ServiceImpl.class); @@ -39,14 +53,29 @@ public static ServiceImplInstance fromInstance(Object instance) { Builder builder = newBuilder().setDefinition(serviceDefinition); for (Method method : methods) { OperationImpl operationImpl = method.getDeclaredAnnotation(OperationImpl.class); - if (operationImpl == null) { - continue; + if (operationImpl != null) { + try { + addOperationHandler(builder, serviceDefinition, instance, method); + } catch (Exception e) { + throw new RuntimeException( + "Failed obtaining operation handler from " + method.getName(), e); + } } - try { - addOperationHandler(builder, serviceDefinition, instance, method); - } catch (Exception e) { - throw new RuntimeException( - "Failed obtaining operation handler from " + method.getName(), e); + for (MethodExtension extension : extensions) { + MethodExtension.Result result = extension.extract(instance, method, serviceDefinition); + if (result == null) { + continue; + } + if (builder.operationHandlers.containsKey(result.getOperationName())) { + throw new IllegalStateException( + "Operation name " + + result.getOperationName() + + " is registered by both " + + builder.operationHandlers.get(result.getOperationName()) + + " and " + + extension.getClass().getName()); + } + builder.putOperationHandler(result.getOperationName(), result.getHandler()); } } @@ -119,7 +148,12 @@ private static void addOperationHandler( // Add to builder if (builder.operationHandlers.containsKey(operationDefinition.getName())) { - throw new RuntimeException("Multiple overloads with @OperationImpl"); + throw new IllegalStateException( + "Operation name " + + operationDefinition.getName() + + " is registered by both " + + method.getName() + + " and an earlier handler"); } builder.putOperationHandler(operationDefinition.getName(), (OperationHandler) handler); }