Skip to content

[Fix #1209] Adding Context and Filter predicate#1224

Open
fjtirado wants to merge 1 commit intoserverlessworkflow:mainfrom
fjtirado:Fix_#1209
Open

[Fix #1209] Adding Context and Filter predicate#1224
fjtirado wants to merge 1 commit intoserverlessworkflow:mainfrom
fjtirado:Fix_#1209

Conversation

@fjtirado
Copy link
Collaborator

@fjtirado fjtirado commented Mar 12, 2026

Fix #1209

Also, as suggested by @ricardozanini, Java prefix is removed from functions.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 JavaFilterFunction to FilterFunction (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 recognizes Predicate and TypedPredicate. With the new ContextPredicate / FilterPredicate (and typed variants), when/if predicates stored in metadata will be silently ignored. Extend this method to handle the new predicate types the same way buildPredicate(...) 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.

Comment on lines +41 to +43
public <T> SwitchCasePredicate withPredicate(ContextPredicate<T> predicate, Class<T> clazz) {
this.predicate = new TypedContextPredicate<>(predicate, clazz);
return this;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +53
public <T> SwitchCasePredicate withPredicate(FilterPredicate<T> predicate, Class<T> clazz) {
this.predicate = new TypedFilterPredicate<>(predicate, clazz);
return this;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to 43
public <T> UntilPredicate withPredicate(ContextPredicate<T> predicate, Class<T> clazz) {
this.predicate = new TypedContextPredicate<>(predicate, clazz);
return this;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to 38
return switchCase instanceof SwitchCasePredicate predicate
? Optional.of(JavaFuncUtils.from(application, predicate))
: super.buildFilter(switchCase);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines 86 to 89
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));
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

private WorkflowPredicate fromPredicate(TypedContextPredicate pred) {
return (w, t, n) -> pred.predicate().test(n.as(pred.argClass()).orElseThrow(), w);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +35 to 37
return until instanceof UntilPredicate untilPred
? JavaFuncUtils.from(application, untilPred)
: super.buildUntilPredicate(until);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.

@FunctionalInterface
public interface LoopPredicateIndexFilter<T, V> {
boolean test(T model, V item, Integer index, WorkflowContextData workfklow, TaskContextData task);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to 48
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();
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +33
public <T> SwitchCasePredicate withPredicate(Predicate<T> predicate, Class<T> predicateClass) {
this.predicate = new TypedPredicate<>(predicate, predicateClass);
return this;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 12, 2026 11:54
Signed-off-by: fjtirado <ftirados@redhat.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 now FilterFunction. 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.

Comment on lines +55 to +59
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));
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +34
public <T> SwitchCasePredicate withPredicate(Predicate<T> predicate, Class<T> predicateClass) {
this.predicate = new TypedPredicate<>(predicate, predicateClass);
return this;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 47 to +67
@@ -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) {
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 44 to 50
@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
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 30 to +33
public <T> UntilPredicate withPredicate(Predicate<T> predicate, Class<T> clazz) {
this.predicate = new TypedPredicate<>(predicate, clazz);
return this;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +111 to 125
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);
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Member

@ricardozanini ricardozanini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but Co-Pilot is pointing out a few null checks that I think are worth adding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Experimental] Add WorkflowContext and TaskContext to Java predicates

3 participants