From f945cefef1604a794bb595053dd606e78cdcb441 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 9 Oct 2025 18:39:10 -0400 Subject: [PATCH 1/5] Fix #866 - Refactor FuncDSL and expose export/input/output to Functions Signed-off-by: Ricardo Zanini --- .../AgentListenConfigurer.java} | 4 +- .../AgentTaskConfigurer.java | 2 +- .../fluent/agentic/dsl/AgentListenSpec.java | 40 ++ .../fluent/agentic/dsl/AgenticDSL.java | 64 +-- .../fluent/agentic/dsl/ListenSpec.java | 72 --- .../fluent/agentic/EmailDrafterIT.java | 7 +- .../fluent/func/FuncCallTaskBuilder.java | 4 +- .../fluent/func/FuncDoTaskBuilder.java | 4 +- .../fluent/func/FuncEmitTaskBuilder.java | 4 +- .../fluent/func/FuncForTaskBuilder.java | 4 +- .../fluent/func/FuncForkTaskBuilder.java | 4 +- .../fluent/func/FuncListenTaskBuilder.java | 4 +- .../fluent/func/FuncListenToBuilder.java | 9 +- .../fluent/func/FuncSwitchTaskBuilder.java | 4 +- .../fluent/func/FuncTaskItemListBuilder.java | 14 +- .../func/configurers/FuncEmitConfigurer.java | 22 + .../func/configurers/FuncEventConfigurer.java | 22 + .../configurers/FuncListenConfigurer.java | 22 + .../FuncPredicateEventConfigurer.java | 2 +- .../func/configurers/FuncTaskConfigurer.java | 21 + .../configurers}/SwitchCaseConfigurer.java | 2 +- .../fluent/func/dsl/BaseFuncListenSpec.java | 52 ++ .../fluent/func/dsl/FuncDSL.java | 195 +++++++ .../fluent/func/dsl/FuncEmitSpec.java | 32 ++ .../fluent/func/dsl/FuncEventFilterSpec.java | 44 ++ .../fluent/func/dsl/FuncListenSpec.java | 37 ++ .../fluent/func/dsl/ReflectionUtils.java | 82 +++ .../fluent/func}/dsl/SwitchCaseSpec.java | 4 +- .../func/dsl/internal/CommonFuncOps.java | 79 +++ .../func/spi/FuncTaskTransformations.java | 63 +++ .../fluent/func/spi/FuncTransformations.java | 58 +- .../fluent/func/FuncDSLTest.java | 508 ++++++++++++++++++ .../fluent/func/JavaWorkflowBuilderTest.java | 281 ---------- ...stractEventConsumptionStrategyBuilder.java | 8 +- .../spec/AbstractEventPropertiesBuilder.java | 7 +- .../fluent/spec/BaseDoTaskBuilder.java | 9 + .../fluent/spec/BaseTaskItemListBuilder.java | 8 +- .../fluent/spec/BaseWorkflowBuilder.java | 48 +- .../spec/EventConsumptionStrategyBuilder.java | 2 +- .../fluent/spec/ListenToBuilder.java | 2 +- .../spec/SubscriptionIteratorBuilder.java | 2 +- .../fluent/spec/TaskBaseBuilder.java | 6 +- .../fluent/spec/TaskItemListBuilder.java | 18 +- .../fluent/spec/dsl/BaseListenSpec.java | 123 +++++ .../fluent/spec/dsl/DSL.java | 11 +- .../fluent/spec/dsl/EmitSpec.java | 10 +- .../fluent/spec/dsl/EventFilterSpec.java | 33 +- .../fluent/spec/dsl/EventSpec.java | 4 +- .../fluent/spec/dsl/ExprEventFilterSpec.java | 40 ++ .../fluent/spec/dsl/ListenSpec.java | 60 +-- .../fluent/spec/spi/OutputFluent.java | 2 +- .../spec/spi/TaskTransformationHandlers.java | 23 + .../spec/spi/TransformationHandlers.java | 4 - 53 files changed, 1657 insertions(+), 529 deletions(-) rename experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/{configurer/ListenConfigurer.java => configurers/AgentListenConfigurer.java} (84%) rename experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/{configurer => configurers}/AgentTaskConfigurer.java (93%) create mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/ListenSpec.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEmitConfigurer.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEventConfigurer.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncListenConfigurer.java rename experimental/fluent/{agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer => func/src/main/java/io/serverlessworkflow/fluent/func/configurers}/FuncPredicateEventConfigurer.java (93%) create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncTaskConfigurer.java rename experimental/fluent/{agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer => func/src/main/java/io/serverlessworkflow/fluent/func/configurers}/SwitchCaseConfigurer.java (93%) create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/BaseFuncListenSpec.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEmitSpec.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncListenSpec.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ReflectionUtils.java rename experimental/fluent/{agentic/src/main/java/io/serverlessworkflow/fluent/agentic => func/src/main/java/io/serverlessworkflow/fluent/func}/dsl/SwitchCaseSpec.java (92%) create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/internal/CommonFuncOps.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTaskTransformations.java create mode 100644 experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java delete mode 100644 experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseListenSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ExprEventFilterSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TaskTransformationHandlers.java diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/ListenConfigurer.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentListenConfigurer.java similarity index 84% rename from experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/ListenConfigurer.java rename to experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentListenConfigurer.java index b8db664c9..78df29218 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/ListenConfigurer.java +++ b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentListenConfigurer.java @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.fluent.agentic.configurer; +package io.serverlessworkflow.fluent.agentic.configurers; import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; import java.util.function.Consumer; @FunctionalInterface -public interface ListenConfigurer extends Consumer {} +public interface AgentListenConfigurer extends Consumer {} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/AgentTaskConfigurer.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentTaskConfigurer.java similarity index 93% rename from experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/AgentTaskConfigurer.java rename to experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentTaskConfigurer.java index 9473eb1ad..d0d2417ed 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/AgentTaskConfigurer.java +++ b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentTaskConfigurer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.fluent.agentic.configurer; +package io.serverlessworkflow.fluent.agentic.configurers; import io.serverlessworkflow.fluent.agentic.AgentDoTaskBuilder; import java.util.function.Consumer; diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java new file mode 100644 index 000000000..1f75daa3e --- /dev/null +++ b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java @@ -0,0 +1,40 @@ +/* + * 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.agentic.dsl; + +import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; +import io.serverlessworkflow.fluent.agentic.configurers.AgentListenConfigurer; +import io.serverlessworkflow.fluent.func.dsl.BaseFuncListenSpec; +import io.serverlessworkflow.fluent.spec.AbstractListenTaskBuilder; + +public final class AgentListenSpec + extends BaseFuncListenSpec + implements AgentListenConfigurer { + + public AgentListenSpec() { + super(AbstractListenTaskBuilder::to); + } + + @Override + protected AgentListenSpec self() { + return this; + } + + @Override + public void accept(AgentListenTaskBuilder agentListenTaskBuilder) { + acceptInto(agentListenTaskBuilder); + } +} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java index ccf77f374..495b26a29 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java +++ b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java @@ -19,12 +19,15 @@ import io.cloudevents.CloudEventData; import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.fluent.agentic.AgentDoTaskBuilder; -import io.serverlessworkflow.fluent.agentic.configurer.AgentTaskConfigurer; -import io.serverlessworkflow.fluent.agentic.configurer.FuncPredicateEventConfigurer; -import io.serverlessworkflow.fluent.agentic.configurer.SwitchCaseConfigurer; +import io.serverlessworkflow.fluent.agentic.configurers.AgentTaskConfigurer; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; +import io.serverlessworkflow.fluent.func.configurers.FuncPredicateEventConfigurer; +import io.serverlessworkflow.fluent.func.configurers.SwitchCaseConfigurer; +import io.serverlessworkflow.fluent.func.dsl.ReflectionUtils; +import io.serverlessworkflow.fluent.func.dsl.SwitchCaseSpec; +import io.serverlessworkflow.fluent.func.dsl.internal.CommonFuncOps; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -33,79 +36,75 @@ public final class AgenticDSL { + private static final CommonFuncOps OPS = new CommonFuncOps() {}; + private AgenticDSL() {} public static Consumer fn( Function function, Class argClass) { - return f -> f.function(function, argClass); + return OPS.fn(function, argClass); } public static Consumer fn(Function function) { - return f -> f.function(function); + return OPS.fn(function); } public static Consumer cases(SwitchCaseConfigurer... cases) { - return s -> { - for (SwitchCaseConfigurer c : cases) { - s.onPredicate(c); - } - }; + return OPS.cases(cases); } - public static SwitchCaseSpec on(Predicate when, Class whenClass) { - return new SwitchCaseSpec().when(when, whenClass); + public static SwitchCaseSpec caseOf(Predicate when, Class whenClass) { + return OPS.caseOf(when, whenClass); } - public static SwitchCaseSpec on(Predicate when) { - return new SwitchCaseSpec().when(when); + public static SwitchCaseSpec caseOf(Predicate when) { + return OPS.caseOf(when); } - public static SwitchCaseConfigurer onDefault(String task) { - return s -> s.then(task); + public static SwitchCaseConfigurer caseDefault(String task) { + return OPS.caseDefault(task); } - public static SwitchCaseConfigurer onDefault(FlowDirectiveEnum directive) { - return s -> s.then(directive); + public static SwitchCaseConfigurer caseDefault(FlowDirectiveEnum directive) { + return OPS.caseDefault(directive); } - public static ListenSpec to() { - return new ListenSpec(); + public static AgentListenSpec to() { + return new AgentListenSpec(); } - public static ListenSpec toOne(String type) { - return new ListenSpec().one(e -> e.type(type)); + public static AgentListenSpec toOne(String type) { + return new AgentListenSpec().one(e -> e.type(type)); } - public static ListenSpec toAll(String... types) { + public static AgentListenSpec toAll(String... types) { FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; for (int i = 0; i < types.length; i++) { events[i] = event(types[i]); } - return new ListenSpec().all(events); + return new AgentListenSpec().all(events); } - public static ListenSpec toAny(String... types) { + public static AgentListenSpec toAny(String... types) { FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; for (int i = 0; i < types.length; i++) { events[i] = event(types[i]); } - return new ListenSpec().any(events); + return new AgentListenSpec().any(events); } public static FuncPredicateEventConfigurer event(String type) { - return e -> e.type(type); + return OPS.event(type); } - // TODO: expand the `event` static ref with more attributes based on community feedback - public static Consumer event( String type, Function function) { - return event -> event.event(e -> e.type(type).data(function)); + return OPS.event(type, function); } public static Consumer event( String type, Function function, Class clazz) { - return event -> event.event(e -> e.type(type).data(function, clazz)); + return OPS.event(type, function, clazz); } // -------- Agentic Workflow Patterns -------- // @@ -148,7 +147,8 @@ public static AgentTaskConfigurer function(Function function, Class } public static AgentTaskConfigurer function(Function function) { - return list -> list.callFn(fn(function)); + Class clazz = ReflectionUtils.inferInputType(function); + return list -> list.callFn(fn(function, clazz)); } public static AgentTaskConfigurer agent(Object agent) { diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/ListenSpec.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/ListenSpec.java deleted file mode 100644 index 7e890ae4e..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/ListenSpec.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.agentic.dsl; - -import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; -import io.serverlessworkflow.fluent.agentic.configurer.FuncPredicateEventConfigurer; -import io.serverlessworkflow.fluent.agentic.configurer.ListenConfigurer; -import io.serverlessworkflow.fluent.func.FuncEventFilterBuilder; -import io.serverlessworkflow.fluent.func.FuncListenToBuilder; -import java.util.Objects; -import java.util.function.Consumer; - -public class ListenSpec implements ListenConfigurer { - - private Consumer strategyStep; - private Consumer untilStep; - - @SuppressWarnings("unchecked") - private static Consumer[] asFilters( - FuncPredicateEventConfigurer[] events) { - Consumer[] filters = new Consumer[events.length]; - for (int i = 0; i < events.length; i++) { - FuncPredicateEventConfigurer ev = Objects.requireNonNull(events[i], "events[" + i + "]"); - filters[i] = f -> f.with(ev); - } - return filters; - } - - public final ListenSpec all(FuncPredicateEventConfigurer... events) { - strategyStep = t -> t.all(asFilters(events)); - return this; - } - - public ListenSpec one(FuncPredicateEventConfigurer e) { - strategyStep = t -> t.one(f -> f.with(e)); - return this; - } - - public final ListenSpec any(FuncPredicateEventConfigurer... events) { - strategyStep = t -> t.any(asFilters(events)); - return this; - } - - public ListenSpec until(String expression) { - untilStep = t -> t.until(expression); - return this; - } - - @Override - public void accept(AgentListenTaskBuilder agentListenTaskBuilder) { - agentListenTaskBuilder.to( - t -> { - strategyStep.accept(t); - if (untilStep != null) { - untilStep.accept(t); - } - }); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java index 35022e279..1e6069c46 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java @@ -18,8 +18,6 @@ import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.cases; import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.event; import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.fn; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.on; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.onDefault; import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.toAny; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; @@ -30,6 +28,7 @@ import io.serverlessworkflow.api.types.EventFilter; import io.serverlessworkflow.api.types.EventProperties; import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL; import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowStatus; import io.serverlessworkflow.impl.jackson.JsonUtils; @@ -68,11 +67,11 @@ void email_drafter_agent() { .switchCase( "needsHumanReview?", cases( - on( + AgenticDSL.caseOf( d -> !EmailPolicies.Decision.AUTO_SEND.equals(d.decision()), PolicyDecision.class) .then("requestReview"), - onDefault("emailFinished"))) + AgenticDSL.caseDefault("emailFinished"))) .emit( "requestReview", event( diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java index 30d874f03..56301a1ed 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java @@ -18,12 +18,12 @@ import io.serverlessworkflow.api.types.func.CallJava; import io.serverlessworkflow.api.types.func.CallTaskJava; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; import java.util.function.Function; public class FuncCallTaskBuilder extends TaskBaseBuilder - implements FuncTransformations, + implements FuncTaskTransformations, ConditionalTaskBuilder { private CallTaskJava callTaskJava; diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java index 613f76a2f..2b03dedf1 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java @@ -17,12 +17,12 @@ import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; import io.serverlessworkflow.fluent.func.spi.FuncDoFluent; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.BaseDoTaskBuilder; import java.util.function.Consumer; public class FuncDoTaskBuilder extends BaseDoTaskBuilder - implements FuncTransformations, + implements FuncTaskTransformations, ConditionalTaskBuilder, FuncDoFluent { diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java index 2db03ae07..5f63d8d46 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java @@ -16,13 +16,13 @@ package io.serverlessworkflow.fluent.func; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.AbstractEmitTaskBuilder; public class FuncEmitTaskBuilder extends AbstractEmitTaskBuilder implements ConditionalTaskBuilder, - FuncTransformations { + FuncTaskTransformations { FuncEmitTaskBuilder() { super(); } 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 d8c63e6f8..98b9883cb 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 @@ -25,7 +25,7 @@ import io.serverlessworkflow.api.types.func.LoopPredicate; import io.serverlessworkflow.api.types.func.LoopPredicateIndex; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; import io.serverlessworkflow.fluent.spec.spi.ForEachTaskFluent; import java.util.ArrayList; @@ -36,7 +36,7 @@ import java.util.function.Function; public class FuncForTaskBuilder extends TaskBaseBuilder - implements FuncTransformations, + implements FuncTaskTransformations, ConditionalTaskBuilder, ForEachTaskFluent { diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java index 372da744f..8db4e1369 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java @@ -22,7 +22,7 @@ import io.serverlessworkflow.api.types.func.CallJava; import io.serverlessworkflow.api.types.func.CallTaskJava; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; import io.serverlessworkflow.fluent.spec.spi.ForkTaskFluent; import java.util.ArrayList; @@ -32,7 +32,7 @@ import java.util.function.Function; public class FuncForkTaskBuilder extends TaskBaseBuilder - implements FuncTransformations, + implements FuncTaskTransformations, ConditionalTaskBuilder, ForkTaskFluent { diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java index b5168f625..a539414e2 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java @@ -19,14 +19,14 @@ import io.serverlessworkflow.api.types.ListenTask; import io.serverlessworkflow.api.types.func.UntilPredicate; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.AbstractListenTaskBuilder; import java.util.function.Predicate; public class FuncListenTaskBuilder extends AbstractListenTaskBuilder implements ConditionalTaskBuilder, - FuncTransformations { + FuncTaskTransformations { private UntilPredicate untilPredicate; diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenToBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenToBuilder.java index a21315cc1..8162d078e 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenToBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenToBuilder.java @@ -20,7 +20,9 @@ import io.serverlessworkflow.api.types.ListenTo; import io.serverlessworkflow.api.types.OneEventConsumptionStrategy; import io.serverlessworkflow.api.types.Until; +import io.serverlessworkflow.api.types.func.UntilPredicate; import io.serverlessworkflow.fluent.spec.AbstractEventConsumptionStrategyBuilder; +import java.util.function.Predicate; public class FuncListenToBuilder extends AbstractEventConsumptionStrategyBuilder< @@ -56,7 +58,12 @@ protected ListenTo getEventConsumptionStrategy() { } @Override - protected void setUntil(Until until) { + protected void setUntilForAny(Until until) { this.listenTo.getAnyEventConsumptionStrategy().setUntil(until); } + + public FuncListenToBuilder until(Predicate predicate, Class predClass) { + this.setUntil(new UntilPredicate().withPredicate(predicate, predClass)); + return this; + } } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSwitchTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSwitchTaskBuilder.java index c4ee2b098..c6d51409c 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSwitchTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSwitchTaskBuilder.java @@ -22,7 +22,7 @@ import io.serverlessworkflow.api.types.SwitchTask; import io.serverlessworkflow.api.types.func.SwitchCaseFunction; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; import io.serverlessworkflow.fluent.spec.spi.SwitchTaskFluent; import java.util.ArrayList; @@ -33,7 +33,7 @@ import java.util.function.Predicate; public class FuncSwitchTaskBuilder extends TaskBaseBuilder - implements FuncTransformations, + implements FuncTaskTransformations, ConditionalTaskBuilder, SwitchTaskFluent { diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java index 6ef8d7b0f..e31faf60f 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java @@ -46,7 +46,7 @@ protected FuncTaskItemListBuilder newItemListBuilder() { @Override public FuncTaskItemListBuilder callFn(String name, Consumer consumer) { - this.requireNameAndConfig(name, consumer); + name = this.defaultNameAndRequireConfig(name, consumer); final FuncCallTaskBuilder callTaskJavaBuilder = new FuncCallTaskBuilder(); consumer.accept(callTaskJavaBuilder); return addTaskItem(new TaskItem(name, new Task().withCallTask(callTaskJavaBuilder.build()))); @@ -59,7 +59,7 @@ public FuncTaskItemListBuilder callFn(Consumer consumer) { @Override public FuncTaskItemListBuilder set(String name, Consumer itemsConfigurer) { - this.requireNameAndConfig(name, itemsConfigurer); + name = this.defaultNameAndRequireConfig(name, itemsConfigurer); final FuncSetTaskBuilder funcSetTaskBuilder = new FuncSetTaskBuilder(); itemsConfigurer.accept(funcSetTaskBuilder); return this.addTaskItem(new TaskItem(name, new Task().withSetTask(funcSetTaskBuilder.build()))); @@ -72,7 +72,7 @@ public FuncTaskItemListBuilder set(String name, String expr) { @Override public FuncTaskItemListBuilder emit(String name, Consumer itemsConfigurer) { - this.requireNameAndConfig(name, itemsConfigurer); + name = this.defaultNameAndRequireConfig(name, itemsConfigurer); final FuncEmitTaskBuilder emitTaskJavaBuilder = new FuncEmitTaskBuilder(); itemsConfigurer.accept(emitTaskJavaBuilder); return this.addTaskItem( @@ -82,7 +82,7 @@ public FuncTaskItemListBuilder emit(String name, Consumer i @Override public FuncTaskItemListBuilder listen( String name, Consumer itemsConfigurer) { - this.requireNameAndConfig(name, itemsConfigurer); + name = this.defaultNameAndRequireConfig(name, itemsConfigurer); final FuncListenTaskBuilder listenTaskJavaBuilder = new FuncListenTaskBuilder(); itemsConfigurer.accept(listenTaskJavaBuilder); return this.addTaskItem( @@ -92,7 +92,7 @@ public FuncTaskItemListBuilder listen( @Override public FuncTaskItemListBuilder forEach( String name, Consumer itemsConfigurer) { - this.requireNameAndConfig(name, itemsConfigurer); + name = this.defaultNameAndRequireConfig(name, itemsConfigurer); final FuncForTaskBuilder forTaskJavaBuilder = new FuncForTaskBuilder(); itemsConfigurer.accept(forTaskJavaBuilder); return this.addTaskItem(new TaskItem(name, new Task().withForTask(forTaskJavaBuilder.build()))); @@ -101,7 +101,7 @@ public FuncTaskItemListBuilder forEach( @Override public FuncTaskItemListBuilder switchCase( String name, Consumer itemsConfigurer) { - this.requireNameAndConfig(name, itemsConfigurer); + name = this.defaultNameAndRequireConfig(name, itemsConfigurer); final FuncSwitchTaskBuilder funcSwitchTaskBuilder = new FuncSwitchTaskBuilder(); itemsConfigurer.accept(funcSwitchTaskBuilder); return this.addTaskItem( @@ -110,7 +110,7 @@ public FuncTaskItemListBuilder switchCase( @Override public FuncTaskItemListBuilder fork(String name, Consumer itemsConfigurer) { - this.requireNameAndConfig(name, itemsConfigurer); + name = this.defaultNameAndRequireConfig(name, itemsConfigurer); final FuncForkTaskBuilder forkTaskJavaBuilder = new FuncForkTaskBuilder(); itemsConfigurer.accept(forkTaskJavaBuilder); return this.addTaskItem( diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEmitConfigurer.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEmitConfigurer.java new file mode 100644 index 000000000..44e5fbd95 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEmitConfigurer.java @@ -0,0 +1,22 @@ +/* + * 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.func.configurers; + +import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface FuncEmitConfigurer extends Consumer {} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEventConfigurer.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEventConfigurer.java new file mode 100644 index 000000000..aac390748 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncEventConfigurer.java @@ -0,0 +1,22 @@ +/* + * 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.func.configurers; + +import io.serverlessworkflow.fluent.func.FuncEventPropertiesBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface FuncEventConfigurer extends Consumer {} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncListenConfigurer.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncListenConfigurer.java new file mode 100644 index 000000000..ee18a05bf --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncListenConfigurer.java @@ -0,0 +1,22 @@ +/* + * 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.func.configurers; + +import io.serverlessworkflow.fluent.func.FuncListenTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface FuncListenConfigurer extends Consumer {} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/FuncPredicateEventConfigurer.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncPredicateEventConfigurer.java similarity index 93% rename from experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/FuncPredicateEventConfigurer.java rename to experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncPredicateEventConfigurer.java index fce8875dd..5507193f6 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/FuncPredicateEventConfigurer.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncPredicateEventConfigurer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.fluent.agentic.configurer; +package io.serverlessworkflow.fluent.func.configurers; import io.serverlessworkflow.fluent.func.FuncPredicateEventPropertiesBuilder; import java.util.function.Consumer; diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncTaskConfigurer.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncTaskConfigurer.java new file mode 100644 index 000000000..e3815ad77 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/FuncTaskConfigurer.java @@ -0,0 +1,21 @@ +/* + * 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.func.configurers; + +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.function.Consumer; + +public interface FuncTaskConfigurer extends Consumer {} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/SwitchCaseConfigurer.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/SwitchCaseConfigurer.java similarity index 93% rename from experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/SwitchCaseConfigurer.java rename to experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/SwitchCaseConfigurer.java index 3e302ff22..55cf25c5a 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurer/SwitchCaseConfigurer.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/configurers/SwitchCaseConfigurer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.fluent.agentic.configurer; +package io.serverlessworkflow.fluent.func.configurers; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; import java.util.function.Consumer; diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/BaseFuncListenSpec.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/BaseFuncListenSpec.java new file mode 100644 index 000000000..6c7f2d110 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/BaseFuncListenSpec.java @@ -0,0 +1,52 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.fluent.func.FuncEventFilterBuilder; +import io.serverlessworkflow.fluent.func.FuncListenToBuilder; +import io.serverlessworkflow.fluent.func.configurers.FuncPredicateEventConfigurer; +import io.serverlessworkflow.fluent.spec.dsl.BaseListenSpec; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public abstract class BaseFuncListenSpec + extends BaseListenSpec< + SELF, LB, FuncListenToBuilder, FuncEventFilterBuilder, FuncPredicateEventConfigurer> { + + protected BaseFuncListenSpec(ToInvoker toInvoker) { + super( + toInvoker, + FuncEventFilterBuilder::with, + // allApplier + (tb, filters) -> tb.all(castFilters(filters)), + // anyApplier + (tb, filters) -> tb.any(castFilters(filters)), + // oneApplier + FuncListenToBuilder::one); + } + + @SuppressWarnings("unchecked") + private static Consumer[] castFilters(Consumer[] arr) { + return (Consumer[]) arr; + } + + public SELF until(Predicate predicate, Class predClass) { + Objects.requireNonNull(predicate, "predicate"); + this.setUntilStep(u -> u.until(predicate, predClass)); + return self(); + } +} 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 new file mode 100644 index 000000000..94768ffc6 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java @@ -0,0 +1,195 @@ +/* + * 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.func.dsl; + +import io.cloudevents.CloudEventData; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncDoTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import io.serverlessworkflow.fluent.func.configurers.FuncPredicateEventConfigurer; +import io.serverlessworkflow.fluent.func.configurers.FuncTaskConfigurer; +import io.serverlessworkflow.fluent.func.configurers.SwitchCaseConfigurer; +import io.serverlessworkflow.fluent.func.dsl.internal.CommonFuncOps; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public final class FuncDSL { + private static final CommonFuncOps OPS = new CommonFuncOps() {}; + + public static Consumer fn( + Function function, Class argClass) { + return OPS.fn(function, argClass); + } + + public static Consumer fn(Function function) { + return f -> f.function(function); + } + + public static Consumer cases(SwitchCaseConfigurer... cases) { + return OPS.cases(cases); + } + + public static SwitchCaseSpec caseOf(Predicate when, Class whenClass) { + return OPS.caseOf(when, whenClass); + } + + public static SwitchCaseSpec caseOf(Predicate when) { + return OPS.caseOf(when); + } + + public static SwitchCaseConfigurer caseDefault(String task) { + return OPS.caseDefault(task); + } + + public static SwitchCaseConfigurer caseDefault(FlowDirectiveEnum directive) { + return OPS.caseDefault(directive); + } + + public static FuncListenSpec to() { + return new FuncListenSpec(); + } + + public static FuncListenSpec toOne(String type) { + return new FuncListenSpec().one(e -> e.type(type)); + } + + public static FuncListenSpec toAll(String... types) { + FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; + for (int i = 0; i < types.length; i++) { + events[i] = event(types[i]); + } + return new FuncListenSpec().all(events); + } + + public static FuncListenSpec toAny(String... types) { + FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; + for (int i = 0; i < types.length; i++) { + events[i] = event(types[i]); + } + return new FuncListenSpec().any(events); + } + + public static Consumer event( + String type, Function function) { + return OPS.event(type, function); + } + + public static Consumer event( + String type, Function function, Class clazz) { + return OPS.event(type, function, clazz); + } + + public static FuncPredicateEventConfigurer event(String type) { + return OPS.event(type); + } + + public static FuncTaskConfigurer function(Function fn) { + Class clazz = ReflectionUtils.inferInputType(fn); + return list -> list.callFn(f -> f.function(fn, clazz)); + } + + public static FuncTaskConfigurer function(Function fn, Class clazz) { + return list -> list.callFn(f -> f.function(fn, clazz)); + } + + // ------------------ tasks ---------------- // + public static Consumer doTasks(FuncTaskConfigurer... steps) { + final Consumer tasks = tasks(steps); + return d -> d.tasks(tasks); + } + + public static Consumer tasks(FuncTaskConfigurer... steps) { + Objects.requireNonNull(steps, "Steps in a tasks are required"); + final List snapshot = List.of(steps.clone()); + return list -> snapshot.forEach(s -> s.accept(list)); + } + + public static FuncTaskConfigurer emit(Consumer emitTask) { + return list -> list.emit(emitTask); + } + + public static FuncTaskConfigurer emit(String type, Function fn) { + return list -> list.emit(event(type, fn)); + } + + public static FuncTaskConfigurer emit( + String name, String type, Function fn) { + return list -> list.emit(name, event(type, fn)); + } + + public static FuncTaskConfigurer listen(FuncListenSpec listen) { + return list -> list.listen(listen); + } + + public static FuncTaskConfigurer listen(String name, FuncListenSpec listen) { + return list -> list.listen(name, listen); + } + + public static FuncTaskConfigurer switchCase( + String taskName, Consumer switchCase) { + return list -> list.switchCase(taskName, switchCase); + } + + public static FuncTaskConfigurer switchCase(Consumer switchCase) { + return list -> list.switchCase(switchCase); + } + + public static FuncTaskConfigurer switchCase(SwitchCaseConfigurer... cases) { + return switchCase(null, cases); + } + + public static FuncTaskConfigurer switchCase(String taskName, SwitchCaseConfigurer... cases) { + Objects.requireNonNull(cases, "cases are required"); + final List snapshot = List.of(cases.clone()); + return list -> list.switchCase(taskName, s -> snapshot.forEach(s::onPredicate)); + } + + // Single predicate -> then task + public static FuncTaskConfigurer switchWhen(Predicate pred, String thenTask) { + return list -> list.switchCase(cases(caseOf(pred).then(thenTask))); + } + + // With default directive + public static FuncTaskConfigurer switchWhenOrElse( + Predicate pred, String thenTask, FlowDirectiveEnum otherwise) { + return list -> + list.switchCase(FuncDSL.cases(caseOf(pred).then(thenTask), caseDefault(otherwise))); + } + + public static FuncTaskConfigurer forEach( + Function> collection, Consumer body) { + return list -> list.forEach(j -> j.collection(collection).tasks(body)); + } + + public static FuncTaskConfigurer forEach( + Collection collection, Consumer body) { + Function> f = ctx -> (Collection) collection; + return list -> list.forEach(j -> j.collection(f).tasks(body)); + } + + // Overload with simple constant collection + public static FuncTaskConfigurer forEach( + List collection, Consumer body) { + return list -> list.forEach(j -> j.collection(ctx -> collection).tasks(body)); + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEmitSpec.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEmitSpec.java new file mode 100644 index 000000000..115419ada --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEmitSpec.java @@ -0,0 +1,32 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; +import io.serverlessworkflow.fluent.func.configurers.FuncEmitConfigurer; + +public class FuncEmitSpec extends FuncEventFilterSpec implements FuncEmitConfigurer { + + @Override + public void accept(FuncEmitTaskBuilder funcEmitTaskBuilder) { + funcEmitTaskBuilder.event(e -> getSteps().forEach(step -> step.accept(e))); + } + + @Override + protected FuncEmitSpec self() { + return this; + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java new file mode 100644 index 000000000..e82cc9089 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java @@ -0,0 +1,44 @@ +/* + * 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.func.dsl; + +import io.cloudevents.CloudEventData; +import io.serverlessworkflow.api.types.func.EventDataFunction; +import io.serverlessworkflow.fluent.func.FuncEventPropertiesBuilder; +import io.serverlessworkflow.fluent.spec.dsl.EventFilterSpec; +import java.util.ArrayList; +import java.util.function.Function; + +public abstract class FuncEventFilterSpec + extends EventFilterSpec { + + FuncEventFilterSpec() { + super(new ArrayList<>()); + } + + /** Sets the event data and the contentType to `application/json` */ + public SELF jsonData(Function function) { + Class clazz = ReflectionUtils.inferInputType(function); + addStep(e -> e.data(new EventDataFunction().withFunction(function, clazz))); + return JSON(); + } + + /** Sets the event data and the contentType to `application/json` */ + public SELF jsonData(Function function, Class clazz) { + addStep(e -> e.data(new EventDataFunction().withFunction(function, clazz))); + return JSON(); + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncListenSpec.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncListenSpec.java new file mode 100644 index 000000000..334a219ac --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncListenSpec.java @@ -0,0 +1,37 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.fluent.func.FuncListenTaskBuilder; +import io.serverlessworkflow.fluent.func.configurers.FuncListenConfigurer; + +public final class FuncListenSpec extends BaseFuncListenSpec + implements FuncListenConfigurer { + + public FuncListenSpec() { + super(FuncListenTaskBuilder::to); + } + + @Override + protected FuncListenSpec self() { + return this; + } + + @Override + public void accept(FuncListenTaskBuilder funcListenTaskBuilder) { + acceptInto(funcListenTaskBuilder); + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ReflectionUtils.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ReflectionUtils.java new file mode 100644 index 000000000..d9f3793a1 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ReflectionUtils.java @@ -0,0 +1,82 @@ +/* + * 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.func.dsl; + +import java.lang.invoke.MethodHandleInfo; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.function.Function; + +/** + * Specially used by {@link Function} parameters in the Java Function. + * + * @see Serialize a Lambda in Java + */ +public final class ReflectionUtils { + + private ReflectionUtils() {} + + @SuppressWarnings("unchecked") + static Optional> safeInferInputType(Function fn) { + try { + Method m = fn.getClass().getDeclaredMethod("writeReplace"); + m.setAccessible(true); + java.lang.invoke.SerializedLambda sl = (java.lang.invoke.SerializedLambda) m.invoke(fn); + + // Owner class of the referenced implementation + String ownerName = sl.getImplClass().replace('/', '.'); + ClassLoader cl = fn.getClass().getClassLoader(); + Class owner = Class.forName(ownerName, false, cl); + + // Parse the impl method descriptor to get raw param types + MethodType mt = MethodType.fromMethodDescriptorString(sl.getImplMethodSignature(), cl); + Class[] params = mt.parameterArray(); + int kind = sl.getImplMethodKind(); + + switch (kind) { + case MethodHandleInfo.REF_invokeStatic: + case MethodHandleInfo.REF_newInvokeSpecial: + // static method or constructor: T is the first parameter + return params.length >= 1 ? Optional.of((Class) params[0]) : Optional.empty(); + + case MethodHandleInfo.REF_invokeVirtual: + case MethodHandleInfo.REF_invokeInterface: + case MethodHandleInfo.REF_invokeSpecial: + // instance method ref like Foo::bar + // For Function, if bar has no params, T is the receiver type (owner). + // If bar has one param, that pattern would usually map to BiFunction, not Function, + // but keep a defensive branch anyway: + return (params.length == 0) + ? Optional.of((Class) owner) + : Optional.of((Class) params[0]); + + default: + return Optional.empty(); + } + } catch (Exception ignore) { + return Optional.empty(); + } + } + + public static Class inferInputType(Function fn) { + return safeInferInputType(fn) + .orElseThrow( + () -> + new IllegalStateException( + "Cannot infer input type from lambda. Pass Class or use a method reference.")); + } +} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/SwitchCaseSpec.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/SwitchCaseSpec.java similarity index 92% rename from experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/SwitchCaseSpec.java rename to experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/SwitchCaseSpec.java index 171d722eb..63d05a16a 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/SwitchCaseSpec.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/SwitchCaseSpec.java @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.fluent.agentic.dsl; +package io.serverlessworkflow.fluent.func.dsl; -import io.serverlessworkflow.fluent.agentic.configurer.SwitchCaseConfigurer; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; +import io.serverlessworkflow.fluent.func.configurers.SwitchCaseConfigurer; import java.util.function.Predicate; public class SwitchCaseSpec implements SwitchCaseConfigurer { diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/internal/CommonFuncOps.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/internal/CommonFuncOps.java new file mode 100644 index 000000000..fe0c05dd7 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/internal/CommonFuncOps.java @@ -0,0 +1,79 @@ +/* + * 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.func.dsl.internal; + +import io.cloudevents.CloudEventData; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; +import io.serverlessworkflow.fluent.func.configurers.FuncPredicateEventConfigurer; +import io.serverlessworkflow.fluent.func.configurers.SwitchCaseConfigurer; +import io.serverlessworkflow.fluent.func.dsl.ReflectionUtils; +import io.serverlessworkflow.fluent.func.dsl.SwitchCaseSpec; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public interface CommonFuncOps { + + default Consumer fn(Function function, Class argClass) { + return f -> f.function(function, argClass); + } + + default Consumer fn(Function function) { + Class clazz = ReflectionUtils.inferInputType(function); + return f -> f.function(function, clazz); + } + + default Consumer cases(SwitchCaseConfigurer... cases) { + return s -> { + for (SwitchCaseConfigurer c : cases) { + s.onPredicate(c); + } + }; + } + + default SwitchCaseSpec caseOf(Predicate when, Class whenClass) { + return new SwitchCaseSpec().when(when, whenClass); + } + + default SwitchCaseSpec caseOf(Predicate when) { + return new SwitchCaseSpec().when(when); + } + + default SwitchCaseConfigurer caseDefault(String task) { + return s -> s.then(task); + } + + default SwitchCaseConfigurer caseDefault(FlowDirectiveEnum directive) { + return s -> s.then(directive); + } + + default Consumer event( + String type, Function function) { + return event -> event.event(e -> e.type(type).data(function)); + } + + default Consumer event( + String type, Function function, Class clazz) { + return event -> event.event(e -> e.type(type).data(function, clazz)); + } + + default FuncPredicateEventConfigurer event(String type) { + return e -> e.type(type); + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTaskTransformations.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTaskTransformations.java new file mode 100644 index 000000000..16d2be288 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTaskTransformations.java @@ -0,0 +1,63 @@ +/* + * 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.func.spi; + +import io.serverlessworkflow.api.types.Export; +import io.serverlessworkflow.api.types.func.ExportAsFunction; +import io.serverlessworkflow.api.types.func.JavaContextFunction; +import io.serverlessworkflow.api.types.func.JavaFilterFunction; +import io.serverlessworkflow.fluent.spec.spi.TaskTransformationHandlers; +import java.util.function.Function; + +public interface FuncTaskTransformations> + extends TaskTransformationHandlers, FuncTransformations { + + @SuppressWarnings("unchecked") + default SELF exportAs(Function function) { + setExport(new Export().withAs(new ExportAsFunction().withFunction(function))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF exportAs(Function function, Class argClass) { + setExport(new Export().withAs(new ExportAsFunction().withFunction(function, argClass))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF exportAs(JavaFilterFunction function) { + setExport(new Export().withAs(new ExportAsFunction().withFunction(function))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF exportAs(JavaFilterFunction function, Class argClass) { + setExport(new Export().withAs(new ExportAsFunction().withFunction(function, argClass))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF exportAs(JavaContextFunction function) { + setExport(new Export().withAs(new ExportAsFunction().withFunction(function))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF exportAs(JavaContextFunction function, Class argClass) { + setExport(new Export().withAs(new ExportAsFunction().withFunction(function, argClass))); + return (SELF) this; + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java index db257dd01..c603b02c3 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java @@ -15,11 +15,11 @@ */ package io.serverlessworkflow.fluent.func.spi; -import io.serverlessworkflow.api.types.Export; import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.Output; -import io.serverlessworkflow.api.types.func.ExportAsFunction; import io.serverlessworkflow.api.types.func.InputFromFunction; +import io.serverlessworkflow.api.types.func.JavaContextFunction; +import io.serverlessworkflow.api.types.func.JavaFilterFunction; import io.serverlessworkflow.api.types.func.OutputAsFunction; import io.serverlessworkflow.fluent.spec.spi.TransformationHandlers; import java.util.function.Function; @@ -27,33 +27,75 @@ public interface FuncTransformations> extends TransformationHandlers { - default SELF exportAsFn(Function function) { - setExport(new Export().withAs(new ExportAsFunction().withFunction(function))); + @SuppressWarnings("unchecked") + default SELF inputFrom(Function function) { + setInput(new Input().withFrom(new InputFromFunction().withFunction(function))); return (SELF) this; } - default SELF exportAsFn(Function function, Class argClass) { - setExport(new Export().withAs(new ExportAsFunction().withFunction(function, argClass))); + @SuppressWarnings("unchecked") + default SELF inputFrom(Function function, Class argClass) { + setInput(new Input().withFrom(new InputFromFunction().withFunction(function, argClass))); return (SELF) this; } - default SELF inputFrom(Function function) { + @SuppressWarnings("unchecked") + default SELF inputFrom(JavaFilterFunction function) { setInput(new Input().withFrom(new InputFromFunction().withFunction(function))); return (SELF) this; } - default SELF inputFrom(Function function, Class argClass) { + @SuppressWarnings("unchecked") + default SELF inputFrom(JavaFilterFunction function, Class argClass) { setInput(new Input().withFrom(new InputFromFunction().withFunction(function, argClass))); return (SELF) this; } + @SuppressWarnings("unchecked") + default SELF inputFrom(JavaContextFunction function) { + setInput(new Input().withFrom(new InputFromFunction().withFunction(function))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF inputFrom(JavaContextFunction function, Class argClass) { + setInput(new Input().withFrom(new InputFromFunction().withFunction(function, argClass))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") default SELF outputAs(Function function) { setOutput(new Output().withAs(new OutputAsFunction().withFunction(function))); return (SELF) this; } + @SuppressWarnings("unchecked") default SELF outputAs(Function function, Class argClass) { setOutput(new Output().withAs(new OutputAsFunction().withFunction(function, argClass))); return (SELF) this; } + + @SuppressWarnings("unchecked") + default SELF outputAs(JavaFilterFunction function) { + setOutput(new Output().withAs(new OutputAsFunction().withFunction(function))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF outputAs(JavaFilterFunction function, Class argClass) { + setOutput(new Output().withAs(new OutputAsFunction().withFunction(function, argClass))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF outputAs(JavaContextFunction function) { + setOutput(new Output().withAs(new OutputAsFunction().withFunction(function))); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + default SELF outputAs(JavaContextFunction function, Class argClass) { + setOutput(new Output().withAs(new OutputAsFunction().withFunction(function, argClass))); + return (SELF) this; + } } diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java new file mode 100644 index 000000000..f3b1065fc --- /dev/null +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java @@ -0,0 +1,508 @@ +/* + * 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.func; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.caseDefault; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.caseOf; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.emit; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.event; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.fn; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.forEach; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.function; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.listen; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.switchCase; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.switchWhen; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.switchWhenOrElse; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.tasks; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.to; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toAll; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toAny; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toOne; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import io.cloudevents.CloudEventData; +import io.cloudevents.core.data.BytesCloudEventData; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import io.serverlessworkflow.fluent.func.dsl.FuncDSL; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** FuncWorkflowBuilder + FuncDSL shortcuts (no JQ set expressions). */ +class FuncDSLTest { + + // ---------- Shortcuts coverage ---------- + + @Test + @DisplayName("function(fn) and function(fn, Class) add CallTask") + void functionOverloads_addCallTask() { + Workflow wf = + FuncWorkflowBuilder.workflow("functionOverloads") + .tasks(function((Long v) -> v + 10, Long.class), function(String::length, String.class)) + .build(); + + List items = wf.getDo(); + assertEquals(2, items.size()); + assertNotNull(items.get(0).getTask().getCallTask()); + assertNotNull(items.get(1).getTask().getCallTask()); + } + + @Test + @DisplayName("emit(event(type, fn)) uses functional event data (inferred type)") + void emit_event_inferredFn() { + Workflow wf = + FuncWorkflowBuilder.workflow("emitFn") + .tasks(emit(event("UserCreated", (String s) -> BytesCloudEventData.wrap(s.getBytes())))) + .build(); + + Task t = wf.getDo().get(0).getTask(); + assertNotNull(t.getEmitTask()); + assertNotNull(t.getEmitTask().getEmit().getEvent()); + assertNotNull(t.getEmitTask().getEmit().getEvent().getWith().getData()); + assertNull(t.getEmitTask().getEmit().getEvent().getWith().getData().getRuntimeExpression()); + } + + @Test + @DisplayName("emit(type, fn) convenience") + void emit_type_fn_shortcut() { + Workflow wf = + FuncWorkflowBuilder.workflow("emitShortcut") + .tasks(emit("Ping", (String s) -> BytesCloudEventData.wrap(s.getBytes()))) + .build(); + + assertNotNull(wf.getDo().get(0).getTask().getEmitTask()); + } + + @Test + @DisplayName("switchCase(cases(on(...), onDefault(...))) produces SwitchTask") + void switchCase_cases_caseOf_and_default() { + Workflow wf = + FuncWorkflowBuilder.workflow("switchShortcuts") + .tasks( + switchCase( + caseOf((Boolean b) -> b).then("thenTask"), caseDefault(FlowDirectiveEnum.END))) + .build(); + + assertNotNull(wf.getDo().get(0).getTask().getSwitchTask()); + } + + @Test + @DisplayName("switchWhen / switchWhenOrElse sugar") + void switchWhen_sugar() { + Workflow wf = + FuncWorkflowBuilder.workflow("switchSugar") + .tasks( + switchWhen((Integer v) -> v > 0, "positive"), + switchWhenOrElse((Integer v) -> v == 0, "zero", FlowDirectiveEnum.END)) + .build(); + + assertNotNull(wf.getDo().get(0).getTask().getSwitchTask()); + assertNotNull(wf.getDo().get(1).getTask().getSwitchTask()); + } + + @Test + @DisplayName("forEach(Collection) with body -> ForTaskFunction") + void forEach_constantCollection() { + List col = List.of("a", "b", "c"); + + Workflow wf = + FuncWorkflowBuilder.workflow("foreachConstant") + .tasks(forEach(col, tasks(function(String::toUpperCase, String.class)))) + .build(); + + Task loopHolder = wf.getDo().get(0).getTask(); + assertNotNull(loopHolder.getForTask()); + ForTaskFunction fn = (ForTaskFunction) loopHolder.getForTask(); + assertEquals(1, fn.getDo().size()); + assertNotNull(fn.getDo().get(0).getTask().getCallTask()); + } + + @Test + @DisplayName("forEach(Function>) matches builder signature and nests body") + void forEach_functionSignature() { + Function> collectionF = ctx -> Arrays.asList(1, 2, 3); + + Workflow wf = + FuncWorkflowBuilder.workflow("foreachFunction") + .tasks(forEach(collectionF, tasks(function(Integer::toHexString, Integer.class)))) + .build(); + + Task loopHolder = wf.getDo().get(0).getTask(); + assertNotNull(loopHolder.getForTask()); + ForTaskFunction fn = (ForTaskFunction) loopHolder.getForTask(); + assertEquals(1, fn.getDo().size()); + assertNotNull(fn.getDo().get(0).getTask().getCallTask()); + } + + @Test + @DisplayName("doTasks(function, emit, switchCase) preserves order") + void doTasks_compositionOrder() { + Workflow wf = + FuncWorkflowBuilder.workflow("composition") + .tasks( + function((Integer x) -> x + 1, Integer.class), + emit("Ping", (String s) -> BytesCloudEventData.wrap(s.getBytes())), + switchCase(caseOf((Boolean b) -> b).then("ok"), FuncDSL.caseDefault("fallback"))) + .build(); + + List items = wf.getDo(); + assertEquals(3, items.size()); + assertNotNull(items.get(0).getTask().getCallTask()); + assertNotNull(items.get(1).getTask().getEmitTask()); + assertNotNull(items.get(2).getTask().getSwitchTask()); + } + + @Test + @DisplayName("fn shortcut can be used directly inside callFn") + void fn_shortcut_in_callFn() { + Workflow wf = + FuncWorkflowBuilder.workflow("fnShortcut") + .tasks(d -> d.callFn("calc", fn((Double v) -> v * 2, Double.class))) + .build(); + + assertNotNull(wf.getDo().get(0).getTask().getCallTask()); + } + + @Test + @DisplayName("Java style forE with collection + whileC builds ForTaskFunction") + void javaForEach_noSet() { + Workflow wf = + FuncWorkflowBuilder.workflow("javaLoopFlow") + .tasks( + d -> + d.forEach( + j -> + j.collection(ctx -> List.of("a", "b", "c")) + .whileC((String val, Object ctx) -> !val.equals("c")) + .tasks( + inner -> + inner.callFn( + c -> { + /* body */ + })))) + .build(); + + List items = wf.getDo(); + assertEquals(1, items.size()); + + TaskItem loopItem = items.get(0); + Task task = loopItem.getTask(); + + assertNotNull(task.getForTask(), "Java ForTaskFunction should be present"); + + ForTaskFunction fn = (ForTaskFunction) task.getForTask(); + assertNotNull(fn.getDo(), "Nested 'do' list inside ForTaskFunction should be populated"); + assertEquals(1, fn.getDo().size()); + Task nested = fn.getDo().get(0).getTask(); + assertNotNull(nested.getCallTask()); + } + + @Test + @DisplayName("Mixed spec and Java loops in one workflow (no set)") + void mixedLoops_noSet() { + Workflow wf = + FuncWorkflowBuilder.workflow("mixed") + .tasks( + d -> + d.forEach(f -> f.each("item").in("$.array")) // spec + .forEach(j -> j.collection(ctx -> List.of(1, 2, 3))) // java + ) + .build(); + + List items = wf.getDo(); + assertEquals(2, items.size()); + + Task specLoop = items.get(0).getTask(); + Task javaLoop = items.get(1).getTask(); + + assertNotNull(specLoop.getForTask()); + assertNotNull(javaLoop.getForTask()); + } + + @Test + @DisplayName("Java functional exportAsFn/inputFrom/outputAs wrappers (no literal set)") + void javaFunctionalIO_noSet() { + Workflow wf = + FuncWorkflowBuilder.workflow("fnIO") + .tasks( + d -> + d.forEach( + j -> + j.collection(ctx -> List.of("x", "y")) + .tasks( + inner -> + inner.callFn( + c -> { + /* calc */ + })) + .exportAs(item -> Map.of("computed", 42)) + .outputAs(item -> Map.of("out", true)))) + .build(); + + assertEquals(1, wf.getDo().size()); + + Task forTaskFnHolder = wf.getDo().get(0).getTask(); + ForTaskFunction fn = (ForTaskFunction) forTaskFnHolder.getForTask(); + assertNotNull(fn); + + List nested = fn.getDo(); + assertEquals(1, nested.size()); + assertNotNull(nested.get(0).getTask().getCallTask()); + + // Structural checks for function-based export/output + assertNotNull(fn.getExport(), "Export should be set via functional variant"); + if (fn.getExport().getAs() != null) { + assertNull(fn.getExport().getAs().getString(), "Export 'as' should not be a literal string"); + } + + if (fn.getOutput() != null && fn.getOutput().getAs() != null) { + assertNull(fn.getOutput().getAs().getString(), "Output 'as' should not be a literal string"); + } + } + + @Test + @DisplayName("callFn task added and retains name + CallTask union") + void callJavaTask_noSet() { + Workflow wf = + FuncWorkflowBuilder.workflow("callJavaFlow") + .tasks( + d -> + d.callFn( + "invokeHandler", + cj -> { + // configure your FuncCallTaskBuilder here + })) + .build(); + + List items = wf.getDo(); + assertEquals(1, items.size()); + TaskItem ti = items.get(0); + + assertEquals("invokeHandler", ti.getName()); + Task task = ti.getTask(); + assertNotNull(task.getCallTask(), "CallTask should be present for callFn"); + } + + @Test + @DisplayName("switchCaseFn (Java variant) without spec set branch") + void switchCaseJava_noSet() { + Workflow wf = + FuncWorkflowBuilder.workflow("switchJava") + .tasks( + d -> + d.switchCase( + sw -> { + // configure Java switch builder (cases / predicates) + })) + .build(); + + List items = wf.getDo(); + assertEquals(1, items.size()); + + Task switchTask = items.get(0).getTask(); + assertNotNull(switchTask.getSwitchTask(), "SwitchTask union should be present"); + } + + @Test + @DisplayName("Composite: java forE + nested callFn (no set)") + void compositeScenario_noSet() { + Workflow wf = + FuncWorkflowBuilder.workflow("composite") + .tasks( + d -> + d.forEach( + j -> + j.collection(ctx -> List.of("a", "b")) + .tasks( + inner -> + inner + .callFn( + cj -> { + /* customizing Java call */ + }) + .callFn( + cj -> { + /* second step */ + })))) + .build(); + + assertEquals(1, wf.getDo().size()); + + Task loopHolder = wf.getDo().get(0).getTask(); + ForTaskFunction fn = (ForTaskFunction) loopHolder.getForTask(); + assertNotNull(fn); + + List nested = fn.getDo(); + assertEquals(2, nested.size()); + + Task nestedCall1 = nested.get(0).getTask(); + Task nestedCall2 = nested.get(1).getTask(); + + assertNotNull(nestedCall1.getCallTask()); + assertNotNull(nestedCall2.getCallTask()); + } + + @Test + @DisplayName("listen(toAny(types...)) produces ListenTask") + void listen_toAny_minimal() { + Workflow wf = + FuncWorkflowBuilder.workflow("listenAny") + .tasks(listen(toAny("org.acme.email.approved", "org.acme.email.denied"))) + .build(); + + assertEquals(1, wf.getDo().size()); + Task t = wf.getDo().get(0).getTask(); + assertNotNull(t.getListenTask(), "ListenTask should be present"); + } + + @Test + @DisplayName("listen(name, toAll(types...).until(expr)) is named and has ListenTask") + void listen_named_toAll_until() { + Workflow wf = + FuncWorkflowBuilder.workflow("listenAllNamed") + .tasks( + listen( + "waitForAll", + toAll("org.acme.signal.one", "org.acme.signal.two") + .until((CloudEventData e) -> e.toString().isEmpty(), CloudEventData.class))) + .build(); + + assertEquals(1, wf.getDo().size()); + TaskItem ti = wf.getDo().get(0); + assertEquals("waitForAll", ti.getName(), "Listen task should preserve given name"); + assertNotNull(ti.getTask().getListenTask(), "ListenTask should be present"); + } + + @Test + @DisplayName("listen(toOne(type)) produces ListenTask") + void listen_toOne() { + Workflow wf = + FuncWorkflowBuilder.workflow("listenOne") + .tasks(listen(toOne("org.acme.email.review.required"))) + .build(); + + assertEquals(1, wf.getDo().size()); + assertNotNull(wf.getDo().get(0).getTask().getListenTask()); + } + + @Test + @DisplayName("emit -> listen -> emit ordering with FuncDSL listen fluent") + void emit_listen_emit_order() { + Workflow wf = + FuncWorkflowBuilder.workflow("emitListenEmit") + .tasks( + emit( + "org.acme.email.started", + (String s) -> BytesCloudEventData.wrap(s.getBytes(UTF_8))), + listen(toAny("org.acme.email.approved", "org.acme.email.denied")), + emit( + "org.acme.email.finished", + (String s) -> BytesCloudEventData.wrap(s.getBytes(UTF_8)))) + .build(); + + List items = wf.getDo(); + assertEquals(3, items.size(), "Three steps should be composed in order"); + + Task first = items.get(0).getTask(); + Task second = items.get(1).getTask(); + Task third = items.get(2).getTask(); + + assertNotNull(first.getEmitTask(), "1st is EmitTask"); + assertNotNull(second.getListenTask(), "2nd is ListenTask"); + assertNotNull(third.getEmitTask(), "3rd is EmitTask"); + } + + @Test + @DisplayName( + "Functional parity of agentic example: callFn -> switchCase -> emit -> listen -> emit") + void functional_parity_example() { + Workflow wf = + FuncWorkflowBuilder.workflow("emailDrafterFunctional") + .tasks( // parseDraft + function(String::trim, String.class), + // policyCheck – pretend it maps string->decision code (0: auto, 1: needs review) + function((String parsed) -> parsed.isEmpty() ? 1 : 0, String.class), + // needsHumanReview? -> requestReview | emailFinished + switchCase( + "needsHumanReview?", + FuncDSL.caseOf((Integer decision) -> decision != 0, Integer.class) + .then("requestReview"), + FuncDSL.caseDefault("emailFinished")), + // emit review request (named branch) + emit( + "requestReview", + "org.acme.email.request", + (String payload) -> BytesCloudEventData.wrap(payload.getBytes(UTF_8))), + // wait for any of approved/denied + listen("waitForReview", toAny("org.acme.email.approved", "org.acme.email.denied")), + // finished event + emit( + "emailFinished", + "org.acme.email.finished", + (String payload) -> BytesCloudEventData.wrap(payload.getBytes(UTF_8)))) + .build(); + + List items = wf.getDo(); + assertEquals(6, items.size()); + + assertNotNull(items.get(0).getTask().getCallTask()); // parseDraft + assertNotNull(items.get(1).getTask().getCallTask()); // policyCheck + assertNotNull(items.get(2).getTask().getSwitchTask()); // needsHumanReview? + assertNotNull(items.get(3).getTask().getEmitTask()); // requestReview + assertNotNull(items.get(4).getTask().getListenTask()); // waitForReview + assertNotNull(items.get(5).getTask().getEmitTask()); // emailFinished + + assertEquals("needsHumanReview?", items.get(2).getName()); + assertEquals("requestReview", items.get(3).getName()); + assertEquals("waitForReview", items.get(4).getName()); + assertEquals("emailFinished", items.get(5).getName()); + } + + @Test + @DisplayName("listen(to().any(...).until(...)) builds ListenTask with chained spec") + void listen_with_to_chaining() { + Workflow wf = + FuncWorkflowBuilder.workflow("listenChained") + .tasks( + listen( + to().any(event("org.acme.sig.one"), event("org.acme.sig.two")) + .until((CloudEventData e) -> e.toString().isEmpty(), CloudEventData.class))) + .build(); + + assertEquals(1, wf.getDo().size()); + assertNotNull(wf.getDo().get(0).getTask().getListenTask()); + assertNotNull( + wf.getDo() + .get(0) + .getTask() + .getListenTask() + .getListen() + .getTo() + .getAnyEventConsumptionStrategy() + .getUntil()); + } +} diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java deleted file mode 100644 index 24de3e7d5..000000000 --- a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * 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.func; - -import static org.junit.jupiter.api.Assertions.*; - -import io.serverlessworkflow.api.types.Document; -import io.serverlessworkflow.api.types.Export; -import io.serverlessworkflow.api.types.Output; -import io.serverlessworkflow.api.types.SetTask; -import io.serverlessworkflow.api.types.Task; -import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.api.types.TaskItem; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.api.types.func.*; -import io.serverlessworkflow.fluent.spec.BaseWorkflowBuilder; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** Tests for FuncWorkflowBuilder + Java DSL extensions. */ -class JavaWorkflowBuilderTest { - - @Test - @DisplayName("Default Java workflow has auto-generated name and default namespace/version") - void testDefaults() { - Workflow wf = FuncWorkflowBuilder.workflow().build(); - assertNotNull(wf); - Document doc = wf.getDocument(); - assertNotNull(doc); - assertEquals(BaseWorkflowBuilder.DEFAULT_NAMESPACE, doc.getNamespace()); - assertEquals(BaseWorkflowBuilder.DEFAULT_VERSION, doc.getVersion()); - assertEquals(BaseWorkflowBuilder.DSL, doc.getDsl()); - assertNotNull(doc.getName()); - } - - @Test - @DisplayName("Spec style forE still works inside Java workflow") - void testSpecForEachInJavaWorkflow() { - Workflow wf = - FuncWorkflowBuilder.workflow("specLoopFlow") - .tasks( - d -> - d.forEach(f -> f.each("pet").in("$.pets")) - .set("markDone", s -> s.expr("$.done = true"))) - .build(); - - List items = wf.getDo(); - assertEquals(2, items.size()); - - TaskItem loopItem = items.get(0); - assertNotNull(loopItem.getTask().getForTask(), "Spec ForTask should be present"); - - TaskItem setItem = items.get(1); - assertNotNull(setItem.getTask().getSetTask()); - SetTask st = setItem.getTask().getSetTask(); - assertEquals("$.done = true", st.getSet().getString()); - } - - @Test - @DisplayName("Java style forE with collection + whileC builds ForTaskFunction") - void testJavaForEach() { - Workflow wf = - FuncWorkflowBuilder.workflow("javaLoopFlow") - .tasks( - d -> - d.forEach( - j -> - j.collection(ctx -> List.of("a", "b", "c")) - .whileC((String val, Object ctx) -> !val.equals("c")) - .tasks( - inner -> inner.set("loopFlag", s -> s.expr("$.flag = true"))))) - .build(); - - List items = wf.getDo(); - assertEquals(1, items.size()); - - TaskItem loopItem = items.get(0); - Task task = loopItem.getTask(); - - assertNotNull(task.getForTask(), "Java ForTaskFunction should be present"); - - // Basic structural checks on nested do inside the function loop - ForTaskFunction fn = (ForTaskFunction) task.getForTask(); - assertNotNull(fn.getDo(), "Nested 'do' list inside ForTaskFunction should be populated"); - assertEquals(1, fn.getDo().size()); - Task nested = fn.getDo().get(0).getTask(); - assertNotNull(nested.getSetTask()); - } - - @Test - @DisplayName("Mixed spec and Java loops in one workflow") - void testMixedLoops() { - Workflow wf = - FuncWorkflowBuilder.workflow("mixed") - .tasks( - d -> - d.forEach(f -> f.each("item").in("$.array")) // spec - .forEach(j -> j.collection(ctx -> List.of(1, 2, 3))) // java - ) - .build(); - - List items = wf.getDo(); - assertEquals(2, items.size()); - - Task specLoop = items.get(0).getTask(); - Task javaLoop = items.get(1).getTask(); - - assertNotNull(specLoop.getForTask()); - assertNotNull(javaLoop.getForTask()); - } - - @Test - @DisplayName("Java functional exportAsFn/inputFrom/outputAs set function wrappers (not literals)") - void testJavaFunctionalIO() { - AtomicBoolean exportCalled = new AtomicBoolean(false); - AtomicBoolean inputCalled = new AtomicBoolean(false); - AtomicBoolean outputCalled = new AtomicBoolean(false); - - Workflow wf = - FuncWorkflowBuilder.workflow("fnIO") - .tasks( - d -> - d.set("init", s -> s.expr("$.x = 1")) - .forEach( - j -> - j.collection( - ctx -> { - inputCalled.set(true); - return List.of("x", "y"); - }) - .tasks(inner -> inner.set("calc", s -> s.expr("$.y = $.x + 1"))) - .exportAsFn( - item -> { - exportCalled.set(true); - return Map.of("computed", 42); - }) - .outputAs( - item -> { - outputCalled.set(true); - return Map.of("out", true); - }))) - .build(); - - // Top-level 'do' structure - assertEquals(2, wf.getDo().size()); - - // Find nested forTaskFunction - Task forTaskFnHolder = wf.getDo().get(1).getTask(); - ForTaskFunction fn = (ForTaskFunction) forTaskFnHolder.getForTask(); - assertNotNull(fn); - - // Inspect nested branche inside the function loop - List nested = fn.getDo(); - assertEquals(1, nested.size()); - TaskBase nestedTask = nested.get(0).getTask().getSetTask(); - assertNotNull(nestedTask); - - // Because functions are likely stored as opaque objects, we check that - // export / output structures exist and are not expression-based. - Export export = fn.getExport(); - assertNotNull(export, "Export should be set via functional variant"); - assertNull( - export.getAs() != null ? export.getAs().getString() : null, - "Export 'as' should not be a plain string when using function variant"); - - Output out = fn.getOutput(); - // If functional output maps to an OutputAsFunction wrapper, adapt the checks: - if (out != null && out.getAs() != null) { - // Expect no literal string if function used - assertNull(out.getAs().getString(), "Output 'as' should not be a literal string"); - } - - // We can't *invoke* lambdas here (unless your runtime exposes them), - // but we verified structural placement. Flipping AtomicBooleans in creation lambdas - // (collection) at least shows one function executed during build (if it is executed now; - // if they are deferred, remove those assertions.) - } - - @Test - @DisplayName("callFn task added and retains name + CallTask union") - void testCallJavaTask() { - Workflow wf = - FuncWorkflowBuilder.workflow("callJavaFlow") - .tasks( - d -> - d.callFn( - "invokeHandler", - cj -> { - // configure your FuncCallTaskBuilder here - // e.g., cj.className("com.acme.Handler").arg("key", "value"); - })) - .build(); - - List items = wf.getDo(); - assertEquals(1, items.size()); - TaskItem ti = items.get(0); - - assertEquals("invokeHandler", ti.getName()); - Task task = ti.getTask(); - assertNotNull(task.getCallTask(), "CallTask should be present for callFn"); - // Additional assertions if FuncCallTaskBuilder populates fields - // e.g., assertEquals("com.acme.Handler", task.getCallTask().getCallJava().getClassName()); - } - - @Test - @DisplayName("switchCaseFn (Java variant) coexists with spec branche") - void testSwitchCaseJava() { - Workflow wf = - FuncWorkflowBuilder.workflow("switchJava") - .tasks( - d -> - d.set("prepare", s -> s.expr("$.ready = true")) - .switchCase( - sw -> { - // configure Java switch builder (cases / predicates) - })) - .build(); - - List items = wf.getDo(); - assertEquals(2, items.size()); - - Task specSet = items.get(0).getTask(); - Task switchTask = items.get(1).getTask(); - - assertNotNull(specSet.getSetTask()); - assertNotNull(switchTask.getSwitchTask(), "SwitchTask union should be present"); - } - - @Test - @DisplayName("Combined: spec set + java forE + callFn inside nested do") - void testCompositeScenario() { - Workflow wf = - FuncWorkflowBuilder.workflow("composite") - .tasks( - d -> - d.set("init", s -> s.expr("$.val = 0")) - .forEach( - j -> - j.collection(ctx -> List.of("a", "b")) - .tasks( - inner -> - inner - .callFn( - cj -> { - // customizing Java call - }) - .set("flag", s -> s.expr("$.flag = true"))))) - .build(); - - assertEquals(2, wf.getDo().size()); - - Task loopHolder = wf.getDo().get(1).getTask(); - ForTaskFunction fn = (ForTaskFunction) loopHolder.getForTask(); - assertNotNull(fn); - - List nested = fn.getDo(); - assertEquals(2, nested.size()); - - Task nestedCall = nested.get(0).getTask(); - Task nestedSet = nested.get(1).getTask(); - - assertNotNull(nestedCall.getCallTask()); - assertNotNull(nestedSet.getSetTask()); - } -} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java index 2857cd7ec..2b5394713 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java @@ -134,12 +134,16 @@ public final T build() { } if (anySet) { - this.setUntil(until); + this.setUntilForAny(until); } return this.getEventConsumptionStrategy(); } protected abstract T getEventConsumptionStrategy(); - protected abstract void setUntil(Until until); + protected abstract void setUntilForAny(Until until); + + protected void setUntil(Until until) { + this.until = until; + } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventPropertiesBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventPropertiesBuilder.java index dc4713997..b9d9baa55 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventPropertiesBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventPropertiesBuilder.java @@ -72,7 +72,12 @@ public SELF data(String expr) { } public SELF data(Object obj) { - eventProperties.setData(new EventData().withObject(obj)); + if (obj instanceof EventData) { + eventProperties.setData((EventData) obj); + } else { + eventProperties.setData(new EventData().withObject(obj)); + } + return self(); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java index 476e0e26f..94e0c8734 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java @@ -16,6 +16,8 @@ package io.serverlessworkflow.fluent.spec; import io.serverlessworkflow.api.types.DoTask; +import java.util.Objects; +import java.util.function.Consumer; public abstract class BaseDoTaskBuilder< SELF extends BaseDoTaskBuilder, LIST extends BaseTaskItemListBuilder> @@ -39,6 +41,13 @@ protected final LIST listBuilder() { return (LIST) itemsListBuilder; } + @SuppressWarnings("unchecked") + public SELF tasks(Consumer itemsConfigurer) { + Objects.requireNonNull(itemsConfigurer, "itemsConfigurer is required"); + itemsConfigurer.accept(this.listBuilder()); + return (SELF) this; + } + public DoTask build() { doTask.setDo(itemsListBuilder.build()); return doTask; diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java index 33a424fc5..d6d2e2292 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.function.Consumer; /** @@ -58,9 +59,12 @@ protected final SELF addTaskItem(TaskItem taskItem) { return self(); } - protected final void requireNameAndConfig(String name, Consumer cfg) { - Objects.requireNonNull(name, "Task name must not be null"); + protected final String defaultNameAndRequireConfig(String name, Consumer cfg) { + if (name == null || name.isBlank()) { + name = UUID.randomUUID().toString(); + } Objects.requireNonNull(cfg, "Configurer must not be null"); + return name; } /** diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java index 993f325f9..b81b2c222 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java @@ -16,14 +16,12 @@ package io.serverlessworkflow.fluent.spec; import io.serverlessworkflow.api.types.Document; -import io.serverlessworkflow.api.types.Export; import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.Output; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.spec.spi.TransformationHandlers; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.function.Consumer; @@ -63,13 +61,6 @@ public void setOutput(Output output) { this.workflow.setOutput(output); } - @Override - public void setExport(Export export) { - // TODO: build another interface with only Output and Input - throw new UnsupportedOperationException( - "export() is not supported on the workflow root; only tasks may export"); - } - @Override public void setInput(Input input) { this.workflow.setInput(input); @@ -89,15 +80,36 @@ public SELF use(Consumer useBuilderConsumer) { } public SELF tasks(Consumer doTaskConsumer) { - final DBuilder doTaskBuilder = newDo(); - doTaskConsumer.accept(doTaskBuilder); - if (this.workflow.getDo() == null) { - this.workflow.setDo(doTaskBuilder.build().getDo()); - } else { - List existingTasks = new ArrayList<>(this.workflow.getDo()); - existingTasks.addAll(doTaskBuilder.build().getDo()); - this.workflow.setDo(Collections.unmodifiableList(existingTasks)); - } + return appendDo(doTaskConsumer); + } + + @SafeVarargs + public final SELF tasks(Consumer... tasks) { + // Snapshot and adapt IListBuilder-consumers into a single DBuilder-consumer + final Consumer configurer = + db -> { + if (tasks == null || tasks.length == 0) return; + for (Consumer c : List.of(tasks.clone())) { + if (c != null) db.tasks(c); + } + }; + return appendDo(configurer); + } + + private SELF appendDo(Consumer configurer) { + if (configurer == null) return self(); + + final DBuilder doBuilder = newDo(); + configurer.accept(doBuilder); + + final List newItems = doBuilder.build().getDo(); + if (newItems == null || newItems.isEmpty()) return self(); + + final List merged = + new ArrayList<>(this.workflow.getDo() != null ? this.workflow.getDo() : List.of()); + merged.addAll(newItems); + + this.workflow.setDo(List.copyOf(merged)); return self(); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java index b4591e0d2..c9bb98834 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java @@ -55,7 +55,7 @@ protected EventConsumptionStrategy getEventConsumptionStrategy() { } @Override - protected void setUntil(Until until) { + protected void setUntilForAny(Until until) { this.eventConsumptionStrategy.getAnyEventConsumptionStrategy().setUntil(until); } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java index ca01805e2..876be8164 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java @@ -54,7 +54,7 @@ protected ListenTo getEventConsumptionStrategy() { } @Override - protected void setUntil(Until until) { + protected void setUntilForAny(Until until) { this.listenTo.getAnyEventConsumptionStrategy().setUntil(until); } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java index 0c9d509bd..fe7201a9a 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java @@ -67,7 +67,7 @@ public SubscriptionIteratorBuilder export(Consumer exportConsu } @Override - public SubscriptionIteratorBuilder exportAs(Object exportAs) { + public SubscriptionIteratorBuilder exportAs(String exportAs) { this.subscriptionIterator.setExport(new ExportBuilder().as(exportAs).build()); return this; } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java index cd6e3a8e2..e7c416951 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java @@ -22,11 +22,11 @@ import io.serverlessworkflow.api.types.Output; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.fluent.spec.spi.OutputFluent; -import io.serverlessworkflow.fluent.spec.spi.TransformationHandlers; +import io.serverlessworkflow.fluent.spec.spi.TaskTransformationHandlers; import java.util.function.Consumer; public abstract class TaskBaseBuilder> - implements TransformationHandlers, OutputFluent { + implements TaskTransformationHandlers, OutputFluent { private TaskBase task; protected TaskBaseBuilder() {} @@ -80,7 +80,7 @@ public T then(String taskName) { return self(); } - public T exportAs(Object exportAs) { + public T exportAs(String exportAs) { this.task.setExport(new ExportBuilder().as(exportAs).build()); return self(); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java index 4c82f62a0..5cfba36c5 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java @@ -45,7 +45,7 @@ protected TaskItemListBuilder newItemListBuilder() { @Override public TaskItemListBuilder set(String name, Consumer itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final SetTaskBuilder setBuilder = new SetTaskBuilder(); itemsConfigurer.accept(setBuilder); return addTaskItem(new TaskItem(name, new Task().withSetTask(setBuilder.build()))); @@ -59,7 +59,7 @@ public TaskItemListBuilder set(String name, final String expr) { @Override public TaskItemListBuilder forEach( String name, Consumer> itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final ForEachTaskBuilder forBuilder = new ForEachTaskBuilder<>(newItemListBuilder()); itemsConfigurer.accept(forBuilder); @@ -68,7 +68,7 @@ public TaskItemListBuilder forEach( @Override public TaskItemListBuilder switchCase(String name, Consumer itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final SwitchTaskBuilder switchBuilder = new SwitchTaskBuilder(); itemsConfigurer.accept(switchBuilder); return addTaskItem(new TaskItem(name, new Task().withSwitchTask(switchBuilder.build()))); @@ -76,7 +76,7 @@ public TaskItemListBuilder switchCase(String name, Consumer i @Override public TaskItemListBuilder raise(String name, Consumer itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final RaiseTaskBuilder raiseBuilder = new RaiseTaskBuilder(); itemsConfigurer.accept(raiseBuilder); return addTaskItem(new TaskItem(name, new Task().withRaiseTask(raiseBuilder.build()))); @@ -84,7 +84,7 @@ public TaskItemListBuilder raise(String name, Consumer itemsCo @Override public TaskItemListBuilder fork(String name, Consumer itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final ForkTaskBuilder forkBuilder = new ForkTaskBuilder(); itemsConfigurer.accept(forkBuilder); return addTaskItem(new TaskItem(name, new Task().withForkTask(forkBuilder.build()))); @@ -92,7 +92,7 @@ public TaskItemListBuilder fork(String name, Consumer itemsConf @Override public TaskItemListBuilder listen(String name, Consumer itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final ListenTaskBuilder listenBuilder = new ListenTaskBuilder(); itemsConfigurer.accept(listenBuilder); return addTaskItem(new TaskItem(name, new Task().withListenTask(listenBuilder.build()))); @@ -100,7 +100,7 @@ public TaskItemListBuilder listen(String name, Consumer items @Override public TaskItemListBuilder emit(String name, Consumer itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final EmitTaskBuilder emitBuilder = new EmitTaskBuilder(); itemsConfigurer.accept(emitBuilder); return addTaskItem(new TaskItem(name, new Task().withEmitTask(emitBuilder.build()))); @@ -109,7 +109,7 @@ public TaskItemListBuilder emit(String name, Consumer itemsConf @Override public TaskItemListBuilder tryCatch( String name, Consumer> itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final TryTaskBuilder tryBuilder = new TryTaskBuilder<>(this.newItemListBuilder()); itemsConfigurer.accept(tryBuilder); @@ -118,7 +118,7 @@ public TaskItemListBuilder tryCatch( @Override public TaskItemListBuilder callHTTP(String name, Consumer itemsConfigurer) { - requireNameAndConfig(name, itemsConfigurer); + name = defaultNameAndRequireConfig(name, itemsConfigurer); final CallHTTPTaskBuilder callHTTPBuilder = new CallHTTPTaskBuilder(); itemsConfigurer.accept(callHTTPBuilder); return addTaskItem( diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseListenSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseListenSpec.java new file mode 100644 index 000000000..fbf45545a --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseListenSpec.java @@ -0,0 +1,123 @@ +/* + * 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.spec.dsl; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Generic base for Listen specs. + * + *

