Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

Expand All @@ -31,6 +32,7 @@
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public final class TaskHandlerProcessor extends AbstractProcessor {
static final String ANNOTATION = "org.byteveda.taskito.annotation.TaskHandler";
static final String RESOURCE = "org.byteveda.taskito.annotation.Resource";

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Expand All @@ -56,10 +58,21 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
}

private boolean validate(ExecutableElement method) {
if (method.getParameters().size() != 1) {
error(method, "@TaskHandler method must take exactly one parameter (the payload)");
List<? extends VariableElement> params = method.getParameters();
if (params.isEmpty()) {
error(method, "@TaskHandler method must take the payload as its first parameter");
return false;
}
if (resourceName(params.get(0)) != null) {
error(method, "the first @TaskHandler parameter is the payload and must not be annotated @Resource");
return false;
}
for (int i = 1; i < params.size(); i++) {
if (resourceName(params.get(i)) == null) {
error(method, "@TaskHandler parameters after the payload must be annotated @Resource");
return false;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (method.getModifiers().contains(Modifier.PRIVATE)) {
error(method, "@TaskHandler method must not be private");
return false;
Expand All @@ -76,6 +89,7 @@ private void generate(TypeElement owner, List<ExecutableElement> methods) {
String ownerSimple = owner.getSimpleName().toString();
String companion = ownerSimple + "Tasks";
String qualified = pkg.isEmpty() ? companion : pkg + "." + companion;
boolean anyResources = methods.stream().anyMatch(m -> m.getParameters().size() > 1);

StringBuilder out = new StringBuilder();
if (!pkg.isEmpty()) {
Expand All @@ -85,7 +99,9 @@ private void generate(TypeElement owner, List<ExecutableElement> methods) {
.append("import org.byteveda.taskito.task.Task;\n")
.append("import org.byteveda.taskito.worker.Handler;\n")
.append("import org.byteveda.taskito.worker.HandlerRegistry;\n")
.append("import org.byteveda.taskito.worker.Worker;\n\n")
.append("import org.byteveda.taskito.worker.Worker;\n")
.append(anyResources ? "import org.byteveda.taskito.resources.Resources;\n" : "")
.append("\n")
.append("/** Generated task descriptors + worker binding for {@link ")
.append(ownerSimple)
.append("}. */\n")
Expand Down Expand Up @@ -118,20 +134,11 @@ private void generate(TypeElement owner, List<ExecutableElement> methods) {
.append(" impl) {\n");
for (ExecutableElement method : methods) {
String constant = upperSnake(method.getSimpleName().toString());
String methodName = method.getSimpleName().toString();
if (isVoid(method)) {
out.append(" builder.handle(")
.append(constant)
.append(", payload -> {\n impl.")
.append(methodName)
.append("(payload);\n return null;\n });\n");
} else {
out.append(" builder.handle(")
.append(constant)
.append(", impl::")
.append(methodName)
.append(");\n");
}
out.append(" builder.handle(")
.append(constant)
.append(", ")
.append(handlerExpr(method))
.append(");\n");
}
out.append(" return builder;\n }\n\n");

Expand All @@ -142,15 +149,11 @@ private void generate(TypeElement owner, List<ExecutableElement> methods) {
for (int i = 0; i < methods.size(); i++) {
ExecutableElement method = methods.get(i);
String constant = upperSnake(method.getSimpleName().toString());
String methodName = method.getSimpleName().toString();
out.append(" Handler.of(").append(constant).append(", ");
if (isVoid(method)) {
out.append("payload -> {\n impl.")
.append(methodName)
.append("(payload);\n return null;\n })");
} else {
out.append("impl::").append(methodName).append(")");
}
out.append(" Handler.of(")
.append(constant)
.append(", ")
.append(handlerExpr(method))
.append(")");
out.append(i + 1 < methods.size() ? ",\n" : ");\n");
}
out.append(" }\n}\n");
Expand Down Expand Up @@ -181,6 +184,37 @@ private String options(ExecutableElement method) {
return chain.toString();
}

/**
* The {@code TaskFunction} expression for a handler: a method reference when it
* takes only the payload, otherwise a lambda that resolves each {@code @Resource}
* parameter from the worker resource runtime.
*/
private String handlerExpr(ExecutableElement method) {
String methodName = method.getSimpleName().toString();
List<? extends VariableElement> params = method.getParameters();
StringBuilder args = new StringBuilder("payload");
for (int i = 1; i < params.size(); i++) {
args.append(", Resources.use(\"")
.append(escape(resourceName(params.get(i))))
.append("\")");
}
if (isVoid(method)) {
return "payload -> { impl." + methodName + "(" + args + "); return null; }";
}
return params.size() > 1 ? "payload -> impl." + methodName + "(" + args + ")" : "impl::" + methodName;
}

/** The {@code @Resource} value on a parameter, or null if it is not annotated. */
private String resourceName(VariableElement param) {
for (AnnotationMirror annotation : param.getAnnotationMirrors()) {
if (annotation.getAnnotationType().toString().equals(RESOURCE)) {
Object value = rawValue(annotation, "value");
return value == null ? "" : value.toString();
}
}
return null;
}

private String taskName(ExecutableElement method) {
String value = stringValue(mirror(method), "value", "");
return value.isEmpty() ? method.getSimpleName().toString() : value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.byteveda.taskito.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Injects a worker resource into a {@code @TaskHandler} method. Place it on
* parameters <em>after</em> the payload; the generated companion resolves each by
* name from the worker's resource runtime (equivalent to {@code Resources.use(name)})
* and passes it positionally. Source-retention — read at compile time, never at
* runtime.
*
* <pre>{@code
* @TaskHandler("send_email")
* void send(EmailPayload payload, @Resource("db") Database db) { ... }
* }</pre>
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.PARAMETER)
public @interface Resource {
/** The registered resource name to resolve. */
String value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ void generatedCompanionEnqueuesAndHandles(@TempDir Path dir) throws Exception {
}
}

@Test
@Timeout(30)
void injectsResourceParameter(@TempDir Path dir) throws Exception {
try (Taskito queue =
Taskito.builder().sqlite(dir.resolve("ri.db").toString()).open()) {
// ResourceGreeter.greet takes a @Resource("salutation") param; the generated
// bind resolves it from the worker resource runtime.
queue.resource("salutation", ctx -> "hi");
String id = queue.enqueue(ResourceGreeterTasks.GREET, "ada");
CountDownLatch done = new CountDownLatch(1);
try (Worker worker = queue.worker()
.apply(builder -> ResourceGreeterTasks.bind(builder, new ResourceGreeter()))
.on(EventName.SUCCESS, event -> done.countDown())
.start()) {
assertTrue(done.await(20, TimeUnit.SECONDS), "task should complete");
assertEquals("hi ada", queue.getResult(id, String.class).orElseThrow());
}
}
}

@Test
@Timeout(30)
void registerViaGeneratedHandlerRegistry(@TempDir Path dir) throws Exception {
Expand Down
13 changes: 13 additions & 0 deletions sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.byteveda.taskito;

import org.byteveda.taskito.annotation.Resource;
import org.byteveda.taskito.annotation.TaskHandler;

/** Test fixture: a handler with a {@code @Resource}-injected parameter after the payload. */
class ResourceGreeter {

@TaskHandler("rg.greet")
String greet(String name, @Resource("salutation") String salutation) {
return salutation + " " + name;
}
}