From e0e57bf1ea4bcef9c70fa6957766843fa8b5cdf6 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 26 Mar 2026 14:48:42 -0400 Subject: [PATCH 1/2] fix #1251 - Add timers and scheduler to the DSL Signed-off-by: Ricardo Zanini --- .../fluent/func/dsl/FuncDSL.java | 321 ++++++++++++++++-- .../fluent/func/dsl/Step.java | 11 + .../fluent/func/FuncDSLScheduleTest.java | 127 +++++++ .../fluent/spec/BaseWorkflowBuilder.java | 27 ++ .../fluent/spec/ScheduleBuilder.java | 135 ++++++++ .../fluent/spec/TaskBaseBuilder.java | 18 +- .../fluent/spec/TimeoutBuilder.java | 54 +++ .../fluent/spec/dsl/BaseListenSpec.java | 12 + .../fluent/spec/dsl/DSL.java | 187 ++++++++++ .../fluent/spec/ScheduleBuilderTest.java | 86 +++++ .../fluent/spec/dsl/DSLScheduleTest.java | 106 ++++++ .../fluent/spec/dsl/DSLTimeoutTest.java | 143 ++++++++ impl/persistence/pom.xml | 2 +- 13 files changed, 1190 insertions(+), 39 deletions(-) create mode 100644 experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLScheduleTest.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ScheduleBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TimeoutBuilder.java create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/ScheduleBuilderTest.java create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLScheduleTest.java create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTimeoutTest.java 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 6ca79b0da..d5d14ef8c 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.OAuth2AuthenticationData; import io.serverlessworkflow.api.types.func.ContextFunction; import io.serverlessworkflow.api.types.func.FilterFunction; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; @@ -27,7 +28,13 @@ import io.serverlessworkflow.fluent.func.configurers.FuncCallOpenAPIConfigurer; import io.serverlessworkflow.fluent.func.configurers.FuncTaskConfigurer; import io.serverlessworkflow.fluent.func.configurers.SwitchCaseConfigurer; +import io.serverlessworkflow.fluent.spec.AbstractEventConsumptionStrategyBuilder; +import io.serverlessworkflow.fluent.spec.EventFilterBuilder; +import io.serverlessworkflow.fluent.spec.ScheduleBuilder; +import io.serverlessworkflow.fluent.spec.TimeoutBuilder; import io.serverlessworkflow.fluent.spec.configurers.AuthenticationConfigurer; +import io.serverlessworkflow.fluent.spec.dsl.DSL; +import io.serverlessworkflow.fluent.spec.dsl.UseSpec; import io.serverlessworkflow.impl.TaskContextData; import io.serverlessworkflow.impl.WorkflowContextData; import java.net.URI; @@ -1039,13 +1046,7 @@ public static FuncTaskConfigurer call(String name, FuncCallHttpConfigurer config *

This overload creates an unnamed HTTP task. * *

{@code
-   * tasks(
-   * FuncDSL.call(
-   * FuncDSL.http()
-   * .GET()
-   * .endpoint("http://service/api")
-   * )
-   * );
+   * tasks(FuncDSL.call(FuncDSL.http().GET().endpoint("http://service/api")));
    * }
* * @param spec fluent HTTP spec built via {@link #http()} @@ -1059,13 +1060,7 @@ public static FuncTaskConfigurer call(FuncCallHttpStep spec) { * HTTP call using a fluent {@link FuncCallHttpStep} with explicit task name. * *
{@code
-   * tasks(
-   * FuncDSL.call("fetchUsers",
-   * FuncDSL.http()
-   * .GET()
-   * .endpoint("http://service/users")
-   * )
-   * );
+   * tasks(FuncDSL.call("fetchUsers", FuncDSL.http().GET().endpoint("http://service/users")));
    * }
* * @param name task name, or {@code null} for an anonymous task @@ -1084,12 +1079,7 @@ public static FuncTaskConfigurer call(String name, FuncCallHttpStep spec) { * *
{@code
    * FuncWorkflowBuilder.workflow("openapi-call")
-   * .tasks(
-   * FuncDSL.call(
-   * FuncDSL.openapi()
-   * .document("https://petstore.swagger.io/v2/swagger.json", DSL.auth("openapi-auth"))
-   * .operation("getPetById")
-   * )
+   *   .tasks(call(openapi().document("https://petstore.swagger.io/v2/swagger.json", auth("openapi-auth")).operation("getPetById"))
    * )
    * .build();
    * }
@@ -1162,12 +1152,7 @@ public static FuncTaskConfigurer call(String name, FuncCallOpenAPIConfigurer con *

Typical usage: * *

{@code
-   * FuncDSL.call(
-   * FuncDSL.openapi()
-   * .document("https://petstore.swagger.io/v2/swagger.json", DSL.auth("openapi-auth"))
-   * .operation("getPetById")
-   * .parameter("id", 123)
-   * );
+   * FuncDSL.call(openapi().document("https://petstore.swagger.io/v2/swagger.json", DSL.auth("openapi-auth")).operation("getPetById").parameter("id", 123));
    * }
* *

The returned spec is a fluent builder that records operations (document, operation, @@ -1197,12 +1182,7 @@ public static FuncCallOpenAPIStep openapi(String name) { *

Typical usage: * *

{@code
-   * FuncDSL.call(
-   * FuncDSL.http()
-   * .GET()
-   * .endpoint("http://service/api")
-   * .acceptJSON()
-   * );
+   * FuncDSL.call(http().GET().endpoint("http://service/api").acceptJSON());
    * }
* * @return a new {@link FuncCallHttpStep} @@ -1474,8 +1454,8 @@ public static T input(WorkflowContextData context, Class inputClass) { * *
{@code
    * inputFrom((Object obj, TaskContextData taskContextData) -> {
-   *   OrderRequest order = input(taskContextData, OrderRequest.class);
-   *   return order;
+   * OrderRequest order = input(taskContextData, OrderRequest.class);
+   * return order;
    * });
    * }
* @@ -1509,9 +1489,9 @@ public static T input(TaskContextData taskContextData, Class inputClass) * *
{@code
    * .exportAs((object, workflowContext, taskContextData) -> {
-   *     Long output = output(taskContextData, Long.class);
-   *     return output * 2;
-   *  })
+   * Long output = output(taskContextData, Long.class);
+   * return output * 2;
+   * })
    * }
* * @param the type to deserialize the task output into @@ -1530,4 +1510,271 @@ public static T output(TaskContextData taskContextData, Class outputClass + outputClass.getName() + " when calling FuncDSL.output(TaskContextData, Class).")); } + + // --------------------------------------------------------------------------- + // Facades to base DSL (Timeouts, Schedules, Event Strategies) + // --------------------------------------------------------------------------- + + /** + * Shortcut to configure a timeout in days. + * + * @see DSL#timeoutDays(int) + */ + public static Consumer timeoutDays(int days) { + return DSL.timeoutDays(days); + } + + /** + * Shortcut to configure a timeout in hours. + * + * @see DSL#timeoutHours(int) + */ + public static Consumer timeoutHours(int hours) { + return DSL.timeoutHours(hours); + } + + /** + * Shortcut to configure a timeout in minutes. + * + * @see DSL#timeoutMinutes(int) + */ + public static Consumer timeoutMinutes(int minutes) { + return DSL.timeoutMinutes(minutes); + } + + /** + * Shortcut to configure a timeout in seconds. + * + * @see DSL#timeoutSeconds(int) + */ + public static Consumer timeoutSeconds(int seconds) { + return DSL.timeoutSeconds(seconds); + } + + /** + * Shortcut to configure a timeout in milliseconds. + * + * @see DSL#timeoutMillis(int) + */ + public static Consumer timeoutMillis(int milliseconds) { + return DSL.timeoutMillis(milliseconds); + } + + // ---- Schedules ----// + + /** + * @see DSL#every(Consumer) + */ + public static Consumer every(Consumer duration) { + return DSL.every(duration); + } + + /** + * @see DSL#every(String) + */ + public static Consumer every(String durationExpression) { + return DSL.every(durationExpression); + } + + /** + * @see DSL#cron(String) + */ + public static Consumer cron(String cron) { + return DSL.cron(cron); + } + + /** + * @see DSL#after(Consumer) + */ + public static Consumer after(Consumer duration) { + return DSL.after(duration); + } + + /** + * @see DSL#after(String) + */ + public static Consumer after(String durationExpression) { + return DSL.after(durationExpression); + } + + /** + * @see DSL#on(Consumer) + */ + public static Consumer on( + Consumer> strategy) { + return DSL.on(strategy); + } + + // ---- Schedule Event Strategies ----// + + /** + * @see DSL#one(String) + */ + public static Consumer> one(String eventType) { + return DSL.one(eventType); + } + + /** + * @see DSL#one(Consumer) + */ + public static Consumer> one( + Consumer filter) { + return DSL.one(filter); + } + + /** + * @see DSL#all(Consumer[]) + */ + @SafeVarargs + public static Consumer> all( + Consumer... filters) { + return DSL.all(filters); + } + + /** + * @see DSL#any(Consumer[]) + */ + @SafeVarargs + public static Consumer> any( + Consumer... filters) { + return DSL.any(filters); + } + + // --------------------------------------------------------------------------- + // Facades to base DSL (Use, Secrets, and Authentication) + // --------------------------------------------------------------------------- + + /** + * @see DSL#secret(String) + */ + public static UseSpec secret(String secret) { + return DSL.secret(secret); + } + + /** + * @see DSL#secrets(String...) + */ + public static UseSpec secrets(String... secret) { + return DSL.secrets(secret); + } + + /** + * @see DSL#auth(String, AuthenticationConfigurer) + */ + public static UseSpec auth(String name, AuthenticationConfigurer auth) { + return DSL.auth(name, auth); + } + + /** + * @see DSL#use() + */ + public static UseSpec use() { + return DSL.use(); + } + + /** + * @see DSL#use(String) + */ + public static AuthenticationConfigurer use(String authName) { + return DSL.use(authName); + } + + /** + * @see DSL#basic(String, String) + */ + public static AuthenticationConfigurer basic(String username, String password) { + return DSL.basic(username, password); + } + + /** + * @see DSL#basic(String) + */ + public static AuthenticationConfigurer basic(String secret) { + return DSL.basic(secret); + } + + /** + * @see DSL#bearer(String) + */ + public static AuthenticationConfigurer bearer(String token) { + return DSL.bearer(token); + } + + /** + * @see DSL#bearerUse(String) + */ + public static AuthenticationConfigurer bearerUse(String secret) { + return DSL.bearerUse(secret); + } + + /** + * @see DSL#digest(String, String) + */ + public static AuthenticationConfigurer digest(String username, String password) { + return DSL.digest(username, password); + } + + /** + * @see DSL#digest(String) + */ + public static AuthenticationConfigurer digest(String secret) { + return DSL.digest(secret); + } + + /** + * @see DSL#oidc(String, OAuth2AuthenticationData.OAuth2AuthenticationDataGrant) + */ + public static AuthenticationConfigurer oidc( + String authority, OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant) { + return DSL.oidc(authority, grant); + } + + /** + * @see DSL#oidc(String, OAuth2AuthenticationData.OAuth2AuthenticationDataGrant, String, String) + */ + public static AuthenticationConfigurer oidc( + String authority, + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant, + String clientId, + String clientSecret) { + return DSL.oidc(authority, grant, clientId, clientSecret); + } + + /** + * @see DSL#oidc(String) + */ + public static AuthenticationConfigurer oidc(String secret) { + return DSL.oidc(secret); + } + + /** + * @see DSL#oauth2(String, + * io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant) + */ + public static AuthenticationConfigurer oauth2( + String authority, + io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant + grant) { + return DSL.oauth2(authority, grant); + } + + /** + * @see DSL#oauth2(String, + * io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant, + * String, String) + */ + public static AuthenticationConfigurer oauth2( + String authority, + io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant, + String clientId, + String clientSecret) { + return DSL.oauth2(authority, grant, clientId, clientSecret); + } + + /** + * @see DSL#oauth2(String) + */ + public static AuthenticationConfigurer oauth2(String secret) { + return DSL.oauth2(secret); + } } 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 94670629b..d1261d438 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 @@ -23,6 +23,7 @@ import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; import io.serverlessworkflow.fluent.func.spi.FuncTaskTransformations; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; +import io.serverlessworkflow.fluent.spec.TimeoutBuilder; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -592,6 +593,16 @@ public SELF inputFrom(String jqExpression) { return self(); } + public SELF timeout(String durationExpression) { + this.postConfigurers.add(b -> ((TaskBaseBuilder) b).timeout(durationExpression)); + return self(); + } + + public SELF timeout(Consumer timeout) { + this.postConfigurers.add(b -> ((TaskBaseBuilder) b).timeout(timeout)); + return self(); + } + // --------------------------------------------------------------------------- // wiring into the underlying list/builder // --------------------------------------------------------------------------- diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLScheduleTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLScheduleTest.java new file mode 100644 index 000000000..9c2f98c41 --- /dev/null +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLScheduleTest.java @@ -0,0 +1,127 @@ +/* + * 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.after; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.cron; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.every; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.on; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.one; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.timeoutHours; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.timeoutMinutes; +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.serverlessworkflow.api.types.Schedule; +import io.serverlessworkflow.api.types.Workflow; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Tests for Schedule chaining via FuncWorkflowBuilder. */ +class FuncDSLScheduleTest { + + @Test + @DisplayName("schedule(every(expression)) configures an 'every' schedule string expression") + void schedule_every_expression_sets_schedule() { + Workflow wf = + FuncWorkflowBuilder.workflow("schedule-every-expr").schedule(every("PT15M")).build(); + + Schedule schedule = wf.getSchedule(); + assertNotNull(schedule, "Schedule should be configured"); + assertNotNull(schedule.getEvery(), "Every configuration should be set"); + assertEquals( + "PT15M", schedule.getEvery().getDurationLiteral(), "Duration expression should match"); + assertNull(schedule.getEvery().getDurationInline(), "Inline duration should be null"); + } + + @Test + @DisplayName("schedule(every(inline)) configures an 'every' schedule inline duration") + void schedule_every_inline_sets_schedule() { + Workflow wf = + FuncWorkflowBuilder.workflow("schedule-every-inline") + .schedule(every(timeoutMinutes(15))) + .build(); + + Schedule schedule = wf.getSchedule(); + assertNotNull(schedule, "Schedule should be configured"); + assertNotNull(schedule.getEvery(), "Every configuration should be set"); + assertNull(schedule.getEvery().getDurationExpression(), "Duration expression should be null"); + assertNotNull(schedule.getEvery().getDurationInline(), "Inline duration should be set"); + assertEquals(15, schedule.getEvery().getDurationInline().getMinutes(), "Minutes should match"); + } + + @Test + @DisplayName("schedule(cron) configures a 'cron' schedule string") + void schedule_cron_sets_schedule() { + Workflow wf = FuncWorkflowBuilder.workflow("schedule-cron").schedule(cron("0 0 * * *")).build(); + + Schedule schedule = wf.getSchedule(); + assertNotNull(schedule, "Schedule should be configured"); + assertEquals("0 0 * * *", schedule.getCron(), "Cron expression should match"); + assertNull(schedule.getEvery(), "Every configuration should be null"); + } + + @Test + @DisplayName("schedule(after(expression)) configures an 'after' schedule string expression") + void schedule_after_expression_sets_schedule() { + Workflow wf = + FuncWorkflowBuilder.workflow("schedule-after-expr").schedule(after("PT1H")).build(); + + Schedule schedule = wf.getSchedule(); + assertNotNull(schedule, "Schedule should be configured"); + assertNotNull(schedule.getAfter(), "After configuration should be set"); + assertEquals( + "PT1H", schedule.getAfter().getDurationLiteral(), "Duration expression should match"); + assertNull(schedule.getAfter().getDurationInline(), "Inline duration should be null"); + } + + @Test + @DisplayName("schedule(after(inline)) configures an 'after' schedule inline duration") + void schedule_after_inline_sets_schedule() { + Workflow wf = + FuncWorkflowBuilder.workflow("schedule-after-inline") + .schedule(after(timeoutHours(1))) + .build(); + + Schedule schedule = wf.getSchedule(); + assertNotNull(schedule, "Schedule should be configured"); + assertNotNull(schedule.getAfter(), "After configuration should be set"); + assertNull(schedule.getAfter().getDurationExpression(), "Duration expression should be null"); + assertNotNull(schedule.getAfter().getDurationInline(), "Inline duration should be set"); + assertEquals(1, schedule.getAfter().getDurationInline().getHours(), "Hours should match"); + } + + @Test + @DisplayName("schedule(on(one(event))) configures an 'on' schedule event consumption strategy") + void schedule_on_event_sets_schedule() { + Workflow wf = + FuncWorkflowBuilder.workflow("schedule-on-event") + .schedule(on(one("org.acme.startup"))) + .build(); + + Schedule schedule = wf.getSchedule(); + assertNotNull(schedule, "Schedule should be configured"); + assertNotNull(schedule.getOn(), "On configuration should be set"); + assertNotNull( + schedule.getOn().getOneEventConsumptionStrategy(), + "One consumption strategy should be set"); + assertEquals( + "org.acme.startup", + schedule.getOn().getOneEventConsumptionStrategy().getOne().getWith().getType(), + "Event type should match"); + } +} 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 bb16ad6e6..0d305d8a8 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 @@ -15,10 +15,13 @@ */ package io.serverlessworkflow.fluent.spec; +import io.serverlessworkflow.api.types.DoTimeout; import io.serverlessworkflow.api.types.Document; import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.Output; import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Timeout; +import io.serverlessworkflow.api.types.TimeoutAfter; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.spec.spi.TransformationHandlers; import java.util.ArrayList; @@ -159,6 +162,30 @@ public SELF output(Consumer outputBuilderConsumer) { return self(); } + public SELF timeout(String afterDurationExpression) { + this.workflow.setTimeout( + new DoTimeout() + .withTimeoutDefinition( + new Timeout() + .withAfter( + new TimeoutAfter().withDurationExpression(afterDurationExpression)))); + return self(); + } + + public SELF timeout(Consumer timeoutBuilderConsumer) { + final TimeoutBuilder timeoutBuilder = new TimeoutBuilder(); + timeoutBuilderConsumer.accept(timeoutBuilder); + this.workflow.setTimeout(new DoTimeout().withTimeoutDefinition(timeoutBuilder.build())); + return self(); + } + + public SELF schedule(Consumer scheduleBuilderConsumer) { + final ScheduleBuilder scheduleBuilder = new ScheduleBuilder(); + scheduleBuilderConsumer.accept(scheduleBuilder); + this.workflow.setSchedule(scheduleBuilder.build()); + return self(); + } + public Workflow build() { return this.workflow; } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ScheduleBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ScheduleBuilder.java new file mode 100644 index 000000000..8f922c380 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ScheduleBuilder.java @@ -0,0 +1,135 @@ +/* + * 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.Schedule; +import io.serverlessworkflow.api.types.TimeoutAfter; +import java.util.function.Consumer; + +public class ScheduleBuilder { + + private final Schedule schedule; + + public ScheduleBuilder() { + this.schedule = new Schedule(); + } + + private void assertMutuallyExclusive(String newProperty) { + if (this.schedule.getEvery() != null && !"every".equals(newProperty)) { + throw new IllegalStateException( + "Schedule already configured with 'every'. Cannot set '" + newProperty + "'."); + } + if (this.schedule.getCron() != null && !"cron".equals(newProperty)) { + throw new IllegalStateException( + "Schedule already configured with 'cron'. Cannot set '" + newProperty + "'."); + } + if (this.schedule.getAfter() != null && !"after".equals(newProperty)) { + throw new IllegalStateException( + "Schedule already configured with 'after'. Cannot set '" + newProperty + "'."); + } + if (this.schedule.getOn() != null && !"on".equals(newProperty)) { + throw new IllegalStateException( + "Schedule already configured with 'on'. Cannot set '" + newProperty + "'."); + } + } + + private void assertDurationNotSet(TimeoutAfter timeoutAfter, String property) { + if (timeoutAfter == null) return; + if (timeoutAfter.getDurationInline() != null) { + throw new IllegalStateException("Inline duration already specified for '" + property + "'."); + } + if (timeoutAfter.getDurationLiteral() != null && !timeoutAfter.getDurationLiteral().isBlank()) { + throw new IllegalStateException("Duration expression already set for '" + property + "'."); + } + } + + public ScheduleBuilder every(Consumer duration) { + assertMutuallyExclusive("every"); + assertDurationNotSet(this.schedule.getEvery(), "every"); + + final TimeoutBuilder timeoutBuilder = new TimeoutBuilder(); + duration.accept(timeoutBuilder); + + this.schedule.setEvery(timeoutBuilder.build().getAfter()); + return this; + } + + public ScheduleBuilder every(String durationExpression) { + assertMutuallyExclusive("every"); + assertDurationNotSet(this.schedule.getEvery(), "every"); + + if (this.schedule.getEvery() == null) { + this.schedule.setEvery(new TimeoutAfter()); + } + this.schedule.getEvery().setDurationExpression(durationExpression); + return this; + } + + public ScheduleBuilder cron(String cron) { + assertMutuallyExclusive("cron"); + if (this.schedule.getCron() != null) { + throw new IllegalStateException("'cron' is already specified for this schedule."); + } + this.schedule.setCron(cron); + return this; + } + + public ScheduleBuilder after(Consumer duration) { + assertMutuallyExclusive("after"); + assertDurationNotSet(this.schedule.getAfter(), "after"); + + final TimeoutBuilder timeoutBuilder = new TimeoutBuilder(); + duration.accept(timeoutBuilder); + + this.schedule.setAfter(timeoutBuilder.build().getAfter()); + return this; + } + + public ScheduleBuilder after(String durationExpression) { + assertMutuallyExclusive("after"); + assertDurationNotSet(this.schedule.getAfter(), "after"); + + if (this.schedule.getAfter() == null) { + this.schedule.setAfter(new TimeoutAfter()); + } + this.schedule.getAfter().setDurationExpression(durationExpression); + return this; + } + + public ScheduleBuilder on( + Consumer> listenToBuilderConsumer) { + assertMutuallyExclusive("on"); + if (this.schedule.getOn() != null) { + throw new IllegalStateException("'on' is already specified for this schedule."); + } + + final ListenToBuilder listenToBuilder = new ListenToBuilder(); + listenToBuilderConsumer.accept(listenToBuilder); + this.schedule.setOn(listenToBuilder.build()); + return this; + } + + public Schedule build() { + if (this.schedule.getEvery() == null + && this.schedule.getCron() == null + && this.schedule.getAfter() == null + && this.schedule.getOn() == null) { + throw new IllegalStateException( + "A schedule must have exactly one property set: 'every', 'cron', 'after', or 'on'."); + } + return this.schedule; + } +} 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 e7c416951..739fe1e79 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 @@ -21,6 +21,9 @@ import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.Output; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.TaskTimeout; +import io.serverlessworkflow.api.types.Timeout; +import io.serverlessworkflow.api.types.TimeoutAfter; import io.serverlessworkflow.fluent.spec.spi.OutputFluent; import io.serverlessworkflow.fluent.spec.spi.TaskTransformationHandlers; import java.util.function.Consumer; @@ -106,6 +109,19 @@ public T output(Consumer outputConsumer) { return self(); } - // TODO: add timeout, metadata + public T timeout(String durationExpression) { + this.task.setTimeout( + new TaskTimeout() + .withTaskTimeoutDefinition( + new Timeout() + .withAfter(new TimeoutAfter().withDurationExpression(durationExpression)))); + return self(); + } + public T timeout(Consumer timeoutConsumer) { + final TimeoutBuilder timeoutBuilder = new TimeoutBuilder(); + timeoutConsumer.accept(timeoutBuilder); + this.task.setTimeout(new TaskTimeout().withTaskTimeoutDefinition(timeoutBuilder.build())); + return self(); + } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TimeoutBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TimeoutBuilder.java new file mode 100644 index 000000000..7af72ea9d --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TimeoutBuilder.java @@ -0,0 +1,54 @@ +/* + * 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.Timeout; +import io.serverlessworkflow.api.types.TimeoutAfter; +import java.util.function.Consumer; + +public class TimeoutBuilder { + + protected final TimeoutAfter timeout; + + public TimeoutBuilder() { + this.timeout = new TimeoutAfter(); + } + + public TimeoutBuilder duration(Consumer duration) { + if (this.timeout.getDurationLiteral() != null && !this.timeout.getDurationLiteral().isBlank()) { + throw new IllegalStateException( + "Duration expression already set as a string: " + this.timeout.getDurationExpression()); + } + final DurationInlineBuilder durationBuilder = new DurationInlineBuilder(); + duration.accept(durationBuilder); + this.timeout.setDurationInline(durationBuilder.build()); + return this; + } + + public TimeoutBuilder duration(String durationExpression) { + if (this.timeout.getDurationInline() != null) { + throw new IllegalStateException( + "Duration already specified inline for this instance: " + + this.timeout.getDurationInline()); + } + this.timeout.setDurationExpression(durationExpression); + return this; + } + + public Timeout build() { + return new Timeout().withAfter(timeout); + } +} 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 index d32efe396..8d326ef7c 100644 --- 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 @@ -89,6 +89,18 @@ public final SELF one(Consumer filter) { return self(); } + /** + * Applies the configured event consumption strategy to a raw builder. This is useful when the + * strategy needs to be used outside of a listen task, such as in a workflow schedule. + */ + public final void acceptStrategyInto(LISTEN_TO toBuilder) { + Objects.requireNonNull(strategyStep, "listening strategy must be set (all/any/one)"); + strategyStep.accept(toBuilder); + if (untilStep != null) { + untilStep.accept(toBuilder); + } + } + protected final void acceptInto(LISTEN_TASK listenTaskBuilder) { Objects.requireNonNull(strategyStep, "listening strategy must be set (all/any/one)"); toInvoker.to( 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 50bc4021e..f364e9ed9 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,10 +16,14 @@ package io.serverlessworkflow.fluent.spec.dsl; import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.fluent.spec.AbstractEventConsumptionStrategyBuilder; import io.serverlessworkflow.fluent.spec.DoTaskBuilder; import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; +import io.serverlessworkflow.fluent.spec.EventFilterBuilder; import io.serverlessworkflow.fluent.spec.ForkTaskBuilder; +import io.serverlessworkflow.fluent.spec.ScheduleBuilder; import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.TimeoutBuilder; import io.serverlessworkflow.fluent.spec.TryTaskBuilder; import io.serverlessworkflow.fluent.spec.configurers.AuthenticationConfigurer; import io.serverlessworkflow.fluent.spec.configurers.CallHttpConfigurer; @@ -949,4 +953,187 @@ public static Consumer branches(TasksConfigurer... steps) { final Consumer tasks = DSL.tasks(steps); return f -> f.compete(false).branches(tasks); } + + /** + * Shortcut to configure a timeout in days. + * + *

Example: {@code timeout(timeoutDays(2))} + * + * @param days timeout duration in days + * @return a consumer configuring {@link TimeoutBuilder} + */ + public static Consumer timeoutDays(int days) { + return t -> t.duration(d -> d.days(days)); + } + + /** + * Shortcut to configure a timeout in hours. + * + *

Example: {@code timeout(timeoutHours(1))} + * + * @param hours timeout duration in hours + * @return a consumer configuring {@link TimeoutBuilder} + */ + public static Consumer timeoutHours(int hours) { + return t -> t.duration(d -> d.hours(hours)); + } + + /** + * Shortcut to configure a timeout in minutes. + * + *

Example: {@code timeout(timeoutMinutes(15))} + * + * @param minutes timeout duration in minutes + * @return a consumer configuring {@link TimeoutBuilder} + */ + public static Consumer timeoutMinutes(int minutes) { + return t -> t.duration(d -> d.minutes(minutes)); + } + + /** + * Shortcut to configure a timeout in seconds. + * + *

Example: {@code timeout(timeoutSeconds(30))} + * + * @param seconds timeout duration in seconds + * @return a consumer configuring {@link TimeoutBuilder} + */ + public static Consumer timeoutSeconds(int seconds) { + return t -> t.duration(d -> d.seconds(seconds)); + } + + /** + * Shortcut to configure a timeout in milliseconds. + * + *

Example: {@code timeout(timeoutMillis(500))} + * + * @param milliseconds timeout duration in milliseconds + * @return a consumer configuring {@link TimeoutBuilder} + */ + public static Consumer timeoutMillis(int milliseconds) { + return t -> t.duration(d -> d.milliseconds(milliseconds)); + } + + // ---- Schedules ----// + + /** + * Shortcut to configure a schedule using an 'every' inline duration. + * + *

Example: {@code schedule(every(timeoutMinutes(15)))} + * + * @param duration consumer configuring the timeout (e.g., from {@link #timeoutMinutes(int)}) + * @return a consumer configuring {@link ScheduleBuilder} + */ + public static Consumer every(Consumer duration) { + return s -> s.every(duration); + } + + /** + * Shortcut to configure a schedule using an 'every' duration expression. + * + *

Example: {@code schedule(every("PT15M"))} + * + * @param durationExpression duration string expression + * @return a consumer configuring {@link ScheduleBuilder} + */ + public static Consumer every(String durationExpression) { + return s -> s.every(durationExpression); + } + + /** + * Shortcut to configure a schedule using a CRON expression. + * + *

Example: {@code schedule(cron("0 0 * * *"))} + * + * @param cron cron string expression + * @return a consumer configuring {@link ScheduleBuilder} + */ + public static Consumer cron(String cron) { + return s -> s.cron(cron); + } + + /** + * Shortcut to configure a schedule using an 'after' inline duration. + * + *

Example: {@code schedule(after(timeoutHours(1)))} + * + * @param duration consumer configuring the timeout (e.g., from {@link #timeoutHours(int)}) + * @return a consumer configuring {@link ScheduleBuilder} + */ + public static Consumer after(Consumer duration) { + return s -> s.after(duration); + } + + /** + * Shortcut to configure a schedule using an 'after' duration expression. + * + *

Example: {@code schedule(after("PT1H"))} + * + * @param durationExpression duration string expression + * @return a consumer configuring {@link ScheduleBuilder} + */ + public static Consumer after(String durationExpression) { + return s -> s.after(durationExpression); + } + + /** + * Shortcut to configure a schedule using an 'on' event consumption strategy. + * + *

Example: {@code schedule(on(one("my.event")))} + * + * @param strategy consumer configuring the event strategy + * @return a consumer configuring {@link ScheduleBuilder} + */ + public static Consumer on( + Consumer> strategy) { + return s -> s.on(strategy); + } + + /** + * Creates a 'one' event consumption strategy using a specific event type string. + * + * @param eventType the event type + * @return a consumer configuring the event strategy builder + */ + public static Consumer> one(String eventType) { + return one(event().type(eventType)); + } + + /** + * Creates a 'one' event consumption strategy. + * + * @param filter consumer configuring the event filter + * @return a consumer configuring the event strategy builder + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Consumer> one( + Consumer filter) { + return b -> ((AbstractEventConsumptionStrategyBuilder) b).one(filter); + } + + /** + * Creates an 'all' event consumption strategy. + * + * @param filters consumers configuring the event filters + * @return a consumer configuring the event strategy builder + */ + @SafeVarargs + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Consumer> all( + Consumer... filters) { + return b -> ((AbstractEventConsumptionStrategyBuilder) b).all(filters); + } + + /** + * Creates an 'any' event consumption strategy. + * + * @param filters consumers configuring the event filters + * @return a consumer configuring the event strategy builder + */ + @SafeVarargs + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Consumer> any( + Consumer... filters) { + return b -> ((AbstractEventConsumptionStrategyBuilder) b).any(filters); + } } diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/ScheduleBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/ScheduleBuilderTest.java new file mode 100644 index 000000000..1c84608d2 --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/ScheduleBuilderTest.java @@ -0,0 +1,86 @@ +/* + * 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 static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutMinutes; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutSeconds; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.serverlessworkflow.api.types.Schedule; +import org.junit.jupiter.api.Test; + +public class ScheduleBuilderTest { + + @Test + public void when_every_is_set_with_expression() { + Schedule schedule = new ScheduleBuilder().every("PT15M").build(); + + assertThat(schedule.getEvery()).isNotNull(); + assertThat(schedule.getEvery().getDurationLiteral()).isEqualTo("PT15M"); + assertThat(schedule.getEvery().getDurationInline()).isNull(); + } + + @Test + public void when_every_is_set_with_inline_shortcut() { + Schedule schedule = new ScheduleBuilder().every(timeoutMinutes(15)).build(); + + assertThat(schedule.getEvery()).isNotNull(); + assertThat(schedule.getEvery().getDurationExpression()).isNull(); + assertThat(schedule.getEvery().getDurationInline().getMinutes()).isEqualTo(15); + } + + @Test + public void when_cron_is_set() { + Schedule schedule = new ScheduleBuilder().cron("0 0 * * *").build(); + + assertThat(schedule.getCron()).isEqualTo("0 0 * * *"); + assertThat(schedule.getEvery()).isNull(); + } + + @Test + public void when_after_is_set_with_inline_shortcut() { + Schedule schedule = new ScheduleBuilder().after(timeoutSeconds(30)).build(); + + assertThat(schedule.getAfter()).isNotNull(); + assertThat(schedule.getAfter().getDurationInline().getSeconds()).isEqualTo(30); + } + + @Test + public void when_multiple_properties_set_throws_exception() { + assertThatThrownBy(() -> new ScheduleBuilder().every("PT1M").cron("0 * * * *")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Schedule already configured with 'every'. Cannot set 'cron'."); + + assertThatThrownBy(() -> new ScheduleBuilder().cron("0 * * * *").after("PT1M")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Schedule already configured with 'cron'. Cannot set 'after'."); + } + + @Test + public void when_no_property_is_set_throws_exception() { + assertThatThrownBy(() -> new ScheduleBuilder().build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("A schedule must have exactly one property set"); + } + + @Test + public void when_inline_and_expression_mix_throws_exception() { + assertThatThrownBy(() -> new ScheduleBuilder().every("PT15M").every(timeoutMinutes(15))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Duration expression already set for 'every'"); + } +} diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLScheduleTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLScheduleTest.java new file mode 100644 index 000000000..219dedb6c --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLScheduleTest.java @@ -0,0 +1,106 @@ +/* + * 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 static io.serverlessworkflow.fluent.spec.dsl.DSL.after; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.cron; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.event; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.every; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.on; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.one; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutHours; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutMinutes; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +public class DSLScheduleTest { + + @Test + public void when_workflow_schedule_every_expression() { + Workflow wf = + WorkflowBuilder.workflow("scheduledFlow", "myNs", "1.0.0").schedule(every("PT15M")).build(); + + assertThat(wf.getSchedule()).isNotNull(); + assertThat(wf.getSchedule().getEvery()).isNotNull(); + assertThat(wf.getSchedule().getEvery().getDurationLiteral()).isEqualTo("PT15M"); + assertThat(wf.getSchedule().getEvery().getDurationInline()).isNull(); + } + + @Test + public void when_workflow_schedule_every_inline_shortcut() { + Workflow wf = + WorkflowBuilder.workflow("scheduledFlow", "myNs", "1.0.0") + .schedule(every(timeoutMinutes(15))) + .build(); + + assertThat(wf.getSchedule()).isNotNull(); + assertThat(wf.getSchedule().getEvery()).isNotNull(); + assertThat(wf.getSchedule().getEvery().getDurationExpression()).isNull(); + assertThat(wf.getSchedule().getEvery().getDurationInline().getMinutes()).isEqualTo(15); + } + + @Test + public void when_workflow_schedule_cron() { + Workflow wf = + WorkflowBuilder.workflow("scheduledFlow", "myNs", "1.0.0") + .schedule(cron("0 0 * * *")) + .build(); + + assertThat(wf.getSchedule()).isNotNull(); + assertThat(wf.getSchedule().getCron()).isEqualTo("0 0 * * *"); + assertThat(wf.getSchedule().getEvery()).isNull(); + } + + @Test + public void when_workflow_schedule_after_expression() { + Workflow wf = + WorkflowBuilder.workflow("scheduledFlow", "myNs", "1.0.0").schedule(after("PT1H")).build(); + + assertThat(wf.getSchedule()).isNotNull(); + assertThat(wf.getSchedule().getAfter()).isNotNull(); + assertThat(wf.getSchedule().getAfter().getDurationLiteral()).isEqualTo("PT1H"); + } + + @Test + public void when_workflow_schedule_after_inline_shortcut() { + Workflow wf = + WorkflowBuilder.workflow("scheduledFlow", "myNs", "1.0.0") + .schedule(after(timeoutHours(1))) + .build(); + + assertThat(wf.getSchedule()).isNotNull(); + assertThat(wf.getSchedule().getAfter()).isNotNull(); + assertThat(wf.getSchedule().getAfter().getDurationInline().getHours()).isEqualTo(1); + } + + @Test + public void when_workflow_schedule_on_event() { + Workflow wf = + WorkflowBuilder.workflow("scheduledFlow", "myNs", "1.0.0") + .schedule(on(one(event().type("org.acme.startup")))) + .build(); + + assertThat(wf.getSchedule()).isNotNull(); + assertThat(wf.getSchedule().getOn()).isNotNull(); + assertThat(wf.getSchedule().getOn().getOneEventConsumptionStrategy()).isNotNull(); + assertThat( + wf.getSchedule().getOn().getOneEventConsumptionStrategy().getOne().getWith().getType()) + .isEqualTo("org.acme.startup"); + } +} diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTimeoutTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTimeoutTest.java new file mode 100644 index 000000000..cd93a2c2f --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTimeoutTest.java @@ -0,0 +1,143 @@ +/* + * 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 static io.serverlessworkflow.fluent.spec.dsl.DSL.call; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutMinutes; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutSeconds; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.serverlessworkflow.api.types.DurationInline; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import io.serverlessworkflow.fluent.spec.configurers.CallHttpConfigurer; +import org.junit.jupiter.api.Test; + +public class DSLTimeoutTest { + + @Test + public void when_workflow_timeout_with_expression() { + Workflow wf = WorkflowBuilder.workflow("timeoutFlow", "myNs", "1.0.0").timeout("PT15M").build(); + + assertThat(wf.getTimeout()).isNotNull(); + + var after = wf.getTimeout().getTimeoutDefinition().getAfter(); + assertThat(after).isNotNull(); + assertThat(after.getDurationExpression()).isEqualTo("PT15M"); + assertThat(after.getDurationInline()).isNull(); + } + + @Test + public void when_workflow_timeout_with_inline_shortcuts() { + Workflow wf = + WorkflowBuilder.workflow("timeoutFlow", "myNs", "1.0.0") + .timeout(timeoutMinutes(15)) + .build(); + + var after = wf.getTimeout().getTimeoutDefinition().getAfter(); + assertThat(after).isNotNull(); + assertThat(after.getDurationExpression()).isNull(); + + DurationInline inline = after.getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getMinutes()).isEqualTo(15); + } + + @Test + public void when_task_timeout_with_inline_shortcuts() { + Workflow wf = + WorkflowBuilder.workflow("taskTimeoutFlow", "myNs", "1.0.0") + .tasks( + call( + (CallHttpConfigurer) + c -> c.endpoint("https://api.example.com").timeout(timeoutSeconds(30)))) + .build(); + + // Assuming task timeout is accessible directly on the TaskBase properties + var taskTimeout = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getTimeout(); + assertThat(taskTimeout).isNotNull(); + + var after = taskTimeout.getTaskTimeoutDefinition().getAfter(); + assertThat(after).isNotNull(); + + DurationInline inline = after.getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getSeconds()).isEqualTo(30); + } + + @Test + public void when_task_timeout_with_composite_inline_duration() { + Workflow wf = + WorkflowBuilder.workflow("compositeTimeoutFlow", "myNs", "1.0.0") + .tasks( + call( + (CallHttpConfigurer) + c -> + c.endpoint("https://api.example.com") + .timeout(t -> t.duration(d -> d.minutes(5).seconds(30))))) + .build(); + + var after = + wf.getDo() + .get(0) + .getTask() + .getCallTask() + .getCallHTTP() + .getTimeout() + .getTaskTimeoutDefinition() + .getAfter(); + + DurationInline inline = after.getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getMinutes()).isEqualTo(5); + assertThat(inline.getSeconds()).isEqualTo(30); + } + + @Test + public void when_all_timeout_shortcuts_are_applied() { + // Just verifying the shortcuts correctly map to the respective inline properties + Workflow wf = + WorkflowBuilder.workflow("flow", "ns", "1") + .timeout( + t -> t.duration(d -> d.days(1).hours(2).minutes(3).seconds(4).milliseconds(500))) + .build(); + + DurationInline inline = wf.getTimeout().getTimeoutDefinition().getAfter().getDurationInline(); + assertThat(inline.getDays()).isEqualTo(1); + assertThat(inline.getHours()).isEqualTo(2); + assertThat(inline.getMinutes()).isEqualTo(3); + assertThat(inline.getSeconds()).isEqualTo(4); + assertThat(inline.getMilliseconds()).isEqualTo(500); + } + + @Test + public void when_mixing_expression_and_inline_throws_exception() { + assertThatThrownBy( + () -> + WorkflowBuilder.workflow("failFlow", "myNs", "1.0.0") + .timeout(t -> t.duration("PT15M").duration(d -> d.minutes(15)))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Duration expression already set"); + + assertThatThrownBy( + () -> + WorkflowBuilder.workflow("failFlow", "myNs", "1.0.0") + .timeout(t -> t.duration(d -> d.minutes(15)).duration("PT15M"))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Duration already specified"); + } +} diff --git a/impl/persistence/pom.xml b/impl/persistence/pom.xml index 85cdea06b..1dbe8e3ba 100644 --- a/impl/persistence/pom.xml +++ b/impl/persistence/pom.xml @@ -6,7 +6,7 @@ 8.0.0-SNAPSHOT serverlessworkflow-persistence - Serverless Workflow :: Implementation:: Persistence + Serverless Workflow :: Impl :: Persistence pom mvstore From 39528d78e6be5c561b09cca6b7c1943ba88ae85a Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 26 Mar 2026 14:56:46 -0400 Subject: [PATCH 2/2] removing unused code Signed-off-by: Ricardo Zanini --- .../fluent/spec/dsl/BaseListenSpec.java | 12 ------------ 1 file changed, 12 deletions(-) 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 index 8d326ef7c..d32efe396 100644 --- 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 @@ -89,18 +89,6 @@ public final SELF one(Consumer filter) { return self(); } - /** - * Applies the configured event consumption strategy to a raw builder. This is useful when the - * strategy needs to be used outside of a listen task, such as in a workflow schedule. - */ - public final void acceptStrategyInto(LISTEN_TO toBuilder) { - Objects.requireNonNull(strategyStep, "listening strategy must be set (all/any/one)"); - strategyStep.accept(toBuilder); - if (untilStep != null) { - untilStep.accept(toBuilder); - } - } - protected final void acceptInto(LISTEN_TASK listenTaskBuilder) { Objects.requireNonNull(strategyStep, "listening strategy must be set (all/any/one)"); toInvoker.to(