[Fix #1209] Adding Context and Filter predicate#1224
[Fix #1209] Adding Context and Filter predicate#1224fjtirado wants to merge 1 commit intoserverlessworkflow:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses issue #1209 by expanding Java predicate/function support to optionally receive workflow and task context, enabling context-aware filtering and evaluation across the experimental Java runtime and fluent DSL.
Changes:
- Introduces new context-aware predicate/function types (
ContextPredicate,FilterPredicate,ContextFunction,FilterFunction) plus typed wrappers. - Updates switch/listen/for execution paths to build predicates/functions from the new container/wrapper types.
- Refactors the fluent DSL and tests to use the renamed/updated function interfaces and new switch predicate container type.
Reviewed changes
Copilot reviewed 41 out of 41 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/UntilPredicate.java | Stores predicates as Object (typed wrappers) and implements PredicateContainer to support multiple predicate shapes. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/TypedFilterPredicate.java | Adds typed wrapper for FilterPredicate argument conversion. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/TypedFilterFunction.java | Adds typed wrapper for FilterFunction argument conversion. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/TypedContextPredicate.java | Adds typed wrapper for ContextPredicate argument conversion. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/TypedContextFunction.java | Renames typed context function wrapper to match new ContextFunction interface. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/SwitchCasePredicate.java | New switch-case container supporting plain/typed/context/task-aware predicates. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/SwitchCaseFunction.java | Removes the old SwitchCaseFunction container implementation. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/PredicateContainer.java | Introduces a common interface for types that “carry” a predicate object. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/OutputAsFunction.java | Updates overloads to use FilterFunction / ContextFunction and new typed wrappers. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/LoopPredicateIndexFilter.java | Adds loop predicate shape that receives workflow/task context. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/LoopPredicateIndexContext.java | Adds loop predicate shape that receives workflow context. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/InputFromFunction.java | Updates overloads to use FilterFunction / ContextFunction and new typed wrappers. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java | Extends while predicate options to include index + workflow/task context forms. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/FilterPredicate.java | Introduces predicate that receives workflow + task context. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/FilterFunction.java | Renames JavaFilterFunction to FilterFunction (context+task-aware function). |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ExportAsFunction.java | Updates overloads to use FilterFunction / ContextFunction and new typed wrappers. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/EventDataPredicate.java | Adds context-aware predicate overloads for event data filtering. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ContextPredicate.java | Introduces predicate that receives workflow context. |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ContextFunction.java | Renames JavaContextFunction to ContextFunction (workflow-context-aware function). |
| experimental/types/src/main/java/io/serverlessworkflow/api/types/func/CallJava.java | Updates call variants and nested types to use ContextFunction / FilterFunction. |
| experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/CallTest.java | Updates switch predicate test to use SwitchCasePredicate. |
| experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/CallJavaContextFunctionTest.java | Updates test to use ContextFunction. |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaExpressionFactory.java | Adds predicate building for new predicate types and typed wrappers; updates priority logic. |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaSwitchExecutorBuilder.java | Switch executor now uses SwitchCasePredicate and JavaFuncUtils.from(...). |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaListenExecutorBuilder.java | Listen executor now uses UntilPredicate + JavaFuncUtils.from(...). |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaFuncUtils.java | Replaces old typed-predicate construction helper with predicate-container based predicate building. |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java | Passes workflow/task context into the new loop predicate signature. |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaFilterFunctionCallExecutorBuilder.java | Updates builder to use FilterFunction. |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaFilterFunctionCallExecutor.java | Updates executor to call FilterFunction.apply(...). |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutorBuilder.java | Updates builder to use ContextFunction. |
| experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java | Updates executor to call ContextFunction.apply(...). |
| experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLUniqueIdTest.java | Updates test extraction/casts to FilterFunction. |
| experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java | Updates DSL tests to use FilterFunction. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java | Updates DSL transformation interfaces to accept ContextFunction / FilterFunction. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTaskTransformations.java | Updates task-specific transformation interfaces to accept ContextFunction / FilterFunction. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java | Updates DSL Step APIs + Javadoc references to renamed function interfaces. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ReflectionUtils.java | Updates type inference helpers to accept ContextFunction / FilterFunction. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java | Updates DSL entrypoints and internal wrappers to use renamed function interfaces. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java | Updates internal step representation to store ContextFunction / FilterFunction. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSwitchTaskBuilder.java | Switch DSL builder now constructs SwitchCasePredicate. |
| experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java | Call-task DSL builder now accepts ContextFunction / FilterFunction. |
Comments suppressed due to low confidence (2)
experimental/types/src/main/java/io/serverlessworkflow/api/types/func/FilterFunction.java:25
- This change renames the public API type
JavaFilterFunctiontoFilterFunction(and similarly for context variants) across modules. That’s a breaking change beyond the stated goal of adding optional context parameters; if intentional, it should be called out in the PR description/release notes and (if needed) provide migration guidance or deprecation shims.
experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaExpressionFactory.java:136 buildIfFilter(...)still only recognizesPredicateandTypedPredicate. With the newContextPredicate/FilterPredicate(and typed variants),when/ifpredicates stored in metadata will be silently ignored. Extend this method to handle the new predicate types the same waybuildPredicate(...)does.
public Optional<WorkflowPredicate> buildIfFilter(TaskBase task) {
TaskMetadata metadata = task.getMetadata();
if (metadata != null) {
Object obj = metadata.getAdditionalProperties().get(TaskMetadataKeys.IF_PREDICATE);
if (obj instanceof Predicate pred) {
return Optional.of(fromPredicate(pred));
} else if (obj instanceof TypedPredicate pred) {
return Optional.of(fromPredicate(pred));
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| public <T> SwitchCasePredicate withPredicate(ContextPredicate<T> predicate, Class<T> clazz) { | ||
| this.predicate = new TypedContextPredicate<>(predicate, clazz); | ||
| return this; |
There was a problem hiding this comment.
withPredicate(ContextPredicate<T> predicate, Class<T> clazz) wraps into TypedContextPredicate, but if clazz is null the predicate evaluation will fail later with an unclear exception. Add a non-null check (or handle null by falling back to the untyped overload).
| public <T> SwitchCasePredicate withPredicate(FilterPredicate<T> predicate, Class<T> clazz) { | ||
| this.predicate = new TypedFilterPredicate<>(predicate, clazz); | ||
| return this; |
There was a problem hiding this comment.
withPredicate(FilterPredicate<T> predicate, Class<T> clazz) creates a typed wrapper but doesn’t guard against clazz == null. Consider requiring a non-null class here to avoid deferred failures during evaluation.
| public <T> UntilPredicate withPredicate(ContextPredicate<T> predicate, Class<T> clazz) { | ||
| this.predicate = new TypedContextPredicate<>(predicate, clazz); | ||
| return this; | ||
| } |
There was a problem hiding this comment.
withPredicate(ContextPredicate<T> predicate, Class<T> clazz) wraps into TypedContextPredicate but doesn’t validate clazz. If null is passed, evaluation will fail later with an unclear exception. Add a non-null check or fall back to the untyped overload when clazz is null.
| return switchCase instanceof SwitchCasePredicate predicate | ||
| ? Optional.of(JavaFuncUtils.from(application, predicate)) | ||
| : super.buildFilter(switchCase); |
There was a problem hiding this comment.
buildFilter always wraps SwitchCasePredicate via JavaFuncUtils.from(...) even when predicate.predicate() is null (default case). JavaExpressionFactory.buildPredicate(...) throws on null/non-predicate values, so default cases will fail at runtime. Add a null check and return Optional.empty() (or delegate to super) when the predicate is not set.
| return switchCase instanceof SwitchCasePredicate predicate | |
| ? Optional.of(JavaFuncUtils.from(application, predicate)) | |
| : super.buildFilter(switchCase); | |
| if (switchCase instanceof SwitchCasePredicate predicate) { | |
| if (predicate.predicate() == null) { | |
| // No predicate defined (default case); no filter to apply | |
| return Optional.empty(); | |
| } | |
| return Optional.of(JavaFuncUtils.from(application, predicate)); | |
| } | |
| return super.buildFilter(switchCase); |
| public <T, V> ForTaskFunction withWhile( | ||
| LoopPredicateIndex<T, V> whilePredicate, Class<T> modelClass, Class<V> itemClass) { | ||
| LoopPredicateIndexFilter<T, V> whilePredicate, Class<T> modelClass, Class<V> itemClass) { | ||
| return withWhile(whilePredicate, Optional.ofNullable(modelClass), Optional.of(itemClass)); | ||
| } |
There was a problem hiding this comment.
withWhile(..., Class<V> itemClass) wraps itemClass with Optional.of(itemClass), which will throw if callers pass null (other APIs in this class accept null and use Optional.ofNullable). Use Optional.ofNullable(itemClass) or enforce non-null with an explicit check and clear exception message.
| } | ||
|
|
||
| private WorkflowPredicate fromPredicate(TypedContextPredicate pred) { | ||
| return (w, t, n) -> pred.predicate().test(n.as(pred.argClass()).orElseThrow(), w); |
There was a problem hiding this comment.
New predicate variants (ContextPredicate / typed wrappers) were added, but this PR doesn’t add unit tests that exercise these code paths (including verifying workflow context is passed through correctly and typed conversion works). Consider adding coverage to prevent regressions.
| return (w, t, n) -> pred.predicate().test(n.as(pred.argClass()).orElseThrow(), w); | |
| return (w, t, n) -> pred.predicate().test(convert(n, pred.argClass()), w); |
| return until instanceof UntilPredicate untilPred | ||
| ? JavaFuncUtils.from(application, untilPred) | ||
| : super.buildUntilPredicate(until); |
There was a problem hiding this comment.
buildUntilPredicate unconditionally calls JavaFuncUtils.from(...) for UntilPredicate. If untilPred.predicate() is null (or not a supported predicate type), JavaExpressionFactory.buildPredicate(...) will throw. Preserve the previous behavior by checking for null and delegating to super.buildUntilPredicate(until) when no predicate is provided.
| return until instanceof UntilPredicate untilPred | |
| ? JavaFuncUtils.from(application, untilPred) | |
| : super.buildUntilPredicate(until); | |
| if (!(until instanceof UntilPredicate untilPred)) { | |
| return super.buildUntilPredicate(until); | |
| } | |
| if (untilPred.predicate() == null) { | |
| return super.buildUntilPredicate(until); | |
| } | |
| return JavaFuncUtils.from(application, untilPred); |
|
|
||
| @FunctionalInterface | ||
| public interface LoopPredicateIndexFilter<T, V> { | ||
| boolean test(T model, V item, Integer index, WorkflowContextData workfklow, TaskContextData task); |
There was a problem hiding this comment.
Parameter name workfklow is misspelled; this is part of the public functional interface signature and will propagate into implementations and IDE hints. Rename it to workflow.
| private static FilterFunction<Object, Object> extractJavaFilterFunction(CallJava callJava) { | ||
| if (callJava instanceof CallJava.CallJavaFilterFunction<?, ?> f) { | ||
| return (JavaFilterFunction<Object, Object>) f.function(); | ||
| return (FilterFunction<Object, Object>) f.function(); | ||
| } |
There was a problem hiding this comment.
Method name extractJavaFilterFunction(...) now returns FilterFunction and the old JavaFilterFunction type no longer exists. Renaming the method (and any related assertion messages) would keep the test intent clear and avoid implying the old API is still present.
| public <T> SwitchCasePredicate withPredicate(Predicate<T> predicate, Class<T> predicateClass) { | ||
| this.predicate = new TypedPredicate<>(predicate, predicateClass); | ||
| return this; |
There was a problem hiding this comment.
withPredicate(Predicate<T> predicate, Class<T> predicateClass) creates a typed wrapper but doesn’t validate predicateClass. If null is passed, the failure will be deferred to evaluation time with a likely NullPointerException. Consider enforcing non-null (e.g., Objects.requireNonNull) or preserving the previous nullable behavior by treating null as the untyped overload.
Signed-off-by: fjtirado <ftirados@redhat.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 41 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java:118
- Test comment and assertion message still reference
JavaFilterFunction, but the API type is nowFilterFunction. Updating these strings will avoid confusion when reading failures and keep tests aligned with the renamed API.
// JavaFilterFunction<T,R> is (T, WorkflowContextData, TaskContextData) -> R
FilterFunction<String, Map<String, Object>> jf =
(val, wfCtx, taskCtx) -> Map.of("wrapped", val, "wfId", wfCtx.instanceData().id());
Workflow wf =
FuncWorkflowBuilder.workflow("step-emit-export")
.tasks(emit("emitWrapped", spec).exportAs(jf)) // chaining on Step
.build();
List<TaskItem> items = wf.getDo();
assertEquals(1, items.size());
Task t = items.get(0).getTask();
assertNotNull(t.getEmitTask(), "EmitTask expected");
// Export is attached to Task
Export ex = t.getEmitTask().getExport();
assertNotNull(ex, "Export should be set via Step.exportAs(JavaFilterFunction)");
assertNotNull(ex.getAs(), "'as' should be populated");
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| public <T, V> ForTaskFunction withWhile( | ||
| LoopPredicateIndex<T, V> whilePredicate, Class<T> modelClass, Class<V> itemClass) { | ||
| return withWhile( | ||
| toPredicate(whilePredicate), Optional.ofNullable(modelClass), Optional.of(itemClass)); | ||
| } |
There was a problem hiding this comment.
These overloads wrap itemClass using Optional.of(itemClass), which will throw if a caller passes null. Use Optional.ofNullable(itemClass) (consistent with modelClass) or explicitly requireNonNull(itemClass) to fail fast with a clearer message. (Same pattern repeats in the other overloads that accept itemClass.)
| public <T> SwitchCasePredicate withPredicate(Predicate<T> predicate, Class<T> predicateClass) { | ||
| this.predicate = new TypedPredicate<>(predicate, predicateClass); | ||
| return this; | ||
| } |
There was a problem hiding this comment.
The typed overload withPredicate(..., Class<T> ...) doesn't validate the class parameter. If a caller passes null, the typed wrapper will later fail when the model is converted. Consider adding Objects.requireNonNull(...) here (and similarly in the other typed overloads) for a clearer, earlier failure.
| @@ -59,12 +59,12 @@ public final class FuncCallStep<T, R> extends Step<FuncCallStep<T, R>, FuncCallT | |||
| } | |||
|
|
|||
| /** JavaFilterFunction<T,R> variant (unnamed). */ | |||
| FuncCallStep(JavaFilterFunction<T, R> filterFn, Class<T> argClass) { | |||
| FuncCallStep(FilterFunction<T, R> filterFn, Class<T> argClass) { | |||
| this(null, filterFn, argClass); | |||
| } | |||
|
|
|||
| /** JavaFilterFunction<T,R> variant (named). */ | |||
| FuncCallStep(String name, JavaFilterFunction<T, R> filterFn, Class<T> argClass) { | |||
| FuncCallStep(String name, FilterFunction<T, R> filterFn, Class<T> argClass) { | |||
There was a problem hiding this comment.
The constructor/variant comments still refer to JavaContextFunction / JavaFilterFunction, but the types were renamed to ContextFunction / FilterFunction. Updating these comments will keep the DSL docs aligned with the public API names.
| @SuppressWarnings("unchecked") | ||
| private static JavaFilterFunction<Object, Object> extractJavaFilterFunction(CallJava callJava) { | ||
| private static FilterFunction<Object, Object> extractJavaFilterFunction(CallJava callJava) { | ||
| if (callJava instanceof CallJava.CallJavaFilterFunction<?, ?> f) { | ||
| return (JavaFilterFunction<Object, Object>) f.function(); | ||
| return (FilterFunction<Object, Object>) f.function(); | ||
| } | ||
| fail("CallTask is not a CallJavaFilterFunction; DSL contract may have changed."); | ||
| return null; // unreachable |
There was a problem hiding this comment.
Helper method name extractJavaFilterFunction (and related assertion message below) still references the old JavaFilterFunction naming even though it now returns FilterFunction. Consider renaming to extractFilterFunction (and updating messages) to keep terminology consistent with the API rename.
| public <T> UntilPredicate withPredicate(Predicate<T> predicate, Class<T> clazz) { | ||
| this.predicate = new TypedPredicate<>(predicate, clazz); | ||
| return this; | ||
| } |
There was a problem hiding this comment.
The withPredicate(..., Class<T> clazz) overload does not validate clazz. Passing null will create a typed wrapper with a null argClass, which later leads to confusing failures when converting the model. Consider Objects.requireNonNull(clazz) here (and similarly for the other overloads that take a Class).
| private WorkflowPredicate fromPredicate(ContextPredicate pred) { | ||
| return (w, t, n) -> pred.test(n.asJavaObject(), w); | ||
| } | ||
|
|
||
| private WorkflowPredicate fromPredicate(TypedContextPredicate pred) { | ||
| return (w, t, n) -> pred.predicate().test(convert(n, pred.argClass()), w); | ||
| } | ||
|
|
||
| private WorkflowPredicate fromPredicate(FilterPredicate pred) { | ||
| return (w, t, n) -> pred.test(n.asJavaObject(), w, t); | ||
| } | ||
|
|
||
| private WorkflowPredicate fromPredicate(TypedFilterPredicate pred) { | ||
| return (w, t, n) -> pred.predicate().test(convert(n, pred.argClass()), w, t); | ||
| } |
There was a problem hiding this comment.
JavaExpressionFactory adds support for ContextPredicate / FilterPredicate (and typed variants) in buildIfFilter / buildPredicate, but there are no tests covering these new predicate types. Add unit tests that exercise both untyped and typed variants to ensure workflow/task context is correctly passed and conversions behave as expected.
ricardozanini
left a comment
There was a problem hiding this comment.
LGTM, but Co-Pilot is pointing out a few null checks that I think are worth adding.
Fix #1209
Also, as suggested by @ricardozanini, Java prefix is removed from functions.