From 98a4bf7b4d47dddabefce6f27943f2a7240210dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Andr=C3=A9?= <92062874+matheusandre1@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:59:39 -0300 Subject: [PATCH] feat: support to subflows on the Java DSL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matheus André <92062874+matheusandre1@users.noreply.github.com> --- .../fluent/spec/DoTaskBuilder.java | 6 ++ .../fluent/spec/TaskItemListBuilder.java | 13 ++++ .../fluent/spec/WorkflowTaskBuilder.java | 56 +++++++++++++++ .../spec/configurers/WorkflowConfigurer.java | 22 ++++++ .../fluent/spec/dsl/DSL.java | 17 +++++ .../fluent/spec/dsl/WorkflowSpec.java | 71 +++++++++++++++++++ .../fluent/spec/spi/DoFluent.java | 4 +- .../fluent/spec/spi/WorkflowFluent.java | 29 ++++++++ .../fluent/spec/spi/WorkflowTaskFluent.java | 68 ++++++++++++++++++ .../fluent/spec/WorkflowBuilderTest.java | 48 +++++++++++++ .../fluent/spec/dsl/DSLTest.java | 24 +++++++ 11 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowTaskBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/WorkflowConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/WorkflowSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowFluent.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowTaskFluent.java diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java index 98f599a7b..889221498 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java @@ -97,4 +97,10 @@ public DoTaskBuilder openapi(String name, Consumer items this.listBuilder().openapi(name, itemsConfigurer); return this; } + + @Override + public DoTaskBuilder workflow(String name, Consumer itemsConfigurer) { + this.listBuilder().workflow(name, itemsConfigurer); + return this; + } } 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 6a0736b00..cb03e6b28 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 @@ -146,4 +146,17 @@ public TaskItemListBuilder openapi( return addTaskItem(new TaskItem(name, task)); } + + @Override + public TaskItemListBuilder workflow(String name, Consumer itemsConfigurer) { + name = defaultNameAndRequireConfig(name, itemsConfigurer); + + final WorkflowTaskBuilder workflowTaskBuilder = new WorkflowTaskBuilder(); + itemsConfigurer.accept(workflowTaskBuilder); + + final Task task = new Task(); + task.setRunTask(workflowTaskBuilder.build()); + + return addTaskItem(new TaskItem(name, task)); + } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowTaskBuilder.java new file mode 100644 index 000000000..adef4214f --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowTaskBuilder.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.spec; + +import io.serverlessworkflow.api.types.RunTask; +import io.serverlessworkflow.api.types.RunTaskConfigurationUnion; +import io.serverlessworkflow.api.types.RunWorkflow; +import io.serverlessworkflow.api.types.SubflowConfiguration; +import io.serverlessworkflow.fluent.spec.spi.WorkflowTaskFluent; + +public class WorkflowTaskBuilder extends TaskBaseBuilder + implements WorkflowTaskFluent { + + private final RunTask task; + private final RunWorkflow configuration; + private final SubflowConfiguration workflow; + + WorkflowTaskBuilder() { + this.task = new RunTask(); + this.configuration = new RunWorkflow(); + this.workflow = new SubflowConfiguration(); + this.configuration.setWorkflow(this.workflow); + this.task.setRun(new RunTaskConfigurationUnion().withRunWorkflow(this.configuration)); + this.setTask(task); + } + + @Override + public WorkflowTaskBuilder self() { + return this; + } + + public RunWorkflow config() { + return this.configuration; + } + + public SubflowConfiguration workflow() { + return this.workflow; + } + + public RunTask build() { + return this.task; + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/WorkflowConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/WorkflowConfigurer.java new file mode 100644 index 000000000..087b78d73 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/WorkflowConfigurer.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.spec.configurers; + +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface WorkflowConfigurer extends Consumer {} 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 52a6f07e6..ee1a364d2 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 @@ -33,6 +33,7 @@ import io.serverlessworkflow.fluent.spec.configurers.TasksConfigurer; import io.serverlessworkflow.fluent.spec.configurers.TryCatchConfigurer; import io.serverlessworkflow.fluent.spec.configurers.TryConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.WorkflowConfigurer; import io.serverlessworkflow.types.Errors; import java.net.URI; import java.util.List; @@ -102,6 +103,18 @@ public static CallOpenAPISpec openapi() { return new CallOpenAPISpec(); } + public static WorkflowSpec workflow(String namespace, String name, String version) { + return new WorkflowSpec().namespace(namespace).name(name).version(version); + } + + public static WorkflowSpec workflow(String namespace, String name) { + return new WorkflowSpec().namespace(namespace).name(name); + } + + public static WorkflowSpec workflow() { + return new WorkflowSpec(); + } + /** * Convenience for defining a {@code use} block with a single secret. * @@ -623,6 +636,10 @@ public static TasksConfigurer call(CallOpenAPIConfigurer configurer) { return list -> list.openapi(configurer); } + public static TasksConfigurer workflow(WorkflowConfigurer configurer) { + return list -> list.workflow(configurer); + } + /** * Create a {@link TasksConfigurer} that adds a {@code set} task using a low-level configurer. * diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/WorkflowSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/WorkflowSpec.java new file mode 100644 index 000000000..9c3d0539d --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/WorkflowSpec.java @@ -0,0 +1,71 @@ +/* + * 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.api.types.RunTaskConfiguration; +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.WorkflowConfigurer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public final class WorkflowSpec implements WorkflowConfigurer { + + private final List> steps = new ArrayList<>(); + + public WorkflowSpec namespace(String namespace) { + steps.add(b -> b.namespace(namespace)); + return this; + } + + public WorkflowSpec name(String name) { + steps.add(b -> b.name(name)); + return this; + } + + public WorkflowSpec version(String version) { + steps.add(b -> b.version(version)); + return this; + } + + public WorkflowSpec input(Map input) { + steps.add(b -> b.input(input)); + return this; + } + + public WorkflowSpec input(String key, Object value) { + steps.add(b -> b.input(key, value)); + return this; + } + + public WorkflowSpec await(boolean await) { + steps.add(b -> b.await(await)); + return this; + } + + public WorkflowSpec returnType(RunTaskConfiguration.ProcessReturnType returnType) { + steps.add(b -> b.returnType(returnType)); + return this; + } + + @Override + public void accept(WorkflowTaskBuilder builder) { + for (var s : steps) { + s.accept(builder); + } + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java index c11c2d194..4d1048fbf 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java @@ -26,6 +26,7 @@ import io.serverlessworkflow.fluent.spec.SwitchTaskBuilder; import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; /** * Documents the exposed fluent `do` DSL. @@ -44,4 +45,5 @@ public interface DoFluent ForkFluent, ListenFluent, RaiseFluent, - CallOpenAPIFluent {} + CallOpenAPIFluent, + WorkflowFluent {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowFluent.java new file mode 100644 index 000000000..adecdf6aa --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowFluent.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.spec.spi; + +import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; +import java.util.UUID; +import java.util.function.Consumer; + +public interface WorkflowFluent, LIST> { + + LIST workflow(String name, Consumer itemsConfigurer); + + default LIST workflow(Consumer itemsConfigurer) { + return this.workflow(UUID.randomUUID().toString(), itemsConfigurer); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowTaskFluent.java new file mode 100644 index 000000000..f5f9a940c --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/WorkflowTaskFluent.java @@ -0,0 +1,68 @@ +/* + * 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.RunTaskConfiguration; +import io.serverlessworkflow.api.types.SubflowInput; +import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; +import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; +import java.util.Map; + +public interface WorkflowTaskFluent> { + + SELF self(); + + default SELF namespace(String namespace) { + ((WorkflowTaskBuilder) this.self()).workflow().setNamespace(namespace); + return self(); + } + + default SELF name(String name) { + ((WorkflowTaskBuilder) this.self()).workflow().setName(name); + return self(); + } + + default SELF version(String version) { + ((WorkflowTaskBuilder) this.self()).workflow().setVersion(version); + return self(); + } + + default SELF input(Map input) { + final SubflowInput subflowInput = new SubflowInput(); + input.forEach(subflowInput::setAdditionalProperty); + ((WorkflowTaskBuilder) this.self()).workflow().setInput(subflowInput); + return self(); + } + + default SELF input(String key, Object value) { + final WorkflowTaskBuilder builder = (WorkflowTaskBuilder) this.self(); + if (builder.workflow().getInput() == null) { + builder.workflow().setInput(new SubflowInput()); + } + builder.workflow().getInput().setAdditionalProperty(key, value); + return self(); + } + + default SELF await(boolean await) { + ((WorkflowTaskBuilder) this.self()).config().setAwait(await); + return self(); + } + + default SELF returnType(RunTaskConfiguration.ProcessReturnType returnType) { + ((WorkflowTaskBuilder) this.self()).config().setReturn(returnType); + return self(); + } +} diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java index d62fd22af..99112271d 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java @@ -38,6 +38,7 @@ import io.serverlessworkflow.api.types.OneEventConsumptionStrategy; import io.serverlessworkflow.api.types.RetryLimitAttempt; import io.serverlessworkflow.api.types.RetryPolicy; +import io.serverlessworkflow.api.types.RunTaskConfiguration; import io.serverlessworkflow.api.types.SetTask; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.TryTask; @@ -517,4 +518,51 @@ void testDoTaskCallHTTPRedirectAndOutput() { call.getWith().getOutput(), "Output should be overridden"); } + + @Test + void testDoTaskRunWorkflow() { + Workflow wf = + WorkflowBuilder.workflow("parentFlow") + .tasks( + d -> + d.workflow( + "runChild", + w -> + w.namespace("org.acme") + .name("childFlow") + .version("1.0.0") + .input(Map.of("id", 42, "region", "us-east")) + .await(false) + .returnType(RunTaskConfiguration.ProcessReturnType.NONE))) + .build(); + + var runTask = wf.getDo().get(0).getTask().getRunTask(); + assertNotNull(runTask, "RunTask should be present"); + assertNotNull(runTask.getRun(), "RunTask configuration should be present"); + assertNotNull(runTask.getRun().getRunWorkflow(), "RunWorkflow should be selected"); + assertEquals("org.acme", runTask.getRun().getRunWorkflow().getWorkflow().getNamespace()); + assertEquals("childFlow", runTask.getRun().getRunWorkflow().getWorkflow().getName()); + assertEquals("1.0.0", runTask.getRun().getRunWorkflow().getWorkflow().getVersion()); + assertEquals( + 42, + runTask + .getRun() + .getRunWorkflow() + .getWorkflow() + .getInput() + .getAdditionalProperties() + .get("id")); + assertEquals( + "us-east", + runTask + .getRun() + .getRunWorkflow() + .getWorkflow() + .getInput() + .getAdditionalProperties() + .get("region")); + assertEquals( + RunTaskConfiguration.ProcessReturnType.NONE, runTask.getRun().getRunWorkflow().getReturn()); + assertEquals(false, runTask.getRun().getRunWorkflow().isAwait()); + } } diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java index 80b9d1e1b..08c0ab512 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java @@ -24,10 +24,12 @@ import static io.serverlessworkflow.fluent.spec.dsl.DSL.http; import static io.serverlessworkflow.fluent.spec.dsl.DSL.secrets; import static io.serverlessworkflow.fluent.spec.dsl.DSL.to; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.workflow; import static org.assertj.core.api.Assertions.assertThat; import io.serverlessworkflow.api.types.HTTPArguments; import io.serverlessworkflow.api.types.ListenTaskConfiguration; +import io.serverlessworkflow.api.types.RunTaskConfiguration; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.spec.WorkflowBuilder; import io.serverlessworkflow.types.Errors; @@ -281,4 +283,26 @@ void use_spec_accumulates_secrets_and_auths() { assertThat(use.getAuthentications().getAdditionalProperties().keySet()) .containsExactly("basic-auth", "bearer-auth"); } + + @Test + void when_dsl_subflow_workflow_task() { + Workflow wf = + WorkflowBuilder.workflow("parent", "ns", "1") + .tasks( + workflow( + workflow("child.ns", "child-flow", "2.3.4") + .input("id", 99) + .await(false) + .returnType(RunTaskConfiguration.ProcessReturnType.NONE))) + .build(); + + var run = wf.getDo().get(0).getTask().getRunTask().getRun().getRunWorkflow(); + assertThat(run).isNotNull(); + assertThat(run.getWorkflow().getNamespace()).isEqualTo("child.ns"); + assertThat(run.getWorkflow().getName()).isEqualTo("child-flow"); + assertThat(run.getWorkflow().getVersion()).isEqualTo("2.3.4"); + assertThat(run.getWorkflow().getInput().getAdditionalProperties().get("id")).isEqualTo(99); + assertThat(run.isAwait()).isFalse(); + assertThat(run.getReturn()).isEqualTo(RunTaskConfiguration.ProcessReturnType.NONE); + } }