Type params: SELF - fluent self type (the concrete spec) LB - ListenTaskBuilder type (e.g., + * ListenTaskBuilder, AgentListenTaskBuilder, FuncListenTaskBuilder) TB - ListenToBuilder type + * (e.g., ListenToBuilder, FuncListenToBuilder) FB - EventFilterBuilder type (e.g., + * EventFilterBuilder, FuncEventFilterBuilder) EC - Event configurer type (e.g., EventConfigurer, + * FuncPredicateEventConfigurer) + */ +public abstract class BaseListenSpec { + + @FunctionalInterface + public interface ToInvoker { + void to(LB listenTaskBuilder, Consumer toStep); + } + + @FunctionalInterface + public interface WithApplier { + void with(FB filterBuilder, EC eventConfigurer); + } + + @FunctionalInterface + public interface FiltersApplier { + void apply(TB toBuilder, @SuppressWarnings("rawtypes") Consumer[] filters); + } + + @FunctionalInterface + public interface OneFilterApplier { + void apply(TB toBuilder, Consumer filter); + } + + private final ToInvoker toInvoker; + private final WithApplier withApplier; + private final FiltersApplier allApplier; + private final FiltersApplier anyApplier; + private final OneFilterApplier oneApplier; + + private Consumer strategyStep; + private Consumer untilStep; + + protected BaseListenSpec( + ToInvoker toInvoker, + WithApplier withApplier, + FiltersApplier allApplier, + FiltersApplier anyApplier, + OneFilterApplier oneApplier) { + + this.toInvoker = Objects.requireNonNull(toInvoker, "toInvoker"); + this.withApplier = Objects.requireNonNull(withApplier, "withApplier"); + this.allApplier = Objects.requireNonNull(allApplier, "allApplier"); + this.anyApplier = Objects.requireNonNull(anyApplier, "anyApplier"); + this.oneApplier = Objects.requireNonNull(oneApplier, "oneApplier"); + } + + protected abstract SELF self(); + + protected final void setUntilStep(Consumer untilStep) { + this.untilStep = untilStep; + } + + /** Convert EC[] -> Consumer[] that call `filterBuilder.with(event)` */ + @SuppressWarnings("unchecked") + protected final Consumer[] asFilters(EC... events) { + Objects.requireNonNull(events, "events"); + Consumer[] filters = new Consumer[events.length]; + for (int i = 0; i < events.length; i++) { + EC ev = Objects.requireNonNull(events[i], "events[" + i + "]"); + filters[i] = fb -> withApplier.with(fb, ev); + } + return filters; + } + + @SafeVarargs + public final SELF all(EC... events) { + strategyStep = t -> allApplier.apply(t, asFilters(events)); + return self(); + } + + @SafeVarargs + public final SELF any(EC... events) { + strategyStep = t -> anyApplier.apply(t, asFilters(events)); + return self(); + } + + public final SELF one(EC event) { + Objects.requireNonNull(event, "event"); + strategyStep = t -> oneApplier.apply(t, fb -> withApplier.with(fb, event)); + return self(); + } + + /** Concrete 'accept' should delegate here with its concrete LB. */ + protected final void acceptInto(LB listenTaskBuilder) { + Objects.requireNonNull(strategyStep, "listening strategy must be set (all/any/one)"); + toInvoker.to( + listenTaskBuilder, + t -> { + strategyStep.accept(t); + if (untilStep != null) { + untilStep.accept(t); + } + }); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java index 8da5bdbe7..0863ef256 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java @@ -16,6 +16,7 @@ package io.serverlessworkflow.fluent.spec.dsl; import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.fluent.spec.DoTaskBuilder; import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; import io.serverlessworkflow.fluent.spec.ForkTaskBuilder; import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; @@ -236,7 +237,15 @@ public static TasksConfigurer tryCatch(TryConfigurer configurer) { } // ----- Tasks that requires tasks list --// - public static Consumer tasks(TasksConfigurer... steps) { + + /** Main task list to be used in `workflow().tasks()` consumer. */ + public static Consumer doTasks(TasksConfigurer... steps) { + final Consumer tasks = tasks(steps); + return d -> d.tasks(tasks); + } + + /** Task list for tasks that requires it such as `for`, `try`, and so on. */ + public static TasksConfigurer tasks(TasksConfigurer... steps) { Objects.requireNonNull(steps, "Steps in a tasks are required"); final List snapshot = List.of(steps.clone()); return list -> snapshot.forEach(s -> s.accept(list)); diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java index 8c4115bcb..f07dd77c6 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java @@ -17,9 +17,8 @@ import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; import io.serverlessworkflow.fluent.spec.configurers.EmitConfigurer; -import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; -public final class EmitSpec extends EventFilterSpec implements EmitConfigurer { +public final class EmitSpec extends ExprEventFilterSpec implements EmitConfigurer { @Override protected EmitSpec self() { @@ -28,11 +27,6 @@ protected EmitSpec self() { @Override public void accept(EmitTaskBuilder emitTaskBuilder) { - emitTaskBuilder.event( - e -> { - for (EventConfigurer step : steps) { - step.accept(e); - } - }); + emitTaskBuilder.event(e -> getSteps().forEach(step -> step.accept(e))); } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java index 7c47a07b7..b4b4d211e 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java @@ -15,21 +15,32 @@ */ package io.serverlessworkflow.fluent.spec.dsl; -import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; +import io.serverlessworkflow.fluent.spec.AbstractEventPropertiesBuilder; import java.net.URI; import java.time.Instant; -import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Map; import java.util.UUID; +import java.util.function.Consumer; -public abstract class EventFilterSpec { +public abstract class EventFilterSpec> { - protected final List steps = new ArrayList<>(); + private final List> steps; + + protected EventFilterSpec(List> steps) { + this.steps = steps; + } protected abstract SELF self(); + protected void addStep(Consumer step) { + steps.add(step); + } + + protected List> getSteps() { + return steps; + } + public SELF type(String eventType) { steps.add(e -> e.type(eventType)); return self(); @@ -53,18 +64,6 @@ public SELF JSON() { return self(); } - /** Sets the event data and the contentType to `application/json` */ - public SELF jsonData(String expr) { - steps.add(e -> e.data(expr)); - return JSON(); - } - - /** Sets the event data and the contentType to `application/json` */ - public SELF jsonData(Map data) { - steps.add(e -> e.data(data)); - return JSON(); - } - public SELF source(String source) { steps.add(e -> e.source(source)); return self(); diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java index a7a41a80c..3e93c9d2a 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java @@ -18,7 +18,7 @@ import io.serverlessworkflow.fluent.spec.EventPropertiesBuilder; import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; -public final class EventSpec extends EventFilterSpec implements EventConfigurer { +public final class EventSpec extends ExprEventFilterSpec implements EventConfigurer { @Override protected EventSpec self() { @@ -27,6 +27,6 @@ protected EventSpec self() { @Override public void accept(EventPropertiesBuilder eventPropertiesBuilder) { - steps.forEach(step -> step.accept(eventPropertiesBuilder)); + getSteps().forEach(step -> step.accept(eventPropertiesBuilder)); } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ExprEventFilterSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ExprEventFilterSpec.java new file mode 100644 index 000000000..4d4850385 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ExprEventFilterSpec.java @@ -0,0 +1,40 @@ +/* + * 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.spec.dsl; + +import io.serverlessworkflow.fluent.spec.EventPropertiesBuilder; +import java.util.ArrayList; +import java.util.Map; + +public abstract class ExprEventFilterSpec + extends EventFilterSpec { + + ExprEventFilterSpec() { + super(new ArrayList<>()); + } + + /** Sets the event data and the contentType to `application/json` */ + public SELF jsonData(String expr) { + addStep(e -> e.data(expr)); + return JSON(); + } + + /** Sets the event data and the contentType to `application/json` */ + public SELF jsonData(Map data) { + addStep(e -> e.data(data)); + return JSON(); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java index ca538911f..56d7a38fb 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java @@ -15,57 +15,53 @@ */ package io.serverlessworkflow.fluent.spec.dsl; +import io.serverlessworkflow.fluent.spec.AbstractEventConsumptionStrategyBuilder; +import io.serverlessworkflow.fluent.spec.AbstractEventFilterBuilder; +import io.serverlessworkflow.fluent.spec.AbstractListenTaskBuilder; import io.serverlessworkflow.fluent.spec.EventFilterBuilder; import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; import io.serverlessworkflow.fluent.spec.ListenToBuilder; import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; -import io.serverlessworkflow.fluent.spec.configurers.ListenConfigurer; import java.util.Objects; import java.util.function.Consumer; -public final class ListenSpec implements ListenConfigurer { +public final class ListenSpec + extends BaseListenSpec< + ListenSpec, ListenTaskBuilder, ListenToBuilder, EventFilterBuilder, EventConfigurer> + implements io.serverlessworkflow.fluent.spec.configurers.ListenConfigurer { - private Consumer strategyStep; - private Consumer untilStep; - - @SuppressWarnings("unchecked") - private static Consumer[] asFilters(EventConfigurer[] events) { - Consumer[] filters = new Consumer[events.length]; - for (int i = 0; i < events.length; i++) { - EventConfigurer ev = Objects.requireNonNull(events[i], "events[" + i + "]"); - filters[i] = f -> f.with(ev); - } - return filters; - } - - public ListenSpec all(EventConfigurer... events) { - strategyStep = t -> t.all(asFilters(events)); - return this; + public ListenSpec() { + super( + // toInvoker + AbstractListenTaskBuilder::to, + // withApplier + AbstractEventFilterBuilder::with, + // allApplier + (tb, filters) -> tb.all(castFilters(filters)), + // anyApplier + (tb, filters) -> tb.any(castFilters(filters)), + // oneApplier + AbstractEventConsumptionStrategyBuilder::one); } - public ListenSpec one(EventConfigurer e) { - strategyStep = t -> t.one(f -> f.with(e)); - return this; + @SuppressWarnings("unchecked") + private static Consumer[] castFilters(Consumer[] arr) { + return (Consumer[]) arr; } - public ListenSpec any(EventConfigurer... events) { - strategyStep = t -> t.any(asFilters(events)); + @Override + protected ListenSpec self() { return this; } public ListenSpec until(String expression) { - untilStep = t -> t.until(expression); - return this; + Objects.requireNonNull(expression, "expression"); + this.setUntilStep(u -> u.until(expression)); + return self(); } @Override public void accept(ListenTaskBuilder listenTaskBuilder) { - listenTaskBuilder.to( - t -> { - strategyStep.accept(t); - if (untilStep != null) { - untilStep.accept(t); - } - }); + acceptInto(listenTaskBuilder); } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java index cc49a43cd..b66bbd754 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java @@ -25,5 +25,5 @@ public interface OutputFluent { SELF export(Consumer exportConsumer); - SELF exportAs(Object exportAs); + SELF exportAs(String exportAs); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TaskTransformationHandlers.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TaskTransformationHandlers.java new file mode 100644 index 000000000..531e02b9a --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TaskTransformationHandlers.java @@ -0,0 +1,23 @@ +/* + * 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.spec.spi; + +import io.serverlessworkflow.api.types.Export; + +public interface TaskTransformationHandlers extends TransformationHandlers { + + void setExport(final Export export); +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TransformationHandlers.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TransformationHandlers.java index 5894109f6..c4183ef3a 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TransformationHandlers.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/TransformationHandlers.java @@ -15,15 +15,11 @@ */ package io.serverlessworkflow.fluent.spec.spi; -import io.serverlessworkflow.api.types.Export; import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.Output; public interface TransformationHandlers { - void setOutput(final Output output); - void setExport(final Export export); - void setInput(final Input input); } From aa6a329b68b7283f9528e732c97120a4f61db29f Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 14 Oct 2025 19:32:26 -0400 Subject: [PATCH 2/5] Review FuncDSL Signed-off-by: Ricardo Zanini --- experimental/fluent/func/pom.xml | 4 + .../fluent/func/FuncSetTaskBuilder.java | 19 +- .../fluent/func/dsl/EmitStep.java | 51 ++ .../fluent/func/dsl/FuncCallStep.java | 56 ++ .../fluent/func/dsl/FuncDSL.java | 94 +++- .../fluent/func/dsl/FuncEventFilterSpec.java | 26 + .../fluent/func/dsl/ListenStep.java | 51 ++ .../fluent/func/dsl/Step.java | 182 +++++++ .../func/spi/ConditionalTaskBuilder.java | 2 + .../fluent/func/FuncDSLTest.java | 500 +++--------------- experimental/fluent/pom.xml | 5 + .../fluent/spec/SetTaskBuilder.java | 14 +- .../fluent/spec/dsl/EventFilterSpec.java | 10 + 13 files changed, 570 insertions(+), 444 deletions(-) create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/EmitStep.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ListenStep.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java diff --git a/experimental/fluent/func/pom.xml b/experimental/fluent/func/pom.xml index 6f2ea803b..9b652a180 100644 --- a/experimental/fluent/func/pom.xml +++ b/experimental/fluent/func/pom.xml @@ -21,6 +21,10 @@ io.serverlessworkflow serverlessworkflow-experimental-types + + io.serverlessworkflow + serverlessworkflow-impl-json + io.serverlessworkflow serverlessworkflow-fluent-spec diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSetTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSetTaskBuilder.java index fc9753b0a..a9d1bd6cf 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSetTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncSetTaskBuilder.java @@ -15,11 +15,28 @@ */ package io.serverlessworkflow.fluent.func; +import io.serverlessworkflow.api.types.Set; +import io.serverlessworkflow.api.types.SetTask; +import io.serverlessworkflow.api.types.func.MapSetTaskConfiguration; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; import io.serverlessworkflow.fluent.spec.SetTaskBuilder; +import java.util.Map; public class FuncSetTaskBuilder extends SetTaskBuilder implements ConditionalTaskBuilder { - FuncSetTaskBuilder() {} + private final SetTask task; + + FuncSetTaskBuilder() { + this.task = new SetTask(); + this.setTask(task); + } + + public FuncSetTaskBuilder expr(Map map) { + if (this.task.getSet() == null) { + this.task.setSet(new Set()); + } + this.task.getSet().withSetTaskConfiguration(new MapSetTaskConfiguration(map)); + return this; + } } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/EmitStep.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/EmitStep.java new file mode 100644 index 000000000..89c2402d4 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/EmitStep.java @@ -0,0 +1,51 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.Objects; +import java.util.function.Consumer; + +/** Chainable emit step; applies FuncEmitSpec then queued export/when. */ +public final class EmitStep extends Step { + + private final String name; // nullable + private final Consumer cfg; + + EmitStep(String name, Consumer cfg) { + this.name = name; + this.cfg = Objects.requireNonNull(cfg, "cfg"); + } + + @Override + protected void configure(FuncTaskItemListBuilder list, Consumer postApply) { + if (name == null) { + list.emit( + e -> { + cfg.accept(e); + postApply.accept(e); + }); + } else { + list.emit( + name, + e -> { + cfg.accept(e); + postApply.accept(e); + }); + } + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java new file mode 100644 index 000000000..d4cff4352 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java @@ -0,0 +1,56 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.function.Consumer; +import java.util.function.Function; + +// FuncCallStep +public final class FuncCallStep extends Step, FuncCallTaskBuilder> { + private final String name; // may be null + private final Function fn; + private final Class argClass; + + FuncCallStep(Function fn, Class argClass) { + this(null, fn, argClass); + } + + FuncCallStep(String name, Function fn, Class argClass) { + this.name = name; + this.fn = fn; + this.argClass = argClass; + } + + @Override + protected void configure(FuncTaskItemListBuilder list, Consumer post) { + if (name == null) { + list.callFn( + cb -> { + cb.function(fn, argClass); + post.accept(cb); + }); + } else { + list.callFn( + name, + cb -> { + cb.function(fn, argClass); + post.accept(cb); + }); + } + } +} 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 94768ffc6..9a8bb0464 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 @@ -18,7 +18,6 @@ import io.cloudevents.CloudEventData; import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; -import io.serverlessworkflow.fluent.func.FuncDoTaskBuilder; import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; @@ -28,6 +27,7 @@ import io.serverlessworkflow.fluent.func.dsl.internal.CommonFuncOps; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; @@ -99,50 +99,90 @@ public static Consumer event( return OPS.event(type, function, clazz); } + /** Emit a JSON CloudEvent (PojoCloudEventData) from a POJO payload. */ + public static Consumer eventJson(String type, Class clazz) { + return b -> new FuncEmitSpec().type(type).jsonData(clazz).accept(b); + } + + public static Consumer eventBytes( + String type, Function serializer, Class clazz) { + return b -> new FuncEmitSpec().type(type).bytesData(serializer, clazz).accept(b); + } + + public static Consumer eventBytesUtf8(String type) { + return b -> new FuncEmitSpec().type(type).bytesDataUtf8().accept(b); + } + public static FuncPredicateEventConfigurer event(String type) { return OPS.event(type); } - public static FuncTaskConfigurer function(Function fn) { + public static FuncCallStep function(Function fn, Class clazz) { + return new FuncCallStep<>(fn, clazz); + } + + public static FuncCallStep function(Function fn) { Class clazz = ReflectionUtils.inferInputType(fn); - return list -> list.callFn(f -> f.function(fn, clazz)); + return new FuncCallStep<>(fn, clazz); } - public static FuncTaskConfigurer function(Function fn, Class clazz) { - return list -> list.callFn(f -> f.function(fn, clazz)); + public static FuncCallStep function(String name, Function fn) { + Class clazz = ReflectionUtils.inferInputType(fn); + return new FuncCallStep<>(name, fn, clazz); } - // ------------------ tasks ---------------- // - public static Consumer doTasks(FuncTaskConfigurer... steps) { - final Consumer tasks = tasks(steps); - return d -> d.tasks(tasks); + public static FuncCallStep function(String name, Function fn, Class clazz) { + return new FuncCallStep<>(name, fn, clazz); } + // ------------------ tasks ---------------- // + public static Consumer tasks(FuncTaskConfigurer... steps) { Objects.requireNonNull(steps, "Steps in a tasks are required"); final List snapshot = List.of(steps.clone()); return list -> snapshot.forEach(s -> s.accept(list)); } - public static FuncTaskConfigurer emit(Consumer emitTask) { - return list -> list.emit(emitTask); + public static EmitStep emit(Consumer cfg) { + return new EmitStep(null, cfg); + } + + public static EmitStep emit(String name, Consumer cfg) { + return new EmitStep(name, cfg); + } + + public static EmitStep emit(String type, Function fn) { + // `event(type, fn)` is your Consumer for EMIT + return new EmitStep(null, event(type, fn)); + } + + public static EmitStep emit(String name, String type, Function fn) { + return new EmitStep(name, event(type, fn)); + } + + public static EmitStep emit( + String name, String type, Function serializer, Class clazz) { + return new EmitStep(name, eventBytes(type, serializer, clazz)); + } + + public static EmitStep emit(String type, Function serializer, Class clazz) { + return new EmitStep(null, eventBytes(type, serializer, clazz)); } - public static FuncTaskConfigurer emit(String type, Function fn) { - return list -> list.emit(event(type, fn)); + public static EmitStep emitJson(String type, Class clazz) { + return new EmitStep(null, eventJson(type, clazz)); } - public static FuncTaskConfigurer emit( - String name, String type, Function fn) { - return list -> list.emit(name, event(type, fn)); + public static EmitStep emitJson(String name, String type, Class clazz) { + return new EmitStep(name, eventJson(type, clazz)); } - public static FuncTaskConfigurer listen(FuncListenSpec listen) { - return list -> list.listen(listen); + public static ListenStep listen(FuncListenSpec spec) { + return new ListenStep(null, spec); } - public static FuncTaskConfigurer listen(String name, FuncListenSpec listen) { - return list -> list.listen(name, listen); + public static ListenStep listen(String name, FuncListenSpec spec) { + return new ListenStep(name, spec); } public static FuncTaskConfigurer switchCase( @@ -176,6 +216,12 @@ public static FuncTaskConfigurer switchWhenOrElse( list.switchCase(FuncDSL.cases(caseOf(pred).then(thenTask), caseDefault(otherwise))); } + public static FuncTaskConfigurer switchWhenOrElse( + Predicate pred, String thenTask, String otherwiseTask) { + return list -> + list.switchCase(FuncDSL.cases(caseOf(pred).then(thenTask), caseDefault(otherwiseTask))); + } + public static FuncTaskConfigurer forEach( Function> collection, Consumer body) { return list -> list.forEach(j -> j.collection(collection).tasks(body)); @@ -192,4 +238,12 @@ public static FuncTaskConfigurer forEach( List collection, Consumer body) { return list -> list.forEach(j -> j.collection(ctx -> collection).tasks(body)); } + + public static FuncTaskConfigurer set(String expr) { + return list -> list.set(expr); + } + + public static FuncTaskConfigurer set(Map map) { + return list -> list.set(s -> s.expr(map)); + } } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java index e82cc9089..1e254467f 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncEventFilterSpec.java @@ -16,9 +16,13 @@ package io.serverlessworkflow.fluent.func.dsl; import io.cloudevents.CloudEventData; +import io.cloudevents.core.data.BytesCloudEventData; +import io.cloudevents.core.data.PojoCloudEventData; import io.serverlessworkflow.api.types.func.EventDataFunction; import io.serverlessworkflow.fluent.func.FuncEventPropertiesBuilder; import io.serverlessworkflow.fluent.spec.dsl.EventFilterSpec; +import io.serverlessworkflow.impl.jackson.JsonUtils; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.function.Function; @@ -36,9 +40,31 @@ public SELF jsonData(Function function) { return JSON(); } + /** Sets the event data and the contentType to `application/octet-stream` */ + public SELF bytesData(Function serializer, Class clazz) { + addStep(e -> e.data(payload -> BytesCloudEventData.wrap(serializer.apply(payload)), clazz)); + return OCTET_STREAM(); + } + + public SELF bytesDataUtf8() { + return bytesData((String s) -> s.getBytes(StandardCharsets.UTF_8), String.class); + } + /** Sets the event data and the contentType to `application/json` */ public SELF jsonData(Function function, Class clazz) { addStep(e -> e.data(new EventDataFunction().withFunction(function, clazz))); return JSON(); } + + /** JSON with default mapper (PojoCloudEventData + application/json). */ + public SELF jsonData(Class clazz) { + addStep( + e -> + e.data( + payload -> + PojoCloudEventData.wrap( + payload, p -> JsonUtils.mapper().writeValueAsString(p).getBytes()), + clazz)); + return JSON(); + } } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ListenStep.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ListenStep.java new file mode 100644 index 000000000..debd3b968 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ListenStep.java @@ -0,0 +1,51 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.fluent.func.FuncListenTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.function.Consumer; + +/** Chainable listen step; applies FuncListenSpec then queued export/when. */ +public final class ListenStep extends Step { + + private final String name; // nullable + private final FuncListenSpec spec; + + ListenStep(String name, FuncListenSpec spec) { + this.name = name; + this.spec = spec; + } + + @Override + protected void configure( + FuncTaskItemListBuilder list, Consumer postApply) { + if (name == null) { + list.listen( + lb -> { + spec.accept(lb); + postApply.accept(lb); + }); + } else { + list.listen( + name, + lb -> { + spec.accept(lb); + postApply.accept(lb); + }); + } + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java new file mode 100644 index 000000000..07d14104b --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java @@ -0,0 +1,182 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.api.types.func.JavaContextFunction; +import io.serverlessworkflow.api.types.func.JavaFilterFunction; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import io.serverlessworkflow.fluent.func.configurers.FuncTaskConfigurer; +import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; +import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +/** A deferred configurer that can chain when/inputFrom/outputAs/exportAs and apply them later. */ +abstract class Step, B> implements FuncTaskConfigurer { + + private final List> postConfigurers = new ArrayList<>(); + + @SuppressWarnings("unchecked") + protected final SELF self() { + return (SELF) this; + } + + // ---------- ConditionalTaskBuilder passthroughs ---------- + + /** Queue a ConditionalTaskBuilder#when(Predicate) to be applied on the concrete builder. */ + public SELF when(Predicate predicate) { + postConfigurers.add(b -> ((ConditionalTaskBuilder) b).when(predicate)); + return self(); + } + + /** Queue a ConditionalTaskBuilder#when(Predicate, Class) to be applied later. */ + public SELF when(Predicate predicate, Class argClass) { + postConfigurers.add(b -> ((ConditionalTaskBuilder) b).when(predicate, argClass)); + return self(); + } + + // ---------- FuncTaskTransformations passthroughs: exportAs ---------- + + /** Queue a FuncTaskTransformations#exportAs(Function) to be applied later. */ + public SELF exportAs(Function function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#exportAs(Function, Class) to be applied later. */ + public SELF exportAs(Function function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function, argClass)); + return self(); + } + + /** Queue a FuncTaskTransformations#exportAs(JavaFilterFunction) to be applied later. */ + public SELF exportAs(JavaFilterFunction function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#exportAs(JavaFilterFunction, Class) to be applied later. */ + public SELF exportAs(JavaFilterFunction function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function, argClass)); + return self(); + } + + /** Queue a FuncTaskTransformations#exportAs(JavaContextFunction) to be applied later. */ + public SELF exportAs(JavaContextFunction function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#exportAs(JavaContextFunction, Class) to be applied later. */ + public SELF exportAs(JavaContextFunction function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function, argClass)); + return self(); + } + + // ---------- FuncTaskTransformations passthroughs: outputAs ---------- + + /** Queue a FuncTaskTransformations#outputAs(Function) to be applied later. */ + public SELF outputAs(Function function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#outputAs(Function, Class) to be applied later. */ + public SELF outputAs(Function function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function, argClass)); + return self(); + } + + /** Queue a FuncTaskTransformations#outputAs(JavaFilterFunction) to be applied later. */ + public SELF outputAs(JavaFilterFunction function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#outputAs(JavaFilterFunction, Class) to be applied later. */ + public SELF outputAs(JavaFilterFunction function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function, argClass)); + return self(); + } + + /** Queue a FuncTaskTransformations#outputAs(JavaContextFunction) to be applied later. */ + public SELF outputAs(JavaContextFunction function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#outputAs(JavaContextFunction, Class) to be applied later. */ + public SELF outputAs(JavaContextFunction function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function, argClass)); + return self(); + } + + // ---------- FuncTaskTransformations passthroughs: inputFrom ---------- + + /** Queue a FuncTaskTransformations#inputFrom(Function) to be applied later. */ + public SELF inputFrom(Function function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#inputFrom(Function, Class) to be applied later. */ + public SELF inputFrom(Function function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function, argClass)); + return self(); + } + + /** Queue a FuncTaskTransformations#inputFrom(JavaFilterFunction) to be applied later. */ + public SELF inputFrom(JavaFilterFunction function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#inputFrom(JavaFilterFunction, Class) to be applied later. */ + public SELF inputFrom(JavaFilterFunction function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function, argClass)); + return self(); + } + + /** Queue a FuncTaskTransformations#inputFrom(JavaContextFunction) to be applied later. */ + public SELF inputFrom(JavaContextFunction function) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function)); + return self(); + } + + /** Queue a FuncTaskTransformations#inputFrom(JavaContextFunction, Class) to be applied later. */ + public SELF inputFrom(JavaContextFunction function, Class argClass) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function, argClass)); + return self(); + } + + // ---------- wiring into the underlying list/builder ---------- + + @Override + public final void accept(FuncTaskItemListBuilder list) { + configure(list, this::applyPost); + } + + /** Implement per-step to attach to the correct builder and run {@code post} at the end. */ + protected abstract void configure(FuncTaskItemListBuilder list, Consumer post); + + /** Applies all queued post-configurers to the concrete builder. */ + private void applyPost(B builder) { + for (Consumer c : postConfigurers) c.accept(builder); + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/ConditionalTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/ConditionalTaskBuilder.java index 383bf7f33..6c4ca97ab 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/ConditionalTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/ConditionalTaskBuilder.java @@ -24,11 +24,13 @@ public interface ConditionalTaskBuilder { TaskBase getTask(); + @SuppressWarnings("unchecked") default SELF when(Predicate predicate) { ConditionalTaskBuilderHelper.setMetadata(getTask(), predicate); return (SELF) this; } + @SuppressWarnings("unchecked") default SELF when(Predicate predicate, Class argClass) { Objects.requireNonNull(argClass); ConditionalTaskBuilderHelper.setMetadata(getTask(), new TypedPredicate<>(predicate, argClass)); diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java index f3b1065fc..eed79dcd7 100644 --- a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java @@ -15,494 +15,162 @@ */ package io.serverlessworkflow.fluent.func; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.caseDefault; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.caseOf; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.emit; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.event; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.fn; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.forEach; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.function; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.listen; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.switchCase; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.switchWhen; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.switchWhenOrElse; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.tasks; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.to; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toAll; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toAny; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toOne; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; -import io.cloudevents.CloudEventData; import io.cloudevents.core.data.BytesCloudEventData; -import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.api.types.Export; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.api.types.func.ForTaskFunction; -import io.serverlessworkflow.fluent.func.dsl.FuncDSL; -import java.util.Arrays; -import java.util.Collection; +import io.serverlessworkflow.api.types.func.CallJava; +import io.serverlessworkflow.api.types.func.JavaFilterFunction; +import io.serverlessworkflow.fluent.func.dsl.FuncEmitSpec; +import io.serverlessworkflow.fluent.func.dsl.FuncListenSpec; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -/** FuncWorkflowBuilder + FuncDSL shortcuts (no JQ set expressions). */ +/** Tests for Step chaining (exportAs/when) over function/emit/listen. */ class FuncDSLTest { - // ---------- Shortcuts coverage ---------- - - @Test - @DisplayName("function(fn) and function(fn, Class) add CallTask") - void functionOverloads_addCallTask() { - Workflow wf = - FuncWorkflowBuilder.workflow("functionOverloads") - .tasks(function((Long v) -> v + 10, Long.class), function(String::length, String.class)) - .build(); - - List items = wf.getDo(); - assertEquals(2, items.size()); - assertNotNull(items.get(0).getTask().getCallTask()); - assertNotNull(items.get(1).getTask().getCallTask()); - } - - @Test - @DisplayName("emit(event(type, fn)) uses functional event data (inferred type)") - void emit_event_inferredFn() { - Workflow wf = - FuncWorkflowBuilder.workflow("emitFn") - .tasks(emit(event("UserCreated", (String s) -> BytesCloudEventData.wrap(s.getBytes())))) - .build(); - - Task t = wf.getDo().get(0).getTask(); - assertNotNull(t.getEmitTask()); - assertNotNull(t.getEmitTask().getEmit().getEvent()); - assertNotNull(t.getEmitTask().getEmit().getEvent().getWith().getData()); - assertNull(t.getEmitTask().getEmit().getEvent().getWith().getData().getRuntimeExpression()); - } - - @Test - @DisplayName("emit(type, fn) convenience") - void emit_type_fn_shortcut() { - Workflow wf = - FuncWorkflowBuilder.workflow("emitShortcut") - .tasks(emit("Ping", (String s) -> BytesCloudEventData.wrap(s.getBytes()))) - .build(); - - assertNotNull(wf.getDo().get(0).getTask().getEmitTask()); - } - @Test - @DisplayName("switchCase(cases(on(...), onDefault(...))) produces SwitchTask") - void switchCase_cases_caseOf_and_default() { + void function_step_exportAs_function_sets_export() { Workflow wf = - FuncWorkflowBuilder.workflow("switchShortcuts") + FuncWorkflowBuilder.workflow("step-function-export") .tasks( - switchCase( - caseOf((Boolean b) -> b).then("thenTask"), caseDefault(FlowDirectiveEnum.END))) - .build(); - - assertNotNull(wf.getDo().get(0).getTask().getSwitchTask()); - } - - @Test - @DisplayName("switchWhen / switchWhenOrElse sugar") - void switchWhen_sugar() { - Workflow wf = - FuncWorkflowBuilder.workflow("switchSugar") - .tasks( - switchWhen((Integer v) -> v > 0, "positive"), - switchWhenOrElse((Integer v) -> v == 0, "zero", FlowDirectiveEnum.END)) - .build(); - - assertNotNull(wf.getDo().get(0).getTask().getSwitchTask()); - assertNotNull(wf.getDo().get(1).getTask().getSwitchTask()); - } - - @Test - @DisplayName("forEach(Collection) with body -> ForTaskFunction") - void forEach_constantCollection() { - List col = List.of("a", "b", "c"); - - Workflow wf = - FuncWorkflowBuilder.workflow("foreachConstant") - .tasks(forEach(col, tasks(function(String::toUpperCase, String.class)))) + // call + chain exportAs + function(String::trim, String.class) + .exportAs((String s) -> Map.of("len", s.length()))) .build(); - Task loopHolder = wf.getDo().get(0).getTask(); - assertNotNull(loopHolder.getForTask()); - ForTaskFunction fn = (ForTaskFunction) loopHolder.getForTask(); - assertEquals(1, fn.getDo().size()); - assertNotNull(fn.getDo().get(0).getTask().getCallTask()); - } - - @Test - @DisplayName("forEach(Function>) matches builder signature and nests body") - void forEach_functionSignature() { - Function> collectionF = ctx -> Arrays.asList(1, 2, 3); - - Workflow wf = - FuncWorkflowBuilder.workflow("foreachFunction") - .tasks(forEach(collectionF, tasks(function(Integer::toHexString, Integer.class)))) - .build(); + List items = wf.getDo(); + assertEquals(1, items.size()); - Task loopHolder = wf.getDo().get(0).getTask(); - assertNotNull(loopHolder.getForTask()); - ForTaskFunction fn = (ForTaskFunction) loopHolder.getForTask(); - assertEquals(1, fn.getDo().size()); - assertNotNull(fn.getDo().get(0).getTask().getCallTask()); + Task t = items.get(0).getTask(); + assertNotNull(t.getCallTask(), "CallTask expected"); + Export ex = ((CallJava) t.getCallTask().get()).getExport(); + assertNotNull(ex, "Export should be set via Step.exportAs(Function)"); + assertNotNull(ex.getAs(), "'as' should be populated"); + // functional export should not produce a literal string + assertNull( + ex.getAs().getString(), "Export 'as' must not be a literal string when using Function"); } @Test - @DisplayName("doTasks(function, emit, switchCase) preserves order") - void doTasks_compositionOrder() { + void function_step_when_compiles_and_builds() { Workflow wf = - FuncWorkflowBuilder.workflow("composition") + FuncWorkflowBuilder.workflow("step-function-when") .tasks( - function((Integer x) -> x + 1, Integer.class), - emit("Ping", (String s) -> BytesCloudEventData.wrap(s.getBytes())), - switchCase(caseOf((Boolean b) -> b).then("ok"), FuncDSL.caseDefault("fallback"))) + function((Integer v) -> v + 1, Integer.class) + .when((Integer v) -> v > 0, Integer.class)) .build(); List items = wf.getDo(); - assertEquals(3, items.size()); - assertNotNull(items.get(0).getTask().getCallTask()); - assertNotNull(items.get(1).getTask().getEmitTask()); - assertNotNull(items.get(2).getTask().getSwitchTask()); + assertEquals(1, items.size()); + assertNotNull(items.get(0).getTask().getCallTask(), "CallTask should still be present"); + // We don't assert internal predicate storage details; just ensure build success & presence. } @Test - @DisplayName("fn shortcut can be used directly inside callFn") - void fn_shortcut_in_callFn() { - Workflow wf = - FuncWorkflowBuilder.workflow("fnShortcut") - .tasks(d -> d.callFn("calc", fn((Double v) -> v * 2, Double.class))) - .build(); + void emit_step_exportAs_javaFilter_sets_export() { + // Build an emit spec using your DSL (type + data function) + FuncEmitSpec spec = + new FuncEmitSpec() + .type("org.acme.signal") + .bytesData((String s) -> s.getBytes(StandardCharsets.UTF_8), String.class); - assertNotNull(wf.getDo().get(0).getTask().getCallTask()); - } + // JavaFilterFunction is (T, WorkflowContextData, TaskContextData) -> R + JavaFilterFunction> jf = + (val, wfCtx, taskCtx) -> Map.of("wrapped", val, "wfId", wfCtx.instanceData().id()); - @Test - @DisplayName("Java style forE with collection + whileC builds ForTaskFunction") - void javaForEach_noSet() { Workflow wf = - FuncWorkflowBuilder.workflow("javaLoopFlow") - .tasks( - d -> - d.forEach( - j -> - j.collection(ctx -> List.of("a", "b", "c")) - .whileC((String val, Object ctx) -> !val.equals("c")) - .tasks( - inner -> - inner.callFn( - c -> { - /* body */ - })))) + FuncWorkflowBuilder.workflow("step-emit-export") + .tasks(emit("emitWrapped", spec).exportAs(jf)) // chaining on Step .build(); List items = wf.getDo(); assertEquals(1, items.size()); + Task t = items.get(0).getTask(); + assertNotNull(t.getEmitTask(), "EmitTask expected"); - TaskItem loopItem = items.get(0); - Task task = loopItem.getTask(); - - assertNotNull(task.getForTask(), "Java ForTaskFunction should be present"); - - ForTaskFunction fn = (ForTaskFunction) task.getForTask(); - assertNotNull(fn.getDo(), "Nested 'do' list inside ForTaskFunction should be populated"); - assertEquals(1, fn.getDo().size()); - Task nested = fn.getDo().get(0).getTask(); - assertNotNull(nested.getCallTask()); + // 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"); + assertNull( + ex.getAs().getString(), "Export 'as' must not be a literal string when using function"); } @Test - @DisplayName("Mixed spec and Java loops in one workflow (no set)") - void mixedLoops_noSet() { - Workflow wf = - FuncWorkflowBuilder.workflow("mixed") - .tasks( - d -> - d.forEach(f -> f.each("item").in("$.array")) // spec - .forEach(j -> j.collection(ctx -> List.of(1, 2, 3))) // java - ) - .build(); - - List items = wf.getDo(); - assertEquals(2, items.size()); - - Task specLoop = items.get(0).getTask(); - Task javaLoop = items.get(1).getTask(); + @DisplayName("listen(spec).exportAs(Function) sets Export on ListenTask holder") + void listen_step_exportAs_function_sets_export() { + FuncListenSpec spec = toOne("org.acme.review.done"); // using your existing DSL helper - assertNotNull(specLoop.getForTask()); - assertNotNull(javaLoop.getForTask()); - } - - @Test - @DisplayName("Java functional exportAsFn/inputFrom/outputAs wrappers (no literal set)") - void javaFunctionalIO_noSet() { Workflow wf = - FuncWorkflowBuilder.workflow("fnIO") - .tasks( - d -> - d.forEach( - j -> - j.collection(ctx -> List.of("x", "y")) - .tasks( - inner -> - inner.callFn( - c -> { - /* calc */ - })) - .exportAs(item -> Map.of("computed", 42)) - .outputAs(item -> Map.of("out", true)))) - .build(); - - assertEquals(1, wf.getDo().size()); - - Task forTaskFnHolder = wf.getDo().get(0).getTask(); - ForTaskFunction fn = (ForTaskFunction) forTaskFnHolder.getForTask(); - assertNotNull(fn); - - List nested = fn.getDo(); - assertEquals(1, nested.size()); - assertNotNull(nested.get(0).getTask().getCallTask()); - - // Structural checks for function-based export/output - assertNotNull(fn.getExport(), "Export should be set via functional variant"); - if (fn.getExport().getAs() != null) { - assertNull(fn.getExport().getAs().getString(), "Export 'as' should not be a literal string"); - } - - if (fn.getOutput() != null && fn.getOutput().getAs() != null) { - assertNull(fn.getOutput().getAs().getString(), "Output 'as' should not be a literal string"); - } - } - - @Test - @DisplayName("callFn task added and retains name + CallTask union") - void callJavaTask_noSet() { - Workflow wf = - FuncWorkflowBuilder.workflow("callJavaFlow") - .tasks( - d -> - d.callFn( - "invokeHandler", - cj -> { - // configure your FuncCallTaskBuilder here - })) + FuncWorkflowBuilder.workflow("step-listen-export") + .tasks(listen("waitHumanReview", spec).exportAs((Object e) -> Map.of("seen", true))) .build(); List items = wf.getDo(); assertEquals(1, items.size()); - TaskItem ti = items.get(0); + Task t = items.get(0).getTask(); + assertNotNull(t.getListenTask(), "ListenTask expected"); - assertEquals("invokeHandler", ti.getName()); - Task task = ti.getTask(); - assertNotNull(task.getCallTask(), "CallTask should be present for callFn"); + Export ex = t.getListenTask().getExport(); + assertNotNull(ex, "Export should be set via Step.exportAs(Function)"); + assertNotNull(ex.getAs(), "'as' should be populated"); + assertNull( + ex.getAs().getString(), "Export 'as' must not be a literal string when using function"); } @Test - @DisplayName("switchCaseFn (Java variant) without spec set branch") - void switchCaseJava_noSet() { + @DisplayName("emit(event(type, fn)).when(...) -> still an EmitTask and builds") + void emit_step_when_compiles_and_builds() { Workflow wf = - FuncWorkflowBuilder.workflow("switchJava") + FuncWorkflowBuilder.workflow("step-emit-when") .tasks( - d -> - d.switchCase( - sw -> { - // configure Java switch builder (cases / predicates) - })) + emit(event("org.acme.sig", (String s) -> BytesCloudEventData.wrap(s.getBytes()))) + .when((Object ctx) -> true)) .build(); List items = wf.getDo(); assertEquals(1, items.size()); - - Task switchTask = items.get(0).getTask(); - assertNotNull(switchTask.getSwitchTask(), "SwitchTask union should be present"); - } - - @Test - @DisplayName("Composite: java forE + nested callFn (no set)") - void compositeScenario_noSet() { - Workflow wf = - FuncWorkflowBuilder.workflow("composite") - .tasks( - d -> - d.forEach( - j -> - j.collection(ctx -> List.of("a", "b")) - .tasks( - inner -> - inner - .callFn( - cj -> { - /* customizing Java call */ - }) - .callFn( - cj -> { - /* second step */ - })))) - .build(); - - assertEquals(1, wf.getDo().size()); - - Task loopHolder = wf.getDo().get(0).getTask(); - ForTaskFunction fn = (ForTaskFunction) loopHolder.getForTask(); - assertNotNull(fn); - - List nested = fn.getDo(); - assertEquals(2, nested.size()); - - Task nestedCall1 = nested.get(0).getTask(); - Task nestedCall2 = nested.get(1).getTask(); - - assertNotNull(nestedCall1.getCallTask()); - assertNotNull(nestedCall2.getCallTask()); - } - - @Test - @DisplayName("listen(toAny(types...)) produces ListenTask") - void listen_toAny_minimal() { - Workflow wf = - FuncWorkflowBuilder.workflow("listenAny") - .tasks(listen(toAny("org.acme.email.approved", "org.acme.email.denied"))) - .build(); - - assertEquals(1, wf.getDo().size()); - Task t = wf.getDo().get(0).getTask(); - assertNotNull(t.getListenTask(), "ListenTask should be present"); + assertNotNull(items.get(0).getTask().getEmitTask(), "EmitTask should still be present"); } @Test - @DisplayName("listen(name, toAll(types...).until(expr)) is named and has ListenTask") - void listen_named_toAll_until() { + @DisplayName("Mixed chaining: function.exportAs -> emit.when -> listen.exportAs") + void mixed_chaining_order_and_exports() { Workflow wf = - FuncWorkflowBuilder.workflow("listenAllNamed") + FuncWorkflowBuilder.workflow("step-mixed") .tasks( - listen( - "waitForAll", - toAll("org.acme.signal.one", "org.acme.signal.two") - .until((CloudEventData e) -> e.toString().isEmpty(), CloudEventData.class))) - .build(); - - assertEquals(1, wf.getDo().size()); - TaskItem ti = wf.getDo().get(0); - assertEquals("waitForAll", ti.getName(), "Listen task should preserve given name"); - assertNotNull(ti.getTask().getListenTask(), "ListenTask should be present"); - } - - @Test - @DisplayName("listen(toOne(type)) produces ListenTask") - void listen_toOne() { - Workflow wf = - FuncWorkflowBuilder.workflow("listenOne") - .tasks(listen(toOne("org.acme.email.review.required"))) - .build(); - - assertEquals(1, wf.getDo().size()); - assertNotNull(wf.getDo().get(0).getTask().getListenTask()); - } - - @Test - @DisplayName("emit -> listen -> emit ordering with FuncDSL listen fluent") - void emit_listen_emit_order() { - Workflow wf = - FuncWorkflowBuilder.workflow("emitListenEmit") - .tasks( - emit( - "org.acme.email.started", - (String s) -> BytesCloudEventData.wrap(s.getBytes(UTF_8))), - listen(toAny("org.acme.email.approved", "org.acme.email.denied")), - emit( - "org.acme.email.finished", - (String s) -> BytesCloudEventData.wrap(s.getBytes(UTF_8)))) + function(String::strip, String.class).exportAs((String s) -> Map.of("s", s)), + emit(event( + "org.acme.kickoff", (String s) -> BytesCloudEventData.wrap(s.getBytes()))) + .when((Object ignore) -> true), + listen(toOne("org.acme.done")).exportAs((Object e) -> Map.of("ok", true))) .build(); List items = wf.getDo(); - assertEquals(3, items.size(), "Three steps should be composed in order"); - - Task first = items.get(0).getTask(); - Task second = items.get(1).getTask(); - Task third = items.get(2).getTask(); - - assertNotNull(first.getEmitTask(), "1st is EmitTask"); - assertNotNull(second.getListenTask(), "2nd is ListenTask"); - assertNotNull(third.getEmitTask(), "3rd is EmitTask"); - } - - @Test - @DisplayName( - "Functional parity of agentic example: callFn -> switchCase -> emit -> listen -> emit") - void functional_parity_example() { - Workflow wf = - FuncWorkflowBuilder.workflow("emailDrafterFunctional") - .tasks( // parseDraft - function(String::trim, String.class), - // policyCheck – pretend it maps string->decision code (0: auto, 1: needs review) - function((String parsed) -> parsed.isEmpty() ? 1 : 0, String.class), - // needsHumanReview? -> requestReview | emailFinished - switchCase( - "needsHumanReview?", - FuncDSL.caseOf((Integer decision) -> decision != 0, Integer.class) - .then("requestReview"), - FuncDSL.caseDefault("emailFinished")), - // emit review request (named branch) - emit( - "requestReview", - "org.acme.email.request", - (String payload) -> BytesCloudEventData.wrap(payload.getBytes(UTF_8))), - // wait for any of approved/denied - listen("waitForReview", toAny("org.acme.email.approved", "org.acme.email.denied")), - // finished event - emit( - "emailFinished", - "org.acme.email.finished", - (String payload) -> BytesCloudEventData.wrap(payload.getBytes(UTF_8)))) - .build(); - - List items = wf.getDo(); - assertEquals(6, items.size()); - - assertNotNull(items.get(0).getTask().getCallTask()); // parseDraft - assertNotNull(items.get(1).getTask().getCallTask()); // policyCheck - assertNotNull(items.get(2).getTask().getSwitchTask()); // needsHumanReview? - assertNotNull(items.get(3).getTask().getEmitTask()); // requestReview - assertNotNull(items.get(4).getTask().getListenTask()); // waitForReview - assertNotNull(items.get(5).getTask().getEmitTask()); // emailFinished + assertEquals(3, items.size()); - assertEquals("needsHumanReview?", items.get(2).getName()); - assertEquals("requestReview", items.get(3).getName()); - assertEquals("waitForReview", items.get(4).getName()); - assertEquals("emailFinished", items.get(5).getName()); - } + Task t0 = items.get(0).getTask(); + Task t1 = items.get(1).getTask(); + Task t2 = items.get(2).getTask(); - @Test - @DisplayName("listen(to().any(...).until(...)) builds ListenTask with chained spec") - void listen_with_to_chaining() { - Workflow wf = - FuncWorkflowBuilder.workflow("listenChained") - .tasks( - listen( - to().any(event("org.acme.sig.one"), event("org.acme.sig.two")) - .until((CloudEventData e) -> e.toString().isEmpty(), CloudEventData.class))) - .build(); + assertNotNull(t0.getCallTask()); + assertNotNull(t1.getEmitTask()); + assertNotNull(t2.getListenTask()); - assertEquals(1, wf.getDo().size()); - assertNotNull(wf.getDo().get(0).getTask().getListenTask()); assertNotNull( - wf.getDo() - .get(0) - .getTask() - .getListenTask() - .getListen() - .getTo() - .getAnyEventConsumptionStrategy() - .getUntil()); + ((CallJava) t0.getCallTask().get()).getExport(), "function step should carry export"); + assertNotNull(t2.getListenTask().getExport(), "listen step should carry export"); } } diff --git a/experimental/fluent/pom.xml b/experimental/fluent/pom.xml index 3156456e5..57154e0ba 100644 --- a/experimental/fluent/pom.xml +++ b/experimental/fluent/pom.xml @@ -28,6 +28,11 @@ serverlessworkflow-fluent-spec ${project.version} + + io.serverlessworkflow + serverlessworkflow-impl-json + ${project.version} + io.serverlessworkflow serverlessworkflow-experimental-agentic diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java index ede8f2027..6ee2cbf8f 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java @@ -21,13 +21,13 @@ public class SetTaskBuilder extends TaskBaseBuilder { - private final SetTask setTask; + private final SetTask task; private final SetTaskConfiguration setTaskConfiguration; public SetTaskBuilder() { - this.setTask = new SetTask(); + this.task = new SetTask(); this.setTaskConfiguration = new SetTaskConfiguration(); - this.setTask(setTask); + this.setTask(task); } @Override @@ -36,7 +36,7 @@ protected SetTaskBuilder self() { } public SetTaskBuilder expr(String expression) { - this.setTask.setSet(new Set().withString(expression)); + this.task.setSet(new Set().withString(expression)); return this; } @@ -46,10 +46,10 @@ public SetTaskBuilder put(String key, Object value) { } public SetTask build() { - if (this.setTask.getSet() == null) { - this.setTask.setSet(new Set().withSetTaskConfiguration(setTaskConfiguration)); + if (this.task.getSet() == null) { + this.task.setSet(new Set().withSetTaskConfiguration(setTaskConfiguration)); } - return setTask; + return task; } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java index b4b4d211e..873883694 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java @@ -58,12 +58,22 @@ public SELF now() { return self(); } + public SELF contentType(String ct) { + steps.add(e -> e.dataContentType(ct)); + return self(); + } + /** Sets the CloudEvent dataContentType to `application/json` */ public SELF JSON() { steps.add(e -> e.dataContentType("application/json")); return self(); } + public SELF OCTET_STREAM() { + steps.add(e -> e.dataContentType("application/octet-stream")); + return self(); + } + public SELF source(String source) { steps.add(e -> e.source(source)); return self(); From 13eda2ae0db36f44a663413b5257532857eaeaf4 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 21 Oct 2025 13:39:55 -0400 Subject: [PATCH 3/5] Add consumer and JavaContextFunction shortcuts Signed-off-by: Ricardo Zanini --- .../fluent/func/FuncCallTaskBuilder.java | 27 + .../fluent/func/dsl/ConsumeStep.java | 58 ++ .../fluent/func/dsl/CtxBiFunction.java | 29 + .../fluent/func/dsl/FuncCallStep.java | 45 +- .../fluent/func/dsl/FuncDSL.java | 564 +++++++++++++++++- .../fluent/func/dsl/InstanceIdBiFunction.java | 27 + .../fluent/func/dsl/Step.java | 98 ++- .../fluent/func/spi/FuncTransformations.java | 14 + .../fluent/func/FuncDSLConsumeTest.java | 52 ++ .../fluent/func/FuncDSLTest.java | 47 ++ .../func/JavaConsumerCallExecutor.java | 13 +- .../func/JavaContextFunctionCallExecutor.java | 57 ++ ...erlessworkflow.impl.executors.CallableTask | 3 +- .../impl/CallJavaContextFunctionTest.java | 73 +++ .../api/types/func/CallJava.java | 41 +- 15 files changed, 1093 insertions(+), 55 deletions(-) create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ConsumeStep.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/CtxBiFunction.java create mode 100644 experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/InstanceIdBiFunction.java create mode 100644 experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLConsumeTest.java create mode 100644 experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java create mode 100644 experimental/lambda/src/test/java/io/serverless/workflow/impl/CallJavaContextFunctionTest.java diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java index 56301a1ed..180804d08 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java @@ -17,9 +17,11 @@ import io.serverlessworkflow.api.types.func.CallJava; import io.serverlessworkflow.api.types.func.CallTaskJava; +import io.serverlessworkflow.api.types.func.JavaContextFunction; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; +import java.util.function.Consumer; import java.util.function.Function; public class FuncCallTaskBuilder extends TaskBaseBuilder @@ -48,6 +50,31 @@ public FuncCallTaskBuilder function(Function function, Class arg return this; } + public FuncCallTaskBuilder function(JavaContextFunction function) { + return function(function, null); + } + + public FuncCallTaskBuilder function( + JavaContextFunction function, Class argClass) { + this.callTaskJava = new CallTaskJava(CallJava.function(function, argClass)); + super.setTask(this.callTaskJava.getCallJava()); + return this; + } + + /** Accept a side-effect Consumer; engine should pass input through unchanged. */ + public FuncCallTaskBuilder consumer(Consumer consumer) { + this.callTaskJava = new CallTaskJava(CallJava.consumer(consumer)); + super.setTask(this.callTaskJava.getCallJava()); + return this; + } + + /** Accept a Consumer with explicit input type hint. */ + public FuncCallTaskBuilder consumer(Consumer consumer, Class argClass) { + this.callTaskJava = new CallTaskJava(CallJava.consumer(consumer, argClass)); + super.setTask(this.callTaskJava.getCallJava()); + return this; + } + public CallTaskJava build() { return this.callTaskJava; } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ConsumeStep.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ConsumeStep.java new file mode 100644 index 000000000..34339f0e2 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/ConsumeStep.java @@ -0,0 +1,58 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.function.Consumer; + +public final class ConsumeStep extends Step, FuncCallTaskBuilder> { + private final String name; // may be null + private final Consumer consumer; + private final Class argClass; // may be null + + ConsumeStep(Consumer consumer, Class argClass) { + this(null, consumer, argClass); + } + + ConsumeStep(String name, Consumer consumer, Class argClass) { + this.name = name; + this.consumer = consumer; + this.argClass = argClass; + } + + @Override + protected void configure( + FuncTaskItemListBuilder list, java.util.function.Consumer post) { + if (name == null) { + list.callFn( + cb -> { + // prefer the typed consumer if your builder supports it; otherwise fallback: + if (argClass != null) cb.consumer(consumer, argClass); + else cb.consumer(consumer); + post.accept(cb); + }); + } else { + list.callFn( + name, + cb -> { + if (argClass != null) cb.consumer(consumer, argClass); + else cb.consumer(consumer); + post.accept(cb); + }); + } + } +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/CtxBiFunction.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/CtxBiFunction.java new file mode 100644 index 000000000..5668ab6ad --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/CtxBiFunction.java @@ -0,0 +1,29 @@ +/* + * 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.func.dsl; + +import io.serverlessworkflow.impl.WorkflowContextData; + +/** + * Functions that expect a {@link WorkflowContextData} injection in runtime + * + * @param The task payload input + * @param The task result output + */ +@FunctionalInterface +public interface CtxBiFunction { + R apply(WorkflowContextData context, T payload); +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java index d4cff4352..c784cebf3 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncCallStep.java @@ -15,42 +15,61 @@ */ package io.serverlessworkflow.fluent.func.dsl; +import io.serverlessworkflow.api.types.func.JavaContextFunction; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; import java.util.function.Consumer; import java.util.function.Function; -// FuncCallStep public final class FuncCallStep extends Step, FuncCallTaskBuilder> { - private final String name; // may be null + + private final String name; private final Function fn; + private final JavaContextFunction ctxFn; private final Class argClass; + /** Function variant (unnamed). */ FuncCallStep(Function fn, Class argClass) { this(null, fn, argClass); } + /** Function variant (named). */ FuncCallStep(String name, Function fn, Class argClass) { this.name = name; this.fn = fn; + this.ctxFn = null; + this.argClass = argClass; + } + + /** JavaContextFunction variant (unnamed). */ + FuncCallStep(JavaContextFunction ctxFn, Class argClass) { + this(null, ctxFn, argClass); + } + + /** JavaContextFunction variant (named). */ + FuncCallStep(String name, JavaContextFunction ctxFn, Class argClass) { + this.name = name; + this.fn = null; + this.ctxFn = ctxFn; this.argClass = argClass; } @Override protected void configure(FuncTaskItemListBuilder list, Consumer post) { - if (name == null) { - list.callFn( - cb -> { + final Consumer apply = + cb -> { + if (ctxFn != null) { + cb.function(ctxFn, argClass); + } else { cb.function(fn, argClass); - post.accept(cb); - }); + } + post.accept(cb); + }; + + if (name == null) { + list.callFn(apply); } else { - list.callFn( - name, - cb -> { - cb.function(fn, argClass); - post.accept(cb); - }); + list.callFn(name, apply); } } } 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 9a8bb0464..cc0543316 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 @@ -17,6 +17,7 @@ import io.cloudevents.CloudEventData; import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.api.types.func.JavaContextFunction; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; @@ -33,46 +34,145 @@ import java.util.function.Function; import java.util.function.Predicate; +/** + * Fluent, ergonomic shortcuts for building function-centric workflows. + * + *

This DSL wraps the lower-level builders with strongly-typed helpers that: + * + *

    + *
  • Infer input types for Java lambdas where possible. + *
  • Expose chainable steps (e.g., {@code emit(...).exportAs(...).when(...)}) via {@code Step}s. + *
  • Provide opinionated helpers for CloudEvents (JSON/bytes) and listen strategies. + *
  • Offer context-aware function variants ({@code withContext}, {@code withInstanceId}, {@code + * agent}). + *
+ * + *

Typical usage: + * + *

{@code
+ * Workflow wf = FuncWorkflowBuilder.workflow()
+ *   .tasks(
+ *     FuncDSL.function(String::trim, String.class),
+ *     FuncDSL.emitJson("org.acme.started", MyPayload.class),
+ *     FuncDSL.listen(FuncDSL.toAny("type.one", "type.two"))
+ *       .outputAs(map -> map.get("value")),
+ *     FuncDSL.switchWhenOrElse((Integer v) -> v > 0, "positive", FlowDirectiveEnum.END)
+ *   ).build();
+ * }
+ */ public final class FuncDSL { private static final CommonFuncOps OPS = new CommonFuncOps() {}; + private FuncDSL() {} + + /** + * Create a builder configurer for a {@link FuncCallTaskBuilder} that invokes a plain Java {@link + * Function} with an explicit input type. + * + * @param function the function to call during task execution + * @param argClass the expected input class for type-safe model conversion + * @param input type + * @param output type + * @return a consumer that configures a {@code FuncCallTaskBuilder} + */ public static Consumer fn( Function function, Class argClass) { return OPS.fn(function, argClass); } + /** + * Create a builder configurer for a {@link FuncCallTaskBuilder} that invokes a plain Java {@link + * Function}. The input type is inferred when possible. + * + * @param function the function to call during task execution + * @param input type + * @param output type + * @return a consumer that configures a {@code FuncCallTaskBuilder} + */ public static Consumer fn(Function function) { return f -> f.function(function); } + /** + * Compose multiple switch cases into a single configurer for {@link FuncSwitchTaskBuilder}. + * + * @param cases one or more {@link SwitchCaseConfigurer} built via {@link #caseOf(Predicate)} or + * {@link #caseDefault(String)} + * @return a consumer to apply on a switch task builder + */ public static Consumer cases(SwitchCaseConfigurer... cases) { return OPS.cases(cases); } + /** + * Start a typed switch case using a Java {@link Predicate} with explicit input type. + * + * @param when the predicate used to match this case + * @param whenClass the predicate input class (used for typed conversion) + * @param predicate input type + * @return a fluent builder to set the consequent action (e.g., {@code then("taskName")}) + */ public static SwitchCaseSpec caseOf(Predicate when, Class whenClass) { return OPS.caseOf(when, whenClass); } + /** + * Start a switch case using a Java {@link Predicate}. Type inference is used when possible. + * + * @param when the predicate used to match this case + * @param predicate input type + * @return a fluent builder to set the consequent action (e.g., {@code then("taskName")}) + */ public static SwitchCaseSpec caseOf(Predicate when) { return OPS.caseOf(when); } + /** + * Default branch for a switch that jumps to a task name. + * + * @param task task name to continue with when no predicate matches + * @return switch case configurer + */ public static SwitchCaseConfigurer caseDefault(String task) { return OPS.caseDefault(task); } + /** + * Default branch for a switch that uses a {@link FlowDirectiveEnum} (e.g. {@code END}, {@code + * CONTINUE}). + * + * @param directive fallback directive when no predicate matches + * @return switch case configurer + */ public static SwitchCaseConfigurer caseDefault(FlowDirectiveEnum directive) { return OPS.caseDefault(directive); } + /** + * Begin building a {@code listen} specification with no pre-selected strategy. + * + * @return a new {@link FuncListenSpec} + */ public static FuncListenSpec to() { return new FuncListenSpec(); } + /** + * Convenience to listen for exactly one event type. + * + * @param type CloudEvent type to listen for + * @return a {@link FuncListenSpec} set to {@code one(type)} + */ public static FuncListenSpec toOne(String type) { return new FuncListenSpec().one(e -> e.type(type)); } + /** + * Convenience to listen for all the given event types (logical AND). + * + * @param types CloudEvent types + * @return a {@link FuncListenSpec} set to {@code all(types...)} + */ public static FuncListenSpec toAll(String... types) { FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; for (int i = 0; i < types.length; i++) { @@ -81,6 +181,12 @@ public static FuncListenSpec toAll(String... types) { return new FuncListenSpec().all(events); } + /** + * Convenience to listen for any of the given event types (logical OR). + * + * @param types CloudEvent types + * @return a {@link FuncListenSpec} set to {@code any(types...)} + */ public static FuncListenSpec toAny(String... types) { FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; for (int i = 0; i < types.length; i++) { @@ -89,161 +195,605 @@ public static FuncListenSpec toAny(String... types) { return new FuncListenSpec().any(events); } + /** + * Build an {@code emit} event configurer by supplying a type and a function that produces {@link + * CloudEventData}. The function is invoked at runtime with the current payload. + * + * @param type CloudEvent type + * @param function function that maps workflow input to {@link CloudEventData} + * @param input type to the function + * @return a consumer to configure {@link FuncEmitTaskBuilder} + */ public static Consumer event( String type, Function function) { return OPS.event(type, function); } + /** + * Same as {@link #event(String, Function)} but with an explicit input class to guide conversion. + * + * @param type CloudEvent type + * @param function function that maps workflow input to {@link CloudEventData} + * @param clazz expected input class for conversion + * @param input type + * @return a consumer to configure {@link FuncEmitTaskBuilder} + */ public static Consumer event( String type, Function function, Class clazz) { return OPS.event(type, function, clazz); } - /** Emit a JSON CloudEvent (PojoCloudEventData) from a POJO payload. */ + /** + * Emit a JSON CloudEvent for a POJO input type. Sets {@code contentType=application/json} and + * serializes the input to bytes using the configured JSON mapper. + * + * @param type CloudEvent type + * @param clazz input POJO class (used for typing and conversion) + * @param input type + * @return a consumer to configure {@link FuncEmitTaskBuilder} + */ public static Consumer eventJson(String type, Class clazz) { return b -> new FuncEmitSpec().type(type).jsonData(clazz).accept(b); } + /** + * Emit a CloudEvent with arbitrary bytes payload generated by a custom serializer. + * + * @param type CloudEvent type + * @param serializer function producing bytes from the input + * @param clazz expected input class for conversion + * @param input type + * @return a consumer to configure {@link FuncEmitTaskBuilder} + */ public static Consumer eventBytes( String type, Function serializer, Class clazz) { return b -> new FuncEmitSpec().type(type).bytesData(serializer, clazz).accept(b); } + /** + * Emit a CloudEvent where the input model is serialized to bytes using UTF-8. Useful for + * string-like payloads (e.g., already-built JSON or text). + * + * @param type CloudEvent type + * @return a consumer to configure {@link FuncEmitTaskBuilder} + */ public static Consumer eventBytesUtf8(String type) { return b -> new FuncEmitSpec().type(type).bytesDataUtf8().accept(b); } + /** + * Create a predicate event configurer for {@code listen} specs. + * + * @param type CloudEvent type + * @return predicate event configurer for use in {@link FuncListenSpec} + */ public static FuncPredicateEventConfigurer event(String type) { return OPS.event(type); } + /** + * Create a {@link FuncCallStep} that calls a simple Java {@link Function} with explicit input + * type. + * + * @param fn the function to execute at runtime + * @param clazz expected input class for model conversion + * @param input type + * @param result type + * @return a call step which supports chaining (e.g., {@code .exportAs(...).when(...)}) + */ public static FuncCallStep function(Function fn, Class clazz) { return new FuncCallStep<>(fn, clazz); } + /** + * Build a call step for functions that need {@code WorkflowContextData} as the first parameter. + * The DSL wraps it as a {@link JavaContextFunction} and injects the runtime context. + * + *

Signature expected: {@code (ctx, payload) -> result} + * + * @param fn context-aware bi-function + * @param in payload input class + * @param input type + * @param result type + * @return a call step + */ + public static FuncCallStep withContext(CtxBiFunction fn, Class in) { + return withContext(null, fn, in); + } + + /** + * Build a call step for functions that expect the workflow instance ID as the first parameter. + * The instance ID is extracted from the runtime context. + * + *

Signature expected: {@code (instanceId, payload) -> result} + * + * @param fn instance-id-aware bi-function + * @param in payload input class + * @param input type + * @param result type + * @return a call step + */ + public static FuncCallStep withInstanceId( + InstanceIdBiFunction fn, Class in) { + return withInstanceId(null, fn, in); + } + + /** + * Named variant of {@link #withContext(CtxBiFunction, Class)}. + * + * @param name task name + * @param fn context-aware bi-function + * @param in payload input class + * @param input type + * @param result type + * @return a named call step + */ + public static FuncCallStep withContext( + String name, CtxBiFunction fn, Class in) { + JavaContextFunction jcf = (payload, wctx) -> fn.apply(wctx, payload); + return new FuncCallStep<>(name, jcf, in); + } + + /** + * Named variant of {@link #withInstanceId(InstanceIdBiFunction, Class)}. + * + * @param name task name + * @param fn instance-id-aware bi-function + * @param in payload input class + * @param input type + * @param result type + * @return a named call step + */ + public static FuncCallStep withInstanceId( + String name, InstanceIdBiFunction fn, Class in) { + JavaContextFunction jcf = (payload, wctx) -> fn.apply(wctx.instanceData().id(), payload); + return new FuncCallStep<>(name, jcf, in); + } + + /** + * Create a fire-and-forget side-effect step (unnamed). The consumer receives the typed input. + * + * @param consumer side-effect function + * @param clazz expected input class for conversion + * @param input type + * @return a consume step + */ + public static ConsumeStep consume(Consumer consumer, Class clazz) { + return new ConsumeStep<>(consumer, clazz); + } + + /** + * Named variant of {@link #consume(Consumer, Class)}. + * + * @param name task name + * @param consumer side-effect function + * @param clazz expected input class + * @param input type + * @return a consume step + */ + public static ConsumeStep consume(String name, Consumer consumer, Class clazz) { + return new ConsumeStep<>(name, consumer, clazz); + } + + /** + * Agent-style sugar for methods that receive a "memory id" as first parameter. We reuse the + * workflow instance id for that purpose. + * + *

Equivalent to {@link #withInstanceId(InstanceIdBiFunction, Class)}. + * + * @param fn (instanceId, payload) -> result + * @param in payload input class + * @param input type + * @param result type + * @return a call step + */ + public static FuncCallStep agent(InstanceIdBiFunction fn, Class in) { + return withInstanceId(fn, in); + } + + /** + * Named agent-style sugar. See {@link #agent(InstanceIdBiFunction, Class)}. + * + * @param name task name + * @param fn (instanceId, payload) -> result + * @param in payload input class + * @param input type + * @param result type + * @return a named call step + */ + public static FuncCallStep agent( + String name, InstanceIdBiFunction fn, Class in) { + return withInstanceId(name, fn, in); + } + + /** + * Create a {@link FuncCallStep} that invokes a plain Java {@link Function} with inferred input + * type. + * + * @param fn the function to execute + * @param input type + * @param output type + * @return a call step + */ public static FuncCallStep function(Function fn) { Class clazz = ReflectionUtils.inferInputType(fn); return new FuncCallStep<>(fn, clazz); } + /** + * Named variant of {@link #function(Function)} with inferred input type. + * + * @param name task name + * @param fn the function to execute + * @param input type + * @param output type + * @return a named call step + */ public static FuncCallStep function(String name, Function fn) { Class clazz = ReflectionUtils.inferInputType(fn); return new FuncCallStep<>(name, fn, clazz); } + /** + * Named variant of {@link #function(Function, Class)} with explicit input type. + * + * @param name task name + * @param fn the function to execute + * @param clazz expected input class + * @param input type + * @param output type + * @return a named call step + */ public static FuncCallStep function(String name, Function fn, Class clazz) { return new FuncCallStep<>(name, fn, clazz); } // ------------------ tasks ---------------- // + /** + * Compose a list of step configurers into a single {@code tasks} block. Preserves order and + * defers application to the underlying list builder. + * + * @param steps one or more step configurers (including {@link Step} subclasses) + * @return a consumer for {@link FuncTaskItemListBuilder} + */ public static Consumer tasks(FuncTaskConfigurer... steps) { Objects.requireNonNull(steps, "Steps in a tasks are required"); final List snapshot = List.of(steps.clone()); return list -> snapshot.forEach(s -> s.accept(list)); } + /** + * Create an {@code emit} step from a low-level {@link FuncEmitTaskBuilder} configurer. Prefer + * higher-level helpers like {@link #emitJson(String, Class)} where possible. + * + * @param cfg emit builder configurer + * @return an {@link EmitStep} supporting chaining (e.g., {@code .exportAs(...).when(...)}) + */ public static EmitStep emit(Consumer cfg) { return new EmitStep(null, cfg); } + /** + * Named variant of {@link #emit(Consumer)}. + * + * @param name task name + * @param cfg emit builder configurer + * @return a named emit step + */ public static EmitStep emit(String name, Consumer cfg) { return new EmitStep(name, cfg); } + /** + * Convenience for emitting a CloudEvent using a function that builds {@link CloudEventData}. + * + * @param type CloudEvent type + * @param fn function that produces event data from the input + * @param input type + * @return an {@link EmitStep} + */ public static EmitStep emit(String type, Function fn) { - // `event(type, fn)` is your Consumer for EMIT return new EmitStep(null, event(type, fn)); } + /** + * Named variant of {@link #emit(String, Function)}. + * + * @param name task name + * @param type CloudEvent type + * @param fn function producing {@link CloudEventData} + * @param input type + * @return a named {@link EmitStep} + */ public static EmitStep emit(String name, String type, Function fn) { return new EmitStep(name, event(type, fn)); } + /** + * Emit a bytes-based CloudEvent using a custom serializer and explicit input class. + * + * @param name task name + * @param type CloudEvent type + * @param serializer function producing bytes + * @param clazz expected input class + * @param input type + * @return a named {@link EmitStep} + */ public static EmitStep emit( String name, String type, Function serializer, Class clazz) { return new EmitStep(name, eventBytes(type, serializer, clazz)); } + /** Unnamed variant of {@link #emit(String, String, Function, Class)}. */ public static EmitStep emit(String type, Function serializer, Class clazz) { return new EmitStep(null, eventBytes(type, serializer, clazz)); } + /** + * Emit a JSON CloudEvent from a POJO input class (unnamed). + * + * @param type CloudEvent type + * @param clazz input POJO class + * @param input type + * @return an {@link EmitStep} + */ public static EmitStep emitJson(String type, Class clazz) { return new EmitStep(null, eventJson(type, clazz)); } + /** + * Emit a JSON CloudEvent from a POJO input class (named). + * + * @param name task name + * @param type CloudEvent type + * @param clazz input POJO class + * @param input type + * @return a named {@link EmitStep} + */ public static EmitStep emitJson(String name, String type, Class clazz) { return new EmitStep(name, eventJson(type, clazz)); } + /** + * Create a {@code listen} step from a {@link FuncListenSpec}. + * + * @param spec a listen spec (e.g., {@code toAny("a","b")}, {@code toOne("x")}) + * @return a {@link ListenStep} supporting chaining (e.g., {@code .outputAs(...).when(...)}) + */ public static ListenStep listen(FuncListenSpec spec) { return new ListenStep(null, spec); } + /** + * Named variant of {@link #listen(FuncListenSpec)}. + * + * @param name task name + * @param spec listen spec + * @return a named {@link ListenStep} + */ public static ListenStep listen(String name, FuncListenSpec spec) { return new ListenStep(name, spec); } + /** + * Low-level switch case configurer using a custom builder consumer. Prefer the {@link + * #caseOf(Predicate)} helpers when possible. + * + * @param taskName optional task name + * @param switchCase consumer to configure the {@link FuncSwitchTaskBuilder} + * @return a list configurer + */ public static FuncTaskConfigurer switchCase( String taskName, Consumer switchCase) { return list -> list.switchCase(taskName, switchCase); } + /** Variant of {@link #switchCase(String, Consumer)} without a name. */ public static FuncTaskConfigurer switchCase(Consumer switchCase) { return list -> list.switchCase(switchCase); } + /** + * Convenience to apply multiple {@link SwitchCaseConfigurer} built via {@link + * #caseOf(Predicate)}. + * + * @param cases case configurers + * @return list configurer + */ public static FuncTaskConfigurer switchCase(SwitchCaseConfigurer... cases) { return switchCase(null, cases); } + /** + * Named variant of {@link #switchCase(SwitchCaseConfigurer...)}. + * + * @param taskName task name + * @param cases case configurers + * @return list configurer + */ public static FuncTaskConfigurer switchCase(String taskName, SwitchCaseConfigurer... cases) { Objects.requireNonNull(cases, "cases are required"); final List snapshot = List.of(cases.clone()); return list -> list.switchCase(taskName, s -> snapshot.forEach(s::onPredicate)); } - // Single predicate -> then task + /** + * Sugar for a single-case switch: if predicate matches, jump to {@code thenTask}. + * + * @param pred predicate + * @param thenTask task name when predicate is true + * @param predicate input type + * @return list configurer + */ public static FuncTaskConfigurer switchWhen(Predicate pred, String thenTask) { return list -> list.switchCase(cases(caseOf(pred).then(thenTask))); } - // With default directive + public static FuncTaskConfigurer switchWhen(String jqExpression, String thenTask) { + return list -> list.switchCase(sw -> sw.on(c -> c.when(jqExpression).then(thenTask))); + } + + /** + * Sugar for a single-case switch with a default {@link FlowDirectiveEnum} fallback. + * + * @param pred predicate + * @param thenTask task name when predicate is true + * @param otherwise default flow directive when predicate is false + * @param predicate input type + * @return list configurer + */ public static FuncTaskConfigurer switchWhenOrElse( Predicate pred, String thenTask, FlowDirectiveEnum otherwise) { return list -> list.switchCase(FuncDSL.cases(caseOf(pred).then(thenTask), caseDefault(otherwise))); } + /** + * Sugar for a single-case switch with a default task fallback. + * + * @param pred predicate + * @param thenTask task name when predicate is true + * @param otherwiseTask task name when predicate is false + * @param predicate input type + * @return list configurer + */ public static FuncTaskConfigurer switchWhenOrElse( Predicate pred, String thenTask, String otherwiseTask) { + return list -> list.switchCase(cases(caseOf(pred).then(thenTask), caseDefault(otherwiseTask))); + } + + /** + * JQ-based condition: if the JQ expression evaluates truthy → jump to {@code thenTask}, otherwise + * follow the {@link FlowDirectiveEnum} given in {@code otherwise}. + * + *

+   *   switchWhenOrElse(".approved == true", "sendEmail", FlowDirectiveEnum.END)
+   * 
+ * + * The JQ expression is evaluated against the task input at runtime. + */ + public static FuncTaskConfigurer switchWhenOrElse( + String jqExpression, String thenTask, FlowDirectiveEnum otherwise) { + + Objects.requireNonNull(jqExpression, "jqExpression"); + Objects.requireNonNull(thenTask, "thenTask"); + Objects.requireNonNull(otherwise, "otherwise"); + return list -> - list.switchCase(FuncDSL.cases(caseOf(pred).then(thenTask), caseDefault(otherwiseTask))); - } + list.switchCase(sw -> sw.on(c -> c.when(jqExpression).then(thenTask)).onDefault(otherwise)); + } + + /** + * JQ-based condition: if the JQ expression evaluates truthy → jump to {@code thenTask}, else jump + * to {@code otherwiseTask}. + * + *
+   *   switchWhenOrElse(".score >= 80", "pass", "fail")
+   * 
+ * + * The JQ expression is evaluated against the task input at runtime. + */ + public static FuncTaskConfigurer switchWhenOrElse( + String jqExpression, String thenTask, String otherwiseTask) { + + Objects.requireNonNull(jqExpression, "jqExpression"); + Objects.requireNonNull(thenTask, "thenTask"); + Objects.requireNonNull(otherwiseTask, "otherwiseTask"); + return list -> + list.switchCase( + sw -> sw.on(c -> c.when(jqExpression).then(thenTask)).onDefault(otherwiseTask)); + } + + /** + * Java functional {@code forEach}: collection is computed from the current input at runtime. + * + * @param collection function that returns the collection to iterate + * @param body inner task list body + * @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)); } + /** + * Java functional {@code forEach}: iterate over a constant collection. + * + * @param collection static collection to iterate + * @param body inner task list body + * @param ignored (kept for signature consistency) + * @return list configurer + */ public static FuncTaskConfigurer forEach( Collection collection, Consumer body) { Function> f = ctx -> (Collection) collection; return list -> list.forEach(j -> j.collection(f).tasks(body)); } - // Overload with simple constant collection + /** + * Java functional {@code forEach} helper for an immutable {@link List}. + * + * @param collection list to iterate + * @param body inner task list body + * @param element type + * @return list configurer + */ public static FuncTaskConfigurer forEach( List collection, Consumer body) { return list -> list.forEach(j -> j.collection(ctx -> collection).tasks(body)); } + /** + * Set a raw JQ-like expression on the current list (advanced). + * + * @param expr expression string + * @return list configurer + */ public static FuncTaskConfigurer set(String expr) { return list -> list.set(expr); } + /** + * Set a map-based expression on the current list (advanced). + * + * @param map map of values to set + * @return list configurer + */ public static FuncTaskConfigurer set(Map map) { return list -> list.set(s -> s.expr(map)); } + + // ----- JQ helpers for outputAs / inputFrom ----- // + + /** + * JQ: pick the first element if the value is an array, otherwise return the value as-is. Does not + * stringify. Useful when downstream expects non-string JSON. + * + *
(first(.[]? // empty) // .)
+ */ + public static String selectFirst() { + return "(first(.[]? // empty) // .)"; + } + + /** + * JQ: stringify the value only if it is not already a string. + * + *
(if type==\"string\" then . else tostring end)
+ */ + public static String stringifyIfNeeded() { + return "(if type==\"string\" then . else tostring end)"; + } + + /** + * JQ: common “normalize to string”: - if array -> take first element - if null/empty -> coerce to + * empty string - if not a string -> tostring + * + *
(first(.[]? // empty) // . // \"\") | (if type==\"string\" then . else tostring end)
+ */ + public static String selectFirstStringify() { + return "(first(.[]? // empty) // . // \"\") | (if type==\"string\" then . else tostring end)"; + } } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/InstanceIdBiFunction.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/InstanceIdBiFunction.java new file mode 100644 index 000000000..e74876c98 --- /dev/null +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/InstanceIdBiFunction.java @@ -0,0 +1,27 @@ +/* + * 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.func.dsl; + +/** + * Functions that expect a workflow instance ID injection in runtime + * + * @param The task payload input + * @param The task result output + */ +@FunctionalInterface +public interface InstanceIdBiFunction { + R apply(String instanceId, T payload); +} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java index 07d14104b..9ce49b97c 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/Step.java @@ -21,13 +21,17 @@ import io.serverlessworkflow.fluent.func.configurers.FuncTaskConfigurer; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; +import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; -/** A deferred configurer that can chain when/inputFrom/outputAs/exportAs and apply them later. */ +/** + * A deferred configurer that can chain when/inputFrom/outputAs/exportAs and apply them later to the + * concrete task builder (e.g., Call/Emit/Listen builder). + */ abstract class Step, B> implements FuncTaskConfigurer { private final List> postConfigurers = new ArrayList<>(); @@ -37,135 +41,177 @@ protected final SELF self() { return (SELF) this; } - // ---------- ConditionalTaskBuilder passthroughs ---------- + // --------------------------------------------------------------------------- + // ConditionalTaskBuilder passthroughs (if/when) + // --------------------------------------------------------------------------- - /** Queue a ConditionalTaskBuilder#when(Predicate) to be applied on the concrete builder. */ + /** Queue a {@code when(predicate)} to be applied on the concrete builder. */ public SELF when(Predicate predicate) { postConfigurers.add(b -> ((ConditionalTaskBuilder) b).when(predicate)); return self(); } - /** Queue a ConditionalTaskBuilder#when(Predicate, Class) to be applied later. */ + /** Queue a {@code when(predicate, argClass)} to be applied on the concrete builder. */ public SELF when(Predicate predicate, Class argClass) { postConfigurers.add(b -> ((ConditionalTaskBuilder) b).when(predicate, argClass)); return self(); } - // ---------- FuncTaskTransformations passthroughs: exportAs ---------- + public SELF when(String jqExpr) { + postConfigurers.add(b -> ((TaskBaseBuilder) b).when(jqExpr)); + return self(); + } + + // --------------------------------------------------------------------------- + // FuncTaskTransformations passthroughs: EXPORT (fn/context/filter + JQ) + // --------------------------------------------------------------------------- - /** Queue a FuncTaskTransformations#exportAs(Function) to be applied later. */ + /** Queue {@code exportAs(fn)} to be applied later. */ public SELF exportAs(Function function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function)); return self(); } - /** Queue a FuncTaskTransformations#exportAs(Function, Class) to be applied later. */ + /** Queue {@code exportAs(fn, argClass)} to be applied later. */ public SELF exportAs(Function function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function, argClass)); return self(); } - /** Queue a FuncTaskTransformations#exportAs(JavaFilterFunction) to be applied later. */ + /** Queue {@code exportAs(filterFn)} to be applied later. */ public SELF exportAs(JavaFilterFunction function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function)); return self(); } - /** Queue a FuncTaskTransformations#exportAs(JavaFilterFunction, Class) to be applied later. */ + /** Queue {@code exportAs(filterFn, argClass)} to be applied later. */ public SELF exportAs(JavaFilterFunction function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function, argClass)); return self(); } - /** Queue a FuncTaskTransformations#exportAs(JavaContextFunction) to be applied later. */ + /** Queue {@code exportAs(ctxFn)} to be applied later. */ public SELF exportAs(JavaContextFunction function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function)); return self(); } - /** Queue a FuncTaskTransformations#exportAs(JavaContextFunction, Class) to be applied later. */ + /** Queue {@code exportAs(ctxFn, argClass)} to be applied later. */ public SELF exportAs(JavaContextFunction function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).exportAs(function, argClass)); return self(); } - // ---------- FuncTaskTransformations passthroughs: outputAs ---------- + /** + * Queue {@code exportAs(jqExpression)} (JQ string) to be applied later. Example: {@code + * exportAs(FuncDSL.selectFirstStringify())} + */ + public SELF exportAs(String jqExpression) { + postConfigurers.add(b -> ((TaskBaseBuilder) b).exportAs(jqExpression)); + return self(); + } + + // --------------------------------------------------------------------------- + // FuncTaskTransformations passthroughs: OUTPUT (fn/context/filter + JQ) + // --------------------------------------------------------------------------- - /** Queue a FuncTaskTransformations#outputAs(Function) to be applied later. */ + /** Queue {@code outputAs(fn)} to be applied later. */ public SELF outputAs(Function function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function)); return self(); } - /** Queue a FuncTaskTransformations#outputAs(Function, Class) to be applied later. */ + /** Queue {@code outputAs(fn, argClass)} to be applied later. */ public SELF outputAs(Function function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function, argClass)); return self(); } - /** Queue a FuncTaskTransformations#outputAs(JavaFilterFunction) to be applied later. */ + /** Queue {@code outputAs(filterFn)} to be applied later. */ public SELF outputAs(JavaFilterFunction function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function)); return self(); } - /** Queue a FuncTaskTransformations#outputAs(JavaFilterFunction, Class) to be applied later. */ + /** Queue {@code outputAs(filterFn, argClass)} to be applied later. */ public SELF outputAs(JavaFilterFunction function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function, argClass)); return self(); } - /** Queue a FuncTaskTransformations#outputAs(JavaContextFunction) to be applied later. */ + /** Queue {@code outputAs(ctxFn)} to be applied later. */ public SELF outputAs(JavaContextFunction function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function)); return self(); } - /** Queue a FuncTaskTransformations#outputAs(JavaContextFunction, Class) to be applied later. */ + /** Queue {@code outputAs(ctxFn, argClass)} to be applied later. */ public SELF outputAs(JavaContextFunction function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(function, argClass)); return self(); } - // ---------- FuncTaskTransformations passthroughs: inputFrom ---------- + /** + * Queue {@code outputAs(jqExpression)} (JQ string) to be applied later. Example: {@code + * outputAs(FuncDSL.selectFirstStringify())} + */ + public SELF outputAs(String jqExpression) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).outputAs(jqExpression)); + return self(); + } + + // --------------------------------------------------------------------------- + // FuncTaskTransformations passthroughs: INPUT (fn/context/filter + JQ) + // --------------------------------------------------------------------------- - /** Queue a FuncTaskTransformations#inputFrom(Function) to be applied later. */ + /** Queue {@code inputFrom(fn)} to be applied later. */ public SELF inputFrom(Function function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function)); return self(); } - /** Queue a FuncTaskTransformations#inputFrom(Function, Class) to be applied later. */ + /** Queue {@code inputFrom(fn, argClass)} to be applied later. */ public SELF inputFrom(Function function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function, argClass)); return self(); } - /** Queue a FuncTaskTransformations#inputFrom(JavaFilterFunction) to be applied later. */ + /** Queue {@code inputFrom(filterFn)} to be applied later. */ public SELF inputFrom(JavaFilterFunction function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function)); return self(); } - /** Queue a FuncTaskTransformations#inputFrom(JavaFilterFunction, Class) to be applied later. */ + /** Queue {@code inputFrom(filterFn, argClass)} to be applied later. */ public SELF inputFrom(JavaFilterFunction function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function, argClass)); return self(); } - /** Queue a FuncTaskTransformations#inputFrom(JavaContextFunction) to be applied later. */ + /** Queue {@code inputFrom(ctxFn)} to be applied later. */ public SELF inputFrom(JavaContextFunction function) { postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function)); return self(); } - /** Queue a FuncTaskTransformations#inputFrom(JavaContextFunction, Class) to be applied later. */ + /** Queue {@code inputFrom(ctxFn, argClass)} to be applied later. */ public SELF inputFrom(JavaContextFunction function, Class argClass) { postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(function, argClass)); return self(); } - // ---------- wiring into the underlying list/builder ---------- + /** + * Queue {@code inputFrom(jqExpression)} (JQ string) to be applied later. Example: {@code + * inputFrom(".payload")} + */ + public SELF inputFrom(String jqExpression) { + postConfigurers.add(b -> ((FuncTaskTransformations) b).inputFrom(jqExpression)); + return self(); + } + + // --------------------------------------------------------------------------- + // wiring into the underlying list/builder + // --------------------------------------------------------------------------- @Override public final void accept(FuncTaskItemListBuilder list) { diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java index c603b02c3..65e8456b2 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncTransformations.java @@ -16,7 +16,9 @@ package io.serverlessworkflow.fluent.func.spi; import io.serverlessworkflow.api.types.Input; +import io.serverlessworkflow.api.types.InputFrom; import io.serverlessworkflow.api.types.Output; +import io.serverlessworkflow.api.types.OutputAs; import io.serverlessworkflow.api.types.func.InputFromFunction; import io.serverlessworkflow.api.types.func.JavaContextFunction; import io.serverlessworkflow.api.types.func.JavaFilterFunction; @@ -63,6 +65,12 @@ default SELF inputFrom(JavaContextFunction function, Class argCl return (SELF) this; } + @SuppressWarnings("unchecked") + default SELF inputFrom(String jqExpression) { + setInput(new Input().withFrom(new InputFrom().withString(jqExpression))); + return (SELF) this; + } + @SuppressWarnings("unchecked") default SELF outputAs(Function function) { setOutput(new Output().withAs(new OutputAsFunction().withFunction(function))); @@ -98,4 +106,10 @@ default SELF outputAs(JavaContextFunction function, Class argCla setOutput(new Output().withAs(new OutputAsFunction().withFunction(function, argClass))); return (SELF) this; } + + @SuppressWarnings("unchecked") + default SELF outputAs(String jqExpression) { + setOutput(new Output().withAs(new OutputAs().withString(jqExpression))); + return (SELF) this; + } } diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLConsumeTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLConsumeTest.java new file mode 100644 index 000000000..f7b12c9d9 --- /dev/null +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLConsumeTest.java @@ -0,0 +1,52 @@ +/* + * 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.func; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.consume; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Workflow; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FuncDSLConsumeTest { + + @Test + @DisplayName( + "consume(name, Consumer, Class) produces CallTask and leaves output unchanged by contract") + void consume_produces_CallTask() { + AtomicReference sink = new AtomicReference<>(); + + Workflow wf = + FuncWorkflowBuilder.workflow("consumeStep") + .tasks( + consume( + "sendNewsletter", + (String reviewed) -> sink.set("CALLED:" + reviewed), + String.class)) + .build(); + + List items = wf.getDo(); + assertEquals(1, items.size()); + Task t = items.get(0).getTask(); + assertNotNull(t.getCallTask(), "CallTask should be present for consume step"); + } +} diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java index eed79dcd7..3f0ecc9b5 100644 --- a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java @@ -19,16 +19,19 @@ import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.event; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.function; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.listen; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toAny; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toOne; import static org.junit.jupiter.api.Assertions.*; import io.cloudevents.core.data.BytesCloudEventData; import io.serverlessworkflow.api.types.Export; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.api.types.func.CallJava; import io.serverlessworkflow.api.types.func.JavaFilterFunction; +import io.serverlessworkflow.fluent.func.dsl.FuncDSL; import io.serverlessworkflow.fluent.func.dsl.FuncEmitSpec; import io.serverlessworkflow.fluent.func.dsl.FuncListenSpec; import java.nio.charset.StandardCharsets; @@ -173,4 +176,48 @@ void mixed_chaining_order_and_exports() { ((CallJava) t0.getCallTask().get()).getExport(), "function step should carry export"); assertNotNull(t2.getListenTask().getExport(), "listen step should carry export"); } + + @Test + void step_chaining_with_jq_and_java_mix() { + Workflow wf = + FuncWorkflowBuilder.workflow("mix") + .tasks( + listen("L", toAny("a", "b")) + .inputFrom(FuncDSL.selectFirst()) + .outputAs(FuncDSL.stringifyIfNeeded()) + .when(". != null")) + .build(); + + Task t = wf.getDo().get(0).getTask(); + assertNotNull(t.getListenTask()); + assertEquals(". != null", t.getListenTask().getIf()); + assertNotNull(t.getListenTask().getInput()); + assertNotNull(t.getListenTask().getOutput()); + } + + @Test + void switchWhenOrElse_jq_to_taskName() { + Workflow wf = + FuncWorkflowBuilder.workflow("jqSwitch") + .tasks(FuncDSL.switchWhenOrElse(".approved", "send", "draft")) + .build(); + Task switchTask = wf.getDo().get(0).getTask(); + assertNotNull(switchTask.getSwitchTask()); + var items = switchTask.getSwitchTask().getSwitch(); + assertEquals(2, items.size()); + assertEquals(".approved", items.get(0).getSwitchCase().getWhen()); + } + + @Test + void switchWhenOrElse_jq_to_directive() { + Workflow wf = + FuncWorkflowBuilder.workflow("jqSwitchDir") + .tasks(FuncDSL.switchWhenOrElse(".score >= 80", "pass", FlowDirectiveEnum.END)) + .build(); + Task switchTask = wf.getDo().get(0).getTask(); + var items = switchTask.getSwitchTask().getSwitch(); + assertEquals(".score >= 80", items.get(0).getSwitchCase().getWhen()); + assertEquals( + FlowDirectiveEnum.END, items.get(1).getSwitchCase().getThen().getFlowDirectiveEnum()); + } } diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java index b5a195466..a2e38fd06 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java @@ -22,21 +22,26 @@ import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.executors.CallableTask; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -public class JavaConsumerCallExecutor implements CallableTask { +public class JavaConsumerCallExecutor implements CallableTask> { - private Consumer consumer; + private Consumer consumer; + private Optional> inputClass = Optional.empty(); - public void init(CallJava.CallJavaConsumer task, WorkflowDefinition definition) { + public void init(CallJava.CallJavaConsumer task, WorkflowDefinition definition) { consumer = task.consumer(); + inputClass = task.inputClass(); } @Override public CompletableFuture apply( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { - consumer.accept(input.asJavaObject()); + T typed = JavaFuncUtils.convertT(input, inputClass); + consumer.accept(typed); + return CompletableFuture.completedFuture(input); } diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java new file mode 100644 index 000000000..9e799c632 --- /dev/null +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.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.impl.executors.func; + +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.func.CallJava; +import io.serverlessworkflow.api.types.func.JavaContextFunction; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowModelFactory; +import io.serverlessworkflow.impl.executors.CallableTask; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class JavaContextFunctionCallExecutor + implements CallableTask> { + + private JavaContextFunction function; + private Optional> inputClass = Optional.empty(); + + @Override + public void init(CallJava.CallJavaContextFunction task, WorkflowDefinition definition) { + this.function = task.function(); + this.inputClass = task.inputClass(); + } + + @Override + public CompletableFuture apply( + WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { + + WorkflowModelFactory mf = workflowContext.definition().application().modelFactory(); + T typedIn = JavaFuncUtils.convertT(input, inputClass); + + V out = function.apply(typedIn, workflowContext); + return CompletableFuture.completedFuture(mf.fromAny(input, out)); + } + + @Override + public boolean accept(Class clazz) { + return CallJava.CallJavaContextFunction.class.isAssignableFrom(clazz); + } +} diff --git a/experimental/lambda/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask b/experimental/lambda/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask index 1b69b5d30..59f3338aa 100644 --- a/experimental/lambda/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask +++ b/experimental/lambda/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask @@ -1,4 +1,5 @@ io.serverlessworkflow.impl.executors.func.JavaLoopFunctionIndexCallExecutor io.serverlessworkflow.impl.executors.func.JavaLoopFunctionCallExecutor io.serverlessworkflow.impl.executors.func.JavaFunctionCallExecutor -io.serverlessworkflow.impl.executors.func.JavaConsumerCallExecutor \ No newline at end of file +io.serverlessworkflow.impl.executors.func.JavaConsumerCallExecutor +io.serverlessworkflow.impl.executors.func.JavaContextFunctionCallExecutor \ No newline at end of file diff --git a/experimental/lambda/src/test/java/io/serverless/workflow/impl/CallJavaContextFunctionTest.java b/experimental/lambda/src/test/java/io/serverless/workflow/impl/CallJavaContextFunctionTest.java new file mode 100644 index 000000000..71fcbaf08 --- /dev/null +++ b/experimental/lambda/src/test/java/io/serverless/workflow/impl/CallJavaContextFunctionTest.java @@ -0,0 +1,73 @@ +/* + * 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.serverless.workflow.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Document; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.CallJava; +import io.serverlessworkflow.api.types.func.CallTaskJava; +import io.serverlessworkflow.api.types.func.JavaContextFunction; +import io.serverlessworkflow.impl.WorkflowApplication; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; + +class CallJavaContextFunctionTest { + + // Reuse the same Person type used in CallTest + record Person(String name, int age) {} + + @Test + void testJavaContextFunction_simple() throws InterruptedException, ExecutionException { + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + var ctxFn = + (JavaContextFunction) + (person, workflowContext) -> + person.name + + "@" + + workflowContext.definition().workflow().getDocument().getName(); + + Workflow workflow = + new Workflow() + .withDocument( + new Document() + .withNamespace("test") + .withName("testJavaContextCall") + .withVersion("1.0")) + .withDo( + List.of( + new TaskItem( + "javaContextCall", + new Task() + .withCallTask( + new CallTaskJava(CallJava.function(ctxFn, Person.class)))))); + + var out = + app.workflowDefinition(workflow) + .instance(new Person("Elisa", 30)) + .start() + .get() + .asText() + .orElseThrow(); + + assertThat(out).isEqualTo("Elisa@testJavaContextCall"); + } + } +} diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/CallJava.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/CallJava.java index c23e36fd0..971e3a9c1 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/CallJava.java +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/CallJava.java @@ -25,7 +25,11 @@ public abstract class CallJava extends TaskBase { private static final long serialVersionUID = 1L; public static CallJava consumer(Consumer consumer) { - return new CallJavaConsumer<>(consumer); + return new CallJavaConsumer<>(consumer, Optional.empty()); + } + + public static CallJava consumer(Consumer consumer, Class inputClass) { + return new CallJavaConsumer<>(consumer, Optional.ofNullable(inputClass)); } public static CallJavaFunction function(Function function) { @@ -46,18 +50,27 @@ public static CallJava loopFunction(LoopFunction function, St return new CallJavaLoopFunction<>(function, varName); } - public static class CallJavaConsumer extends CallJava { + public static CallJava function(JavaContextFunction function, Class inputClass) { + return new CallJavaContextFunction<>(function, Optional.ofNullable(inputClass)); + } + public static class CallJavaConsumer extends CallJava { private static final long serialVersionUID = 1L; - private Consumer consumer; + private final Consumer consumer; + private final Optional> inputClass; - public CallJavaConsumer(Consumer consumer) { + public CallJavaConsumer(Consumer consumer, Optional> inputClass) { this.consumer = consumer; + this.inputClass = inputClass; } public Consumer consumer() { return consumer; } + + public Optional> inputClass() { + return inputClass; + } } public static class CallJavaFunction extends CallJava { @@ -80,6 +93,26 @@ public Optional> inputClass() { } } + public static class CallJavaContextFunction extends CallJava { + private static final long serialVersionUID = 1L; + private JavaContextFunction function; + private Optional> inputClass; + + public CallJavaContextFunction( + JavaContextFunction function, Optional> inputClass) { + this.function = function; + this.inputClass = inputClass; + } + + public JavaContextFunction function() { + return function; + } + + public Optional> inputClass() { + return inputClass; + } + } + public static class CallJavaLoopFunction extends CallJava { private static final long serialVersionUID = 1L; From dccb303f62826bc53c544c951c50eade71422b2b Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 22 Oct 2025 12:58:42 -0400 Subject: [PATCH 4/5] Add Class args to switch predicates Signed-off-by: Ricardo Zanini --- .../fluent/func/dsl/FuncDSL.java | 49 +++++-------------- .../fluent/func/FuncDSLTest.java | 19 ------- 2 files changed, 12 insertions(+), 56 deletions(-) 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 cc0543316..e6a0d917b 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 @@ -623,11 +623,13 @@ public static FuncTaskConfigurer switchCase(String taskName, SwitchCaseConfigure * * @param pred predicate * @param thenTask task name when predicate is true + * @param predClass predicate class * @param predicate input type * @return list configurer */ - public static FuncTaskConfigurer switchWhen(Predicate pred, String thenTask) { - return list -> list.switchCase(cases(caseOf(pred).then(thenTask))); + public static FuncTaskConfigurer switchWhen( + Predicate pred, String thenTask, Class predClass) { + return list -> list.switchCase(cases(caseOf(pred, predClass).then(thenTask))); } public static FuncTaskConfigurer switchWhen(String jqExpression, String thenTask) { @@ -640,13 +642,15 @@ public static FuncTaskConfigurer switchWhen(String jqExpression, String thenTask * @param pred predicate * @param thenTask task name when predicate is true * @param otherwise default flow directive when predicate is false + * @param predClass predicate class * @param predicate input type * @return list configurer */ public static FuncTaskConfigurer switchWhenOrElse( - Predicate pred, String thenTask, FlowDirectiveEnum otherwise) { + Predicate pred, String thenTask, FlowDirectiveEnum otherwise, Class predClass) { return list -> - list.switchCase(FuncDSL.cases(caseOf(pred).then(thenTask), caseDefault(otherwise))); + list.switchCase( + FuncDSL.cases(caseOf(pred, predClass).then(thenTask), caseDefault(otherwise))); } /** @@ -655,12 +659,14 @@ public static FuncTaskConfigurer switchWhenOrElse( * @param pred predicate * @param thenTask task name when predicate is true * @param otherwiseTask task name when predicate is false + * @param predClass predicate class * @param predicate input type * @return list configurer */ public static FuncTaskConfigurer switchWhenOrElse( - Predicate pred, String thenTask, String otherwiseTask) { - return list -> list.switchCase(cases(caseOf(pred).then(thenTask), caseDefault(otherwiseTask))); + Predicate pred, String thenTask, String otherwiseTask, Class predClass) { + return list -> + list.switchCase(cases(caseOf(pred, predClass).then(thenTask), caseDefault(otherwiseTask))); } /** @@ -765,35 +771,4 @@ public static FuncTaskConfigurer set(String expr) { public static FuncTaskConfigurer set(Map map) { return list -> list.set(s -> s.expr(map)); } - - // ----- JQ helpers for outputAs / inputFrom ----- // - - /** - * JQ: pick the first element if the value is an array, otherwise return the value as-is. Does not - * stringify. Useful when downstream expects non-string JSON. - * - *
(first(.[]? // empty) // .)
- */ - public static String selectFirst() { - return "(first(.[]? // empty) // .)"; - } - - /** - * JQ: stringify the value only if it is not already a string. - * - *
(if type==\"string\" then . else tostring end)
- */ - public static String stringifyIfNeeded() { - return "(if type==\"string\" then . else tostring end)"; - } - - /** - * JQ: common “normalize to string”: - if array -> take first element - if null/empty -> coerce to - * empty string - if not a string -> tostring - * - *
(first(.[]? // empty) // . // \"\") | (if type==\"string\" then . else tostring end)
- */ - public static String selectFirstStringify() { - return "(first(.[]? // empty) // . // \"\") | (if type==\"string\" then . else tostring end)"; - } } diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java index 3f0ecc9b5..843f6e6eb 100644 --- a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java @@ -19,7 +19,6 @@ import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.event; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.function; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.listen; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toAny; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toOne; import static org.junit.jupiter.api.Assertions.*; @@ -177,24 +176,6 @@ void mixed_chaining_order_and_exports() { assertNotNull(t2.getListenTask().getExport(), "listen step should carry export"); } - @Test - void step_chaining_with_jq_and_java_mix() { - Workflow wf = - FuncWorkflowBuilder.workflow("mix") - .tasks( - listen("L", toAny("a", "b")) - .inputFrom(FuncDSL.selectFirst()) - .outputAs(FuncDSL.stringifyIfNeeded()) - .when(". != null")) - .build(); - - Task t = wf.getDo().get(0).getTask(); - assertNotNull(t.getListenTask()); - assertEquals(". != null", t.getListenTask().getIf()); - assertNotNull(t.getListenTask().getInput()); - assertNotNull(t.getListenTask().getOutput()); - } - @Test void switchWhenOrElse_jq_to_taskName() { Workflow wf = From f6acfc89859dd27810867bba5544ec0ed982570b Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 23 Oct 2025 09:43:47 -0400 Subject: [PATCH 5/5] Incorporating Javi's review Signed-off-by: Ricardo Zanini --- .../impl/executors/func/JavaConsumerCallExecutor.java | 1 - .../impl/executors/func/JavaContextFunctionCallExecutor.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java index a2e38fd06..14db6543d 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaConsumerCallExecutor.java @@ -41,7 +41,6 @@ public CompletableFuture apply( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { T typed = JavaFuncUtils.convertT(input, inputClass); consumer.accept(typed); - return CompletableFuture.completedFuture(input); } diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java index 9e799c632..91cb5c488 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaContextFunctionCallExecutor.java @@ -42,10 +42,8 @@ public void init(CallJava.CallJavaContextFunction task, WorkflowDefinition @Override public CompletableFuture apply( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { - WorkflowModelFactory mf = workflowContext.definition().application().modelFactory(); T typedIn = JavaFuncUtils.convertT(input, inputClass); - V out = function.apply(typedIn, workflowContext); return CompletableFuture.completedFuture(mf.fromAny(input, out)); }