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 0e01dd60f..0546a12c4 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 @@ -19,6 +19,7 @@ import io.serverlessworkflow.fluent.func.spi.FuncDoFluent; import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.BaseDoTaskBuilder; +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; import java.util.function.Consumer; public class FuncDoTaskBuilder extends BaseDoTaskBuilder @@ -96,4 +97,22 @@ public FuncDoTaskBuilder openapi( this.listBuilder().openapi(name, itemsConfigurer); return this; } + + @Override + public FuncDoTaskBuilder workflow(String name, Consumer itemsConfigurer) { + this.listBuilder().workflow(name, itemsConfigurer); + return this; + } + + @Override + public FuncDoTaskBuilder subflow(String name, Consumer itemsConfigurer) { + this.listBuilder().subflow(name, itemsConfigurer); + return this; + } + + @Override + public FuncDoTaskBuilder subflow(Consumer itemsConfigurer) { + this.listBuilder().subflow(itemsConfigurer); + return this; + } } 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 da32d61dc..6bd7ede90 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 @@ -22,6 +22,8 @@ import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.fluent.func.spi.FuncDoFluent; import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; import java.util.List; import java.util.function.Consumer; @@ -154,4 +156,28 @@ public FuncTaskItemListBuilder openapi( return this.addTaskItem(new TaskItem(name, task)); } + + @Override + public FuncTaskItemListBuilder workflow( + String name, Consumer itemsConfigurer) { + final TaskItemListBuilder delegate = new TaskItemListBuilder(this.mutableList().size()); + delegate.workflow(name, itemsConfigurer); + final List taskItems = delegate.build(); + if (taskItems.size() != 1) { + throw new IllegalStateException( + "Expected workflow delegate to build exactly 1 TaskItem, but got " + taskItems.size()); + } + return addTaskItem(taskItems.get(0)); + } + + @Override + public FuncTaskItemListBuilder subflow( + String name, Consumer itemsConfigurer) { + return this.workflow(name, itemsConfigurer); + } + + @Override + public FuncTaskItemListBuilder subflow(Consumer itemsConfigurer) { + return this.workflow(itemsConfigurer); + } } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java index 746d0ce1f..ca7b0521e 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 @@ -38,9 +38,12 @@ import io.serverlessworkflow.fluent.spec.EventFilterBuilder; import io.serverlessworkflow.fluent.spec.ScheduleBuilder; import io.serverlessworkflow.fluent.spec.TimeoutBuilder; +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; import io.serverlessworkflow.fluent.spec.configurers.AuthenticationConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.WorkflowConfigurer; import io.serverlessworkflow.fluent.spec.dsl.DSL; import io.serverlessworkflow.fluent.spec.dsl.UseSpec; +import io.serverlessworkflow.fluent.spec.dsl.WorkflowSpec; import io.serverlessworkflow.impl.TaskContextData; import io.serverlessworkflow.impl.WorkflowContextData; import java.net.URI; @@ -927,14 +930,19 @@ public static FuncTaskConfigurer switchWhen(String jqExpression, String thenTask * @return list configurer */ public static FuncTaskConfigurer switchWhenOrElse( - Predicate pred, String thenTask, FlowDirectiveEnum otherwise, Class predClass) { + Predicate pred, + String thenTask, + io.serverlessworkflow.api.types.FlowDirectiveEnum otherwise, + Class predClass) { return list -> list.switchCase( FuncDSL.cases(caseOf(pred, predClass).then(thenTask), caseDefault(otherwise))); } public static FuncTaskConfigurer switchWhenOrElse( - SerializablePredicate pred, String thenTask, FlowDirectiveEnum otherwise) { + SerializablePredicate pred, + String thenTask, + io.serverlessworkflow.api.types.FlowDirectiveEnum otherwise) { return switchWhenOrElse(pred, thenTask, otherwise, ReflectionUtils.inferInputType(pred)); } @@ -975,7 +983,9 @@ public static FuncTaskConfigurer switchWhenOrElse( * @return list configurer */ public static FuncTaskConfigurer switchWhenOrElse( - String jqExpression, String thenTask, FlowDirectiveEnum otherwise) { + String jqExpression, + String thenTask, + io.serverlessworkflow.api.types.FlowDirectiveEnum otherwise) { Objects.requireNonNull(jqExpression, "jqExpression"); Objects.requireNonNull(thenTask, "thenTask"); @@ -1072,6 +1082,111 @@ public static FuncTaskConfigurer set(Map map) { return list -> list.set(s -> s.expr(map)); } + /** + * Create a {@link FuncTaskConfigurer} that adds a workflow subflow task. + * + * @param configurer configurer for the nested workflow task + * @return a {@link FuncTaskConfigurer} that adds a workflow task to the tasks list + */ + /** + * Create a {@link FuncTaskConfigurer} that adds a sub-workflow call task using a {@link + * WorkflowConfigurer}. + * + *

+ * + *

{@code
+   * tasks(
+   *  subflow(workflow("org.acme", "sub-workflow", "0.1.0").input(...)
+   * );
+   * }
+ * + * @param configurer nested workflow configurer + * @return a {@link FuncTaskConfigurer} that adds a workflow task to the tasks list + */ + public static FuncTaskConfigurer subflow(WorkflowConfigurer configurer) { + Objects.requireNonNull(configurer, "configurer"); + return list -> list.subflow(configurer); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named sub-workflow call task. + * + * @param name task name + * @param configurer nested workflow configurer + * @return a {@link FuncTaskConfigurer} that adds a workflow task to the tasks list + */ + public static FuncTaskConfigurer subflow(String name, Consumer configurer) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(configurer, "configurer"); + return list -> list.subflow(name, configurer); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds an unnamed sub-workflow call task. + * + * @param configurer nested workflow configurer + * @return a {@link FuncTaskConfigurer} that adds a workflow task to the tasks list + */ + public static FuncTaskConfigurer subflow(Consumer configurer) { + Objects.requireNonNull(configurer, "configurer"); + return list -> list.subflow(configurer); + } + + /** + * Alias for {@link #subflow(WorkflowConfigurer)}. + * + * @param configurer nested workflow configurer + * @return a {@link FuncTaskConfigurer} that adds a workflow task to the tasks list + */ + public static FuncTaskConfigurer workflowTask(WorkflowConfigurer configurer) { + return subflow(configurer); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a workflow subflow task. + * + * @param configurer configurer for the nested workflow task + * @return a {@link FuncTaskConfigurer} that adds a workflow task to the tasks list + * @deprecated use {@link #subflow(WorkflowConfigurer)} to avoid ambiguity with spec-side factory + * methods + */ + @Deprecated + public static FuncTaskConfigurer workflow(WorkflowConfigurer configurer) { + return subflow(configurer); + } + + /** + * Create a new {@link WorkflowSpec} to be used as a factory for workflow definitions. + * + * @param namespace workflow namespace + * @param name workflow name + * @param version workflow version + * @return a new {@link WorkflowSpec} instance + */ + public static WorkflowSpec workflow(String namespace, String name, String version) { + return DSL.workflow(namespace, name, version); + } + + /** + * Create a new {@link WorkflowSpec} to be used as a factory for workflow definitions. + * + * @param namespace workflow namespace + * @param name workflow name + * @return a new {@link WorkflowSpec} instance + */ + public static WorkflowSpec workflow(String namespace, String name) { + return DSL.workflow(namespace, name); + } + + /** + * Create a new {@link WorkflowSpec} to be used as a factory for workflow definitions. + * + * @return a new {@link WorkflowSpec} instance + */ + public static WorkflowSpec workflow() { + return DSL.workflow(); + } + // --------------------------------------------------------------------------- // HTTP / OpenAPI // --------------------------------------------------------------------------- diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java index ee9f857e0..361290b70 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java @@ -24,6 +24,7 @@ import io.serverlessworkflow.fluent.func.FuncListenTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSetTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; import io.serverlessworkflow.fluent.spec.spi.CallHttpFluent; import io.serverlessworkflow.fluent.spec.spi.CallOpenAPIFluent; import io.serverlessworkflow.fluent.spec.spi.EmitFluent; @@ -32,6 +33,8 @@ import io.serverlessworkflow.fluent.spec.spi.ListenFluent; import io.serverlessworkflow.fluent.spec.spi.SetFluent; import io.serverlessworkflow.fluent.spec.spi.SwitchFluent; +import io.serverlessworkflow.fluent.spec.spi.WorkflowFluent; +import java.util.function.Consumer; public interface FuncDoFluent> extends SetFluent, @@ -42,4 +45,14 @@ public interface FuncDoFluent> ListenFluent, CallFnFluent, CallHttpFluent, - CallOpenAPIFluent {} + CallOpenAPIFluent, + WorkflowFluent { + + default SELF subflow(String name, Consumer itemsConfigurer) { + return this.workflow(name, itemsConfigurer); + } + + default SELF subflow(Consumer itemsConfigurer) { + return this.workflow(itemsConfigurer); + } +} 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 a11d2c753..461ba1b1f 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 @@ -23,17 +23,19 @@ import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.http; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.listen; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.produced; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.subflow; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.switchWhenOrElse; import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.toOne; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.workflow; import static io.serverlessworkflow.fluent.spec.dsl.DSL.use; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import io.serverlessworkflow.api.types.CallHTTP; 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; @@ -174,6 +176,50 @@ void mixed_chaining_order_and_exports() { assertNotNull(t2.getListenTask().getExport(), "listen step should carry export"); } + @Test + void subflow_task_builds_from_func_dsl() { + Workflow wf = + FuncWorkflowBuilder.workflow("step-subflow") + .tasks( + subflow(workflow("child.ns", "child-flow", "2.3.4").input("id", 99).await(false))) + .build(); + + List items = wf.getDo(); + assertEquals(1, items.size()); + + Task t = items.get(0).getTask(); + assertNotNull(t.getRunTask(), "RunTask expected"); + var run = t.getRunTask().getRun().getRunWorkflow(); + assertNotNull(run, "RunWorkflow should be present"); + assertEquals("child.ns", run.getWorkflow().getNamespace()); + assertEquals("child-flow", run.getWorkflow().getName()); + assertEquals("2.3.4", run.getWorkflow().getVersion()); + assertEquals(99, run.getWorkflow().getInput().getAdditionalProperties().get("id")); + assertFalse(run.isAwait(), "await(false) should be preserved as false"); + } + + @Test + void subflow_builder_style_builds_correctly() { + Workflow wf = + FuncWorkflowBuilder.workflow("subflow-builder") + .tasks( + subflow( + "my-subflow", + sub -> + sub.namespace("child.ns") + .name("child-flow") + .version("1.0.0") + .input(Map.of("key", "val")))) + .build(); + + Task t = wf.getDo().get(0).getTask(); + assertNotNull(t.getRunTask()); + var run = t.getRunTask().getRun().getRunWorkflow(); + assertEquals("my-subflow", wf.getDo().get(0).getName()); + assertEquals("child-flow", run.getWorkflow().getName()); + assertEquals("val", run.getWorkflow().getInput().getAdditionalProperties().get("key")); + } + @Test void switchWhenOrElse_jq_to_taskName() { Workflow wf = @@ -191,13 +237,16 @@ void switchWhenOrElse_jq_to_taskName() { void switchWhenOrElse_jq_to_directive() { Workflow wf = FuncWorkflowBuilder.workflow("jqSwitchDir") - .tasks(switchWhenOrElse(".score >= 80", "pass", FlowDirectiveEnum.END)) + .tasks( + switchWhenOrElse( + ".score >= 80", "pass", io.serverlessworkflow.api.types.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()); + io.serverlessworkflow.api.types.FlowDirectiveEnum.END, + items.get(1).getSwitchCase().getThen().getFlowDirectiveEnum()); } @Test @@ -447,7 +496,9 @@ void function_step_then_task_name_sets_flow_directive() { void function_step_then_flow_directive_enum_sets_end() { Workflow wf = FuncWorkflowBuilder.workflow("intelligent-newsletter") - .tasks(function("myfunction", String::trim, String.class).then(FlowDirectiveEnum.END)) + .tasks( + function("myfunction", String::trim, String.class) + .then(io.serverlessworkflow.api.types.FlowDirectiveEnum.END)) .build(); List items = wf.getDo(); @@ -459,7 +510,7 @@ void function_step_then_flow_directive_enum_sets_end() { CallJava callJava = (CallJava) t.getCallTask().get(); assertNotNull(callJava.getThen(), "then() should be set on the task"); assertEquals( - FlowDirectiveEnum.END, + io.serverlessworkflow.api.types.FlowDirectiveEnum.END, callJava.getThen().getFlowDirectiveEnum(), "then() should be FlowDirectiveEnum.END"); } @@ -495,7 +546,7 @@ void consume_step_then_flow_directive_enum_sets_end() { FuncWorkflowBuilder.workflow("intelligent-newsletter") .tasks( consume("sendNewsletter", (String s) -> {}, String.class) - .then(FlowDirectiveEnum.END)) + .then(io.serverlessworkflow.api.types.FlowDirectiveEnum.END)) .build(); List items = wf.getDo(); @@ -507,7 +558,7 @@ void consume_step_then_flow_directive_enum_sets_end() { CallJava callJava = (CallJava) t.getCallTask().get(); assertNotNull(callJava.getThen(), "then() should be set on the consume task"); assertEquals( - FlowDirectiveEnum.END, + io.serverlessworkflow.api.types.FlowDirectiveEnum.END, callJava.getThen().getFlowDirectiveEnum(), "then() should be FlowDirectiveEnum.END"); }