From 723ba4a7bff9d0e379591778c276c57dfe0f19fb Mon Sep 17 00:00:00 2001 From: fjtirado Date: Fri, 24 Apr 2026 19:55:57 +0200 Subject: [PATCH] [Fix #1339] Add SerializableFunction support to ForEach on DSL Signed-off-by: fjtirado --- .../fluent/func/FuncForTaskBuilder.java | 8 ++- .../fluent/func/dsl/FuncDSL.java | 29 ++++++++-- .../fluent/test/ForEachFuncTest.java | 57 +++++++++++++++++++ .../api/types/func/ForTaskFunction.java | 8 +-- 4 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 experimental/test/src/test/java/io/serverlessworkflow/fluent/test/ForEachFuncTest.java diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForTaskBuilder.java index bf64868d2..3e0a59d60 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForTaskBuilder.java @@ -64,11 +64,17 @@ public FuncForTaskBuilder whileC(LoopPredicateIndex predicate) { return this; } - public FuncForTaskBuilder collection(Function> collectionF) { + public FuncForTaskBuilder collection(Function> collectionF) { this.forTaskFunction.withCollection(collectionF); return this; } + public FuncForTaskBuilder collection( + Function> collectionF, Class clazz) { + this.forTaskFunction.withCollection(collectionF, clazz); + return this; + } + public FuncForTaskBuilder tasks(String name, LoopFunction function) { if (name == null || name.isBlank()) { name = "for-task-" + this.items.size(); diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java index 746d0ce1f..40ff30ca2 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java @@ -26,6 +26,7 @@ import io.serverlessworkflow.api.types.OAuth2AuthenticationData; import io.serverlessworkflow.api.types.func.ContextFunction; import io.serverlessworkflow.api.types.func.FilterFunction; +import io.serverlessworkflow.api.types.func.LoopFunction; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; @@ -1020,9 +1021,25 @@ public static FuncTaskConfigurer switchWhenOrElse( * @param input type for the collection function * @return list configurer */ - public static FuncTaskConfigurer forEach( - Function> collection, Consumer body) { - return list -> list.forEach(j -> j.collection(collection).tasks(body)); + public static FuncTaskConfigurer forEach( + SerializableFunction> collection, Consumer body) { + return list -> + list.forEach( + j -> j.collection(collection, ReflectionUtils.inferInputType(collection)).tasks(body)); + } + + public static FuncTaskConfigurer forEach( + SerializableFunction> collection, LoopFunction function) { + return list -> + list.forEach( + j -> + j.collection(collection, ReflectionUtils.inferInputType(collection)) + .tasks(function)); + } + + public static FuncTaskConfigurer forEachItem( + SerializableFunction> collection, Function function) { + return forEach(collection, ((t, v) -> function.apply((V) v))); } /** @@ -1033,9 +1050,9 @@ public static FuncTaskConfigurer forEach( * @param ignored (kept for signature consistency) * @return list configurer */ - public static FuncTaskConfigurer forEach( - Collection collection, Consumer body) { - Function> f = ctx -> (Collection) collection; + public static FuncTaskConfigurer forEach( + Collection collection, Consumer body) { + Function> f = ctx -> collection; return list -> list.forEach(j -> j.collection(f).tasks(body)); } diff --git a/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/ForEachFuncTest.java b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/ForEachFuncTest.java new file mode 100644 index 000000000..11b94fd68 --- /dev/null +++ b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/ForEachFuncTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.test; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowModel; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class ForEachFuncTest { + + private static record Order(String id) {} + + private static record EnhancedOrder(String id, int salary) {} + + private static record OrdersPayload(List orders) {} + + @Test + void testForEachIteration() throws Exception { + + Workflow workflow = + FuncWorkflowBuilder.workflow("foreach-workflow") + .tasks(forEachItem(OrdersPayload::orders, ForEachFuncTest::enhace)) + .build(); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + OrdersPayload input = + new OrdersPayload( + List.of(new Order("ORD-001"), new Order("ORD-002"), new Order("ORD-003"))); + WorkflowModel result = app.workflowDefinition(workflow).instance(input).start().join(); + assertThat(result.as(EnhancedOrder.class).orElseThrow().id()) + .isEqualTo(input.orders().get(input.orders.size() - 1).id()); + } + } + + private static EnhancedOrder enhace(Order order) { + return new EnhancedOrder(order.id(), 1000); + } +} diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java index 205050370..d6932f2e3 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java @@ -29,7 +29,7 @@ public class ForTaskFunction extends ForTask { private Optional> whileClass = Optional.empty(); private Optional> itemClass = Optional.empty(); private Optional> forClass = Optional.empty(); - private Function> collection; + private Function collection; public ForTaskFunction withWhile(LoopPredicate whilePredicate) { return withWhile(toPredicate(whilePredicate)); @@ -119,12 +119,12 @@ private ForTaskFunction withWhile( return this; } - public ForTaskFunction withCollection(Function> collection) { + public ForTaskFunction withCollection(Function> collection) { return withCollection(collection, null); } - public ForTaskFunction withCollection( - Function> collection, Class colArgClass) { + public ForTaskFunction withCollection( + Function> collection, Class colArgClass) { this.collection = collection; this.forClass = Optional.ofNullable(colArgClass); return this;