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 6ca79b0d..d5d14ef8 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 94670629..d1261d43 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 00000000..9c2f98c4
--- /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 bb16ad6e..0d305d8a 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 00000000..8f922c38
--- /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 e7c41695..739fe1e7 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 00000000..7af72ea9
--- /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/DSL.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java
index 50bc4021..f364e9ed 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 00000000..1c84608d
--- /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 00000000..219dedb6
--- /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 00000000..cd93a2c2
--- /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 85cdea06..1dbe8e3b 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