From 718a8a5d12a6abc786b59e93ac6139f1177a8fd6 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Mon, 8 Sep 2025 20:00:41 -0400 Subject: [PATCH] Fix #730 - Introduce static imports for the spec DSL Signed-off-by: Ricardo Zanini --- fluent/spec/pom.xml | 5 + ...stractEventConsumptionStrategyBuilder.java | 17 +- .../fluent/spec/CallHTTPTaskBuilder.java | 49 +++- .../fluent/spec/SetTaskBuilder.java | 2 +- .../fluent/spec/TryTaskBuilder.java | 44 +-- .../fluent/spec/WorkflowBuilderConsumers.java | 37 --- .../configurers/AuthenticationConfigurer.java | 22 ++ .../spec/configurers/CallHTTPConfigurer.java | 22 ++ .../spec/configurers/EmitConfigurer.java | 22 ++ .../spec/configurers/EventConfigurer.java | 22 ++ .../spec/configurers/ForEachConfigurer.java | 23 ++ .../spec/configurers/ListenConfigurer.java | 22 ++ .../spec/configurers/RaiseConfigurer.java | 22 ++ .../spec/configurers/RetryConfigurer.java | 21 ++ .../spec/configurers/SetConfigurer.java | 22 ++ .../spec/configurers/SwitchConfigurer.java | 22 ++ .../spec/configurers/TasksConfigurer.java | 22 ++ .../spec/configurers/TryCatchConfigurer.java | 23 ++ .../spec/configurers/TryConfigurer.java | 22 ++ .../fluent/spec/dsl/CallHTTPSpec.java | 103 +++++++ .../fluent/spec/dsl/DSL.java | 261 +++++++++++++++++ .../fluent/spec/dsl/EmitSpec.java | 38 +++ .../fluent/spec/dsl/EventFilterSpec.java | 77 +++++ .../fluent/spec/dsl/EventSpec.java | 32 +++ .../fluent/spec/dsl/ListenSpec.java | 71 +++++ .../fluent/spec/dsl/RaiseSpec.java | 66 +++++ .../fluent/spec/dsl/RetrySpec.java | 77 +++++ .../fluent/spec/dsl/SwitchSpec.java | 82 ++++++ .../fluent/spec/dsl/TryCatchSpec.java | 82 ++++++ .../fluent/spec/dsl/TrySpec.java | 41 +++ .../spi/EventConsumptionStrategyFluent.java | 4 +- .../fluent/spec/WorkflowBuilderTest.java | 56 ++-- .../fluent/spec/dsl/CallHttpAuthDslTest.java | 265 ++++++++++++++++++ .../fluent/spec/dsl/DSLTest.java | 263 +++++++++++++++++ .../fluent/spec/dsl/TryCatchDslTest.java | 212 ++++++++++++++ impl/core/pom.xml | 13 +- .../impl/WorkflowError.java | 21 +- .../impl/WorkflowException.java | 8 +- .../impl/executors/TryExecutor.java | 2 +- .../lifecycle/ce/WorkflowErrorCEData.java | 2 +- .../impl/test/WorkflowDefinitionTest.java | 10 +- .../io/serverlessworkflow/types/Errors.java | 96 +++++++ 42 files changed, 2207 insertions(+), 116 deletions(-) delete mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderConsumers.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/AuthenticationConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/CallHTTPConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EmitConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EventConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ForEachConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ListenConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RaiseConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RetryConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SetConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SwitchConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TasksConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryCatchConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryConfigurer.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/CallHTTPSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RaiseSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/SwitchSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TrySpec.java create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java create mode 100644 types/src/main/java/io/serverlessworkflow/types/Errors.java diff --git a/fluent/spec/pom.xml b/fluent/spec/pom.xml index 5ec62a98..45051baf 100644 --- a/fluent/spec/pom.xml +++ b/fluent/spec/pom.xml @@ -27,6 +27,11 @@ junit-jupiter-api test + + org.assertj + assertj-core + test + \ No newline at end of file diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java index 4c12d199..2857cd7e 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java @@ -58,13 +58,22 @@ public SELF one(Consumer c) { protected abstract void setOne(OneEventConsumptionStrategy strategy); - public SELF all(Consumer c) { + @SafeVarargs + public final SELF all(Consumer... consumers) { ensureNoneSet(); allSet = true; - F fb = this.newEventFilterBuilder(); - c.accept(fb); + + List built = new ArrayList<>(consumers.length); + + for (Consumer c : consumers) { + Objects.requireNonNull(c, "consumer"); + F fb = this.newEventFilterBuilder(); + c.accept(fb); + built.add(fb.build()); + } + AllEventConsumptionStrategy strat = new AllEventConsumptionStrategy(); - strat.setAll(List.of(fb.build())); + strat.setAll(built); this.setAll(strat); return this.self(); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallHTTPTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallHTTPTaskBuilder.java index 95819162..21b84fe9 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallHTTPTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallHTTPTaskBuilder.java @@ -17,11 +17,13 @@ import io.serverlessworkflow.api.types.CallHTTP; import io.serverlessworkflow.api.types.Endpoint; +import io.serverlessworkflow.api.types.EndpointConfiguration; import io.serverlessworkflow.api.types.HTTPArguments; import io.serverlessworkflow.api.types.HTTPHeaders; import io.serverlessworkflow.api.types.HTTPQuery; import io.serverlessworkflow.api.types.Headers; import io.serverlessworkflow.api.types.Query; +import io.serverlessworkflow.api.types.ReferenceableAuthenticationPolicy; import io.serverlessworkflow.api.types.UriTemplate; import java.net.URI; import java.util.Map; @@ -55,12 +57,44 @@ public CallHTTPTaskBuilder endpoint(URI endpoint) { return this; } + public CallHTTPTaskBuilder endpoint( + URI endpoint, Consumer auth) { + final AuthenticationPolicyUnionBuilder policy = new AuthenticationPolicyUnionBuilder(); + auth.accept(policy); + this.callHTTP + .getWith() + .setEndpoint( + new Endpoint() + .withEndpointConfiguration( + new EndpointConfiguration() + .withAuthentication( + new ReferenceableAuthenticationPolicy() + .withAuthenticationPolicy(policy.build()))) + .withUriTemplate(new UriTemplate().withLiteralUri(endpoint))); + return this; + } + public CallHTTPTaskBuilder endpoint(String expr) { this.callHTTP.getWith().setEndpoint(new Endpoint().withRuntimeExpression(expr)); return this; } - // TODO: add endpoint configuration to support authentication + public CallHTTPTaskBuilder endpoint( + String expr, Consumer auth) { + final AuthenticationPolicyUnionBuilder policy = new AuthenticationPolicyUnionBuilder(); + auth.accept(policy); + this.callHTTP + .getWith() + .setEndpoint( + new Endpoint() + .withEndpointConfiguration( + new EndpointConfiguration() + .withAuthentication( + new ReferenceableAuthenticationPolicy() + .withAuthenticationPolicy(policy.build()))) + .withRuntimeExpression(expr)); + return this; + } public CallHTTPTaskBuilder headers(String expr) { this.callHTTP.getWith().setHeaders(new Headers().withRuntimeExpression(expr)); @@ -70,7 +104,18 @@ public CallHTTPTaskBuilder headers(String expr) { public CallHTTPTaskBuilder headers(Consumer consumer) { HTTPHeadersBuilder hb = new HTTPHeadersBuilder(); consumer.accept(hb); - callHTTP.getWith().setHeaders(hb.build()); + if (callHTTP.getWith().getHeaders() != null + && callHTTP.getWith().getHeaders().getHTTPHeaders() != null) { + Headers h = callHTTP.getWith().getHeaders(); + Headers built = hb.build(); + built + .getHTTPHeaders() + .getAdditionalProperties() + .forEach((k, v) -> h.getHTTPHeaders().setAdditionalProperty(k, v)); + } else { + callHTTP.getWith().setHeaders(hb.build()); + } + return this; } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java index ced59032..ede8f202 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SetTaskBuilder.java @@ -40,7 +40,7 @@ public SetTaskBuilder expr(String expression) { return this; } - public SetTaskBuilder put(String key, String value) { + public SetTaskBuilder put(String key, Object value) { setTaskConfiguration.withAdditionalProperty(key, value); return this; } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TryTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TryTaskBuilder.java index a707e916..f8ad386a 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TryTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TryTaskBuilder.java @@ -57,8 +57,9 @@ public TryTaskBuilder tryHandler(Consumer consumer) { return this; } - public TryTaskBuilder catchHandler(Consumer consumer) { - final TryTaskCatchBuilder catchBuilder = new TryTaskCatchBuilder(); + public TryTaskBuilder catchHandler(Consumer> consumer) { + final TryTaskCatchBuilder catchBuilder = + new TryTaskCatchBuilder<>(this.doTaskBuilderFactory); consumer.accept(catchBuilder); this.tryTask.setCatch(catchBuilder.build()); return this; @@ -68,42 +69,51 @@ public TryTask build() { return tryTask; } - public static final class TryTaskCatchBuilder { + public static final class TryTaskCatchBuilder> { private final TryTaskCatch tryTaskCatch; + private final T doTaskBuilderFactory; - TryTaskCatchBuilder() { + TryTaskCatchBuilder(T doTaskBuilderFactory) { + this.doTaskBuilderFactory = doTaskBuilderFactory; this.tryTaskCatch = new TryTaskCatch(); } - public TryTaskCatchBuilder as(final String as) { + public TryTaskCatchBuilder as(final String as) { this.tryTaskCatch.setAs(as); return this; } - public TryTaskCatchBuilder when(final String when) { + public TryTaskCatchBuilder when(final String when) { this.tryTaskCatch.setWhen(when); return this; } - public TryTaskCatchBuilder exceptWhen(final String exceptWhen) { + public TryTaskCatchBuilder exceptWhen(final String exceptWhen) { this.tryTaskCatch.setExceptWhen(exceptWhen); return this; } - public TryTaskCatchBuilder retry(Consumer consumer) { + public TryTaskCatchBuilder retry(Consumer consumer) { final RetryPolicyBuilder retryPolicyBuilder = new RetryPolicyBuilder(); consumer.accept(retryPolicyBuilder); this.tryTaskCatch.setRetry(new Retry().withRetryPolicyDefinition(retryPolicyBuilder.build())); return this; } - public TryTaskCatchBuilder errorsWith(Consumer consumer) { + public TryTaskCatchBuilder errorsWith(Consumer consumer) { final CatchErrorsBuilder catchErrorsBuilder = new CatchErrorsBuilder(); consumer.accept(catchErrorsBuilder); this.tryTaskCatch.setErrors(catchErrorsBuilder.build()); return this; } + public TryTaskCatchBuilder doTasks(Consumer consumer) { + final T taskItemListBuilder = this.doTaskBuilderFactory.newItemListBuilder(); + consumer.accept(taskItemListBuilder); + this.tryTaskCatch.setDo(taskItemListBuilder.build()); + return this; + } + public TryTaskCatch build() { return tryTaskCatch; } @@ -153,30 +163,30 @@ public static final class RetryPolicyJitterBuilder { this.retryPolicyJitter = new RetryPolicyJitter(); } - public RetryPolicyJitter to(Consumer consumer) { + public RetryPolicyJitterBuilder to(Consumer consumer) { final DurationInlineBuilder durationInlineBuilder = new DurationInlineBuilder(); consumer.accept(durationInlineBuilder); this.retryPolicyJitter.setTo( new TimeoutAfter().withDurationInline(durationInlineBuilder.build())); - return retryPolicyJitter; + return this; } - public RetryPolicyJitter to(String expression) { + public RetryPolicyJitterBuilder to(String expression) { this.retryPolicyJitter.setTo(new TimeoutAfter().withDurationExpression(expression)); - return retryPolicyJitter; + return this; } - public RetryPolicyJitter from(Consumer consumer) { + public RetryPolicyJitterBuilder from(Consumer consumer) { final DurationInlineBuilder durationInlineBuilder = new DurationInlineBuilder(); consumer.accept(durationInlineBuilder); this.retryPolicyJitter.setFrom( new TimeoutAfter().withDurationInline(durationInlineBuilder.build())); - return retryPolicyJitter; + return this; } - public RetryPolicyJitter from(String expression) { + public RetryPolicyJitterBuilder from(String expression) { this.retryPolicyJitter.setFrom(new TimeoutAfter().withDurationExpression(expression)); - return retryPolicyJitter; + return this; } public RetryPolicyJitter build() { diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderConsumers.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderConsumers.java deleted file mode 100644 index e0dbb54c..00000000 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderConsumers.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.spec; - -import java.util.function.Consumer; - -public final class WorkflowBuilderConsumers { - - private WorkflowBuilderConsumers() {} - - public static Consumer authBasic( - final String username, final String password) { - return auth -> auth.basic(b -> b.username(username).password(password)); - } - - public static Consumer authBearer(final String token) { - return auth -> auth.bearer(b -> b.token(token)); - } - - public static Consumer authDigest( - final String username, final String password) { - return auth -> auth.digest(d -> d.username(username).password(password)); - } -} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/AuthenticationConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/AuthenticationConfigurer.java new file mode 100644 index 00000000..97b9bd4b --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/AuthenticationConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.AuthenticationPolicyUnionBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface AuthenticationConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/CallHTTPConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/CallHTTPConfigurer.java new file mode 100644 index 00000000..11799b47 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/CallHTTPConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.CallHTTPTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface CallHTTPConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EmitConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EmitConfigurer.java new file mode 100644 index 00000000..4a969387 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EmitConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface EmitConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EventConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EventConfigurer.java new file mode 100644 index 00000000..3cb365f9 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/EventConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.EventPropertiesBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface EventConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ForEachConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ForEachConfigurer.java new file mode 100644 index 00000000..bec8ba3c --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ForEachConfigurer.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.ForEachTaskBuilder; +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface ForEachConfigurer extends Consumer> {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ListenConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ListenConfigurer.java new file mode 100644 index 00000000..8cefae7f --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/ListenConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface ListenConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RaiseConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RaiseConfigurer.java new file mode 100644 index 00000000..5c9897d7 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RaiseConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.RaiseTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface RaiseConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RetryConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RetryConfigurer.java new file mode 100644 index 00000000..2de6920e --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/RetryConfigurer.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import java.util.function.Consumer; + +public interface RetryConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SetConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SetConfigurer.java new file mode 100644 index 00000000..e6a939f2 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SetConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.SetTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface SetConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SwitchConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SwitchConfigurer.java new file mode 100644 index 00000000..54778888 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/SwitchConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.SwitchTaskBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface SwitchConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TasksConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TasksConfigurer.java new file mode 100644 index 00000000..276019fc --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TasksConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import java.util.function.Consumer; + +@FunctionalInterface +public interface TasksConfigurer extends Consumer {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryCatchConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryCatchConfigurer.java new file mode 100644 index 00000000..e256cc30 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryCatchConfigurer.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import java.util.function.Consumer; + +public interface TryCatchConfigurer + extends Consumer> {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryConfigurer.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryConfigurer.java new file mode 100644 index 00000000..59b5f289 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/configurers/TryConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.configurers; + +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import java.util.function.Consumer; + +public interface TryConfigurer extends Consumer> {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/CallHTTPSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/CallHTTPSpec.java new file mode 100644 index 00000000..83bec629 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/CallHTTPSpec.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.CallHTTPTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.AuthenticationConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.CallHTTPConfigurer; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class CallHTTPSpec implements CallHTTPConfigurer { + + private final List steps = new ArrayList<>(); + + public CallHTTPSpec GET() { + steps.add(c -> c.method("GET")); + return this; + } + + public CallHTTPSpec POST() { + steps.add(c -> c.method("POST")); + return this; + } + + public CallHTTPSpec acceptJSON() { + return header("Accept", "application/json"); + } + + public CallHTTPSpec endpoint(String urlExpr) { + steps.add(b -> b.endpoint(urlExpr)); + return this; + } + + public CallHTTPSpec endpoint(String urlExpr, AuthenticationConfigurer auth) { + steps.add(b -> b.endpoint(urlExpr, auth)); + return this; + } + + public CallHTTPSpec uri(String url) { + steps.add(b -> b.endpoint(URI.create(url))); + return this; + } + + public CallHTTPSpec uri(String url, AuthenticationConfigurer auth) { + steps.add(b -> b.endpoint(URI.create(url), auth)); + return this; + } + + public CallHTTPSpec uri(URI uri) { + steps.add(b -> b.endpoint(uri)); + return this; + } + + public CallHTTPSpec uri(URI uri, AuthenticationConfigurer auth) { + steps.add(b -> b.endpoint(uri, auth)); + return this; + } + + public CallHTTPSpec body(String bodyExpr) { + steps.add(c -> c.body(bodyExpr)); + return this; + } + + public CallHTTPSpec body(Map body) { + steps.add(c -> c.body(body)); + return this; + } + + public CallHTTPSpec method(String method) { + steps.add(b -> b.method(method)); + return this; + } + + public CallHTTPSpec header(String name, String value) { + steps.add(c -> c.headers(h -> h.header(name, value))); + return this; + } + + public CallHTTPSpec headers(Map headers) { + steps.add(b -> b.headers(headers)); + return this; + } + + @Override + public void accept(CallHTTPTaskBuilder b) { + for (var s : steps) s.accept(b); + } +} 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 new file mode 100644 index 00000000..8da5bdbe --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java @@ -0,0 +1,261 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; +import io.serverlessworkflow.fluent.spec.ForkTaskBuilder; +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.AuthenticationConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.CallHTTPConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.ForEachConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.ListenConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.RaiseConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.RetryConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.SetConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.SwitchConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.TasksConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.TryCatchConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.TryConfigurer; +import io.serverlessworkflow.types.Errors; +import java.net.URI; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public final class DSL { + + private DSL() {} + + // ---- Convenient shortcuts ----// + + public static CallHTTPSpec http() { + return new CallHTTPSpec(); + } + + public static SwitchSpec cases() { + return new SwitchSpec(); + } + + public static ListenSpec to() { + return new ListenSpec(); + } + + public static EventSpec event() { + return new EventSpec(); + } + + public static TrySpec tryCatch() { + return new TrySpec(); + } + + public static TryCatchConfigurer catchWhen( + String when, RetryConfigurer retry, TasksConfigurer... doTasks) { + return c -> c.when(when).retry(retry).doTasks(tasks(doTasks)); + } + + public static TryCatchConfigurer catchExceptWhen( + String when, RetryConfigurer retry, TasksConfigurer... doTasks) { + return c -> c.exceptWhen(when).retry(retry).doTasks(tasks(doTasks)); + } + + public static RetryConfigurer retryWhen(String when, String limitDuration) { + return r -> r.when(when).limit(l -> l.duration(limitDuration)); + } + + public static RetryConfigurer retryExceptWhen(String when, String limitDuration) { + return r -> r.exceptWhen(when).limit(l -> l.duration(limitDuration)); + } + + public static Consumer errorFilter(URI errType, int status) { + return e -> e.type(errType.toString()).status(status); + } + + public static Consumer errorFilter( + Errors.Standard errType, int status) { + return e -> e.type(errType.toString()).status(status); + } + + public static AuthenticationConfigurer basic(String username, String password) { + return a -> a.basic(b -> b.username(username).password(password)); + } + + public static AuthenticationConfigurer bearer(String token) { + return a -> a.bearer(b -> b.token(token)); + } + + public static AuthenticationConfigurer digest(String username, String password) { + return a -> a.digest(d -> d.username(username).password(password)); + } + + public static AuthenticationConfigurer oidc( + String authority, OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant) { + return a -> a.openIDConnect(o -> o.authority(authority).grant(grant)); + } + + public static AuthenticationConfigurer oidc( + String authority, + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant, + String clientId, + String clientSecret) { + return a -> + a.openIDConnect( + o -> + o.authority(authority) + .grant(grant) + .client(c -> c.id(clientId).secret(clientSecret))); + } + + // TODO: we may create an OIDCSpec for chained builders if necessary + + public static AuthenticationConfigurer oauth2( + String authority, OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant) { + return a -> a.openIDConnect(o -> o.authority(authority).grant(grant)); + } + + public static AuthenticationConfigurer oauth2( + String authority, + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant, + String clientId, + String clientSecret) { + return a -> + a.openIDConnect( + o -> + o.authority(authority) + .grant(grant) + .client(c -> c.id(clientId).secret(clientSecret))); + } + + public static RaiseSpec error(String errExpr, int status) { + return new RaiseSpec().type(errExpr).status(status); + } + + public static RaiseSpec error(String errExpr) { + return new RaiseSpec().type(errExpr); + } + + public static RaiseSpec error(URI errType) { + return new RaiseSpec().type(errType); + } + + public static RaiseSpec error(URI errType, int status) { + return new RaiseSpec().type(errType).status(status); + } + + // --- Errors Recipes --- // + public static RaiseSpec error(Errors.Standard std) { + return error(std.uri(), std.status()); + } + + public static RaiseSpec error(Errors.Standard std, int status) { + return error(std.uri(), status); + } + + public static RaiseSpec serverError() { + return error(Errors.RUNTIME); + } + + public static RaiseSpec communicationError() { + return error(Errors.COMMUNICATION); + } + + public static RaiseSpec notImplementedError() { + return error(Errors.NOT_IMPLEMENTED); + } + + public static RaiseSpec unauthorizedError() { + return error(Errors.AUTHENTICATION); + } + + public static RaiseSpec forbiddenError() { + return error(Errors.AUTHORIZATION); + } + + public static RaiseSpec timeoutError() { + return error(Errors.TIMEOUT); + } + + public static RaiseSpec dataError() { + return error(Errors.DATA); + } + + // ---- Tasks ----// + + public static TasksConfigurer call(CallHTTPConfigurer configurer) { + return list -> list.callHTTP(configurer); + } + + public static TasksConfigurer set(SetConfigurer configurer) { + return list -> list.set(configurer); + } + + public static TasksConfigurer set(String expr) { + return list -> list.set(expr); + } + + public static TasksConfigurer emit(Consumer configurer) { + return list -> list.emit(configurer); + } + + public static TasksConfigurer listen(ListenConfigurer configurer) { + return list -> list.listen(configurer); + } + + public static TasksConfigurer forEach(ForEachConfigurer configurer) { + return list -> list.forEach(configurer); + } + + public static TasksConfigurer fork(Consumer configurer) { + return list -> list.fork(configurer); + } + + public static TasksConfigurer switchCase(SwitchConfigurer configurer) { + return list -> list.switchCase(configurer); + } + + public static TasksConfigurer raise(RaiseConfigurer configurer) { + return list -> list.raise(configurer); + } + + public static TasksConfigurer tryCatch(TryConfigurer configurer) { + return list -> list.tryCatch(configurer); + } + + // ----- Tasks that requires tasks list --// + public static Consumer tasks(TasksConfigurer... steps) { + Objects.requireNonNull(steps, "Steps in a tasks are required"); + final List snapshot = List.of(steps.clone()); + return list -> snapshot.forEach(s -> s.accept(list)); + } + + public static ForEachConfigurer forEach(TasksConfigurer... steps) { + final Consumer tasks = DSL.tasks(steps); + return f -> f.tasks(tasks); + } + + /** Recipe for {@link io.serverlessworkflow.api.types.ForkTask} branch that DO compete */ + public static Consumer branchesCompete(TasksConfigurer... steps) { + final Consumer tasks = DSL.tasks(steps); + return f -> f.compete(true).branches(tasks); + } + + /** Recipe for {@link io.serverlessworkflow.api.types.ForkTask} branch that DO NOT compete */ + public static Consumer branches(TasksConfigurer... steps) { + final Consumer tasks = DSL.tasks(steps); + return f -> f.compete(false).branches(tasks); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java new file mode 100644 index 00000000..8c4115bc --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EmitSpec.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.EmitConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; + +public final class EmitSpec extends EventFilterSpec implements EmitConfigurer { + + @Override + protected EmitSpec self() { + return this; + } + + @Override + public void accept(EmitTaskBuilder emitTaskBuilder) { + emitTaskBuilder.event( + e -> { + for (EventConfigurer step : steps) { + step.accept(e); + } + }); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java new file mode 100644 index 00000000..7c47a07b --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventFilterSpec.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; +import java.net.URI; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public abstract class EventFilterSpec { + + protected final List steps = new ArrayList<>(); + + protected abstract SELF self(); + + public SELF type(String eventType) { + steps.add(e -> e.type(eventType)); + return self(); + } + + /** Sets the CloudEvent id to a random UUID */ + public SELF randomId() { + steps.add(e -> e.id(UUID.randomUUID().toString())); + return self(); + } + + /** Sets the CloudEvent time to the current system time */ + public SELF now() { + steps.add(e -> e.time(Date.from(Instant.now()))); + return self(); + } + + /** Sets the CloudEvent dataContentType to `application/json` */ + public SELF JSON() { + steps.add(e -> e.dataContentType("application/json")); + return self(); + } + + /** Sets the event data and the contentType to `application/json` */ + public SELF jsonData(String expr) { + steps.add(e -> e.data(expr)); + return JSON(); + } + + /** Sets the event data and the contentType to `application/json` */ + public SELF jsonData(Map data) { + steps.add(e -> e.data(data)); + return JSON(); + } + + public SELF source(String source) { + steps.add(e -> e.source(source)); + return self(); + } + + public SELF source(URI source) { + steps.add(e -> e.source(source)); + return self(); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java new file mode 100644 index 00000000..a7a41a80 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/EventSpec.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.EventPropertiesBuilder; +import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; + +public final class EventSpec extends EventFilterSpec implements EventConfigurer { + + @Override + protected EventSpec self() { + return this; + } + + @Override + public void accept(EventPropertiesBuilder eventPropertiesBuilder) { + steps.forEach(step -> step.accept(eventPropertiesBuilder)); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java new file mode 100644 index 00000000..ca538911 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/ListenSpec.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.EventFilterBuilder; +import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; +import io.serverlessworkflow.fluent.spec.ListenToBuilder; +import io.serverlessworkflow.fluent.spec.configurers.EventConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.ListenConfigurer; +import java.util.Objects; +import java.util.function.Consumer; + +public final class ListenSpec implements ListenConfigurer { + + private Consumer strategyStep; + private Consumer untilStep; + + @SuppressWarnings("unchecked") + private static Consumer[] asFilters(EventConfigurer[] events) { + Consumer[] filters = new Consumer[events.length]; + for (int i = 0; i < events.length; i++) { + EventConfigurer ev = Objects.requireNonNull(events[i], "events[" + i + "]"); + filters[i] = f -> f.with(ev); + } + return filters; + } + + public ListenSpec all(EventConfigurer... events) { + strategyStep = t -> t.all(asFilters(events)); + return this; + } + + public ListenSpec one(EventConfigurer e) { + strategyStep = t -> t.one(f -> f.with(e)); + return this; + } + + public ListenSpec any(EventConfigurer... events) { + strategyStep = t -> t.any(asFilters(events)); + return this; + } + + public ListenSpec until(String expression) { + untilStep = t -> t.until(expression); + return this; + } + + @Override + public void accept(ListenTaskBuilder listenTaskBuilder) { + listenTaskBuilder.to( + t -> { + strategyStep.accept(t); + if (untilStep != null) { + untilStep.accept(t); + } + }); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RaiseSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RaiseSpec.java new file mode 100644 index 00000000..da42bb30 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RaiseSpec.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.RaiseTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.RaiseConfigurer; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public final class RaiseSpec implements RaiseConfigurer { + private final List> steps = new ArrayList<>(); + + public RaiseSpec type(String expr) { + steps.add(e -> e.type(expr)); + return this; + } + + public RaiseSpec type(URI uri) { + steps.add(e -> e.type(uri)); + return this; + } + + public RaiseSpec status(int s) { + steps.add(e -> e.status(s)); + return this; + } + + public RaiseSpec title(String t) { + steps.add(e -> e.title(t)); + return this; + } + + public RaiseSpec detail(String d) { + steps.add(e -> e.detail(d)); + return this; + } + + public RaiseSpec then(Consumer step) { + steps.add(Objects.requireNonNull(step)); + return this; + } + + @Override + public void accept(RaiseTaskBuilder b) { + b.error( + err -> { + for (var s : steps) s.accept(err); + }); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java new file mode 100644 index 00000000..5649c696 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.RetryConfigurer; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +public final class RetrySpec implements RetryConfigurer { + + private final TryCatchSpec tryCatchSpec; + + RetrySpec(final TryCatchSpec tryCatchSpec) { + this.tryCatchSpec = tryCatchSpec; + } + + private final List steps = new LinkedList<>(); + + public RetrySpec when(String when) { + steps.add(t -> t.when(when)); + return this; + } + + public RetrySpec exceptWhen(String when) { + steps.add(t -> t.exceptWhen(when)); + return this; + } + + public RetrySpec limit(String duration) { + steps.add(r -> r.limit(l -> l.duration(duration))); + return this; + } + + public RetrySpec limit(Consumer retry) { + steps.add(r -> r.limit(retry)); + return this; + } + + public RetrySpec backoff(Consumer backoff) { + steps.add(r -> r.backoff(backoff)); + return this; + } + + public RetrySpec jitter(Consumer jitter) { + steps.add(r -> r.jitter(jitter)); + return this; + } + + public RetrySpec jitter(String from, String to) { + steps.add(r -> r.jitter(j -> j.to(to).from(from))); + return this; + } + + public TryCatchSpec done() { + return tryCatchSpec; + } + + @Override + public void accept(TryTaskBuilder.RetryPolicyBuilder retryPolicyBuilder) { + steps.forEach(step -> step.accept(retryPolicyBuilder)); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/SwitchSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/SwitchSpec.java new file mode 100644 index 00000000..2b68fd61 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/SwitchSpec.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.fluent.spec.SwitchTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.SwitchConfigurer; +import io.serverlessworkflow.fluent.spec.spi.SwitchTaskFluent; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +public final class SwitchSpec implements SwitchConfigurer { + + private final Map> cases = + new LinkedHashMap<>(); + private boolean defaultSet = false; + + public SwitchSpec on(String caseKey, String whenExpr, String thenKey) { + Objects.requireNonNull(caseKey); + Objects.requireNonNull(whenExpr); + Objects.requireNonNull(thenKey); + putCase(caseKey, c -> c.when(whenExpr).then(thenKey)); + return this; + } + + public SwitchSpec on(String caseKey, String whenExpr, FlowDirectiveEnum thenKey) { + Objects.requireNonNull(caseKey); + Objects.requireNonNull(whenExpr); + Objects.requireNonNull(thenKey); + putCase(caseKey, c -> c.when(whenExpr).then(thenKey)); + return this; + } + + public SwitchSpec onDefault(String thenKey) { + Objects.requireNonNull(thenKey); + setDefault(c -> c.then(thenKey)); + return this; + } + + public SwitchSpec onDefault(FlowDirectiveEnum thenKey) { + Objects.requireNonNull(thenKey); + setDefault(c -> c.then(thenKey)); + return this; + } + + private void putCase(String key, Consumer cfg) { + if (SwitchTaskFluent.DEFAULT_CASE.equals(key)) { + throw new IllegalArgumentException("Use onDefault(...) for the default case."); + } + if (cases.putIfAbsent(key, cfg) != null) { + throw new IllegalStateException("Duplicate switch case key: " + key); + } + } + + private void setDefault(Consumer cfg) { + if (defaultSet) { + throw new IllegalStateException("Default case already defined."); + } + defaultSet = true; + cases.put(SwitchTaskFluent.DEFAULT_CASE, cfg); + } + + @Override + public void accept(SwitchTaskBuilder switchTaskBuilder) { + cases.forEach(switchTaskBuilder::on); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java new file mode 100644 index 00000000..27733fe9 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.TasksConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.TryCatchConfigurer; +import io.serverlessworkflow.types.Errors; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +public final class TryCatchSpec implements TryCatchConfigurer { + private final List steps = new LinkedList<>(); + private final RetrySpec retry = new RetrySpec(this); + private final TrySpec trySpec; + + TryCatchSpec(final TrySpec trySpec) { + this.trySpec = trySpec; + } + + public TryCatchSpec when(String when) { + steps.add(t -> t.when(when)); + return this; + } + + public TryCatchSpec exceptWhen(String when) { + steps.add(t -> t.exceptWhen(when)); + return this; + } + + public TryCatchSpec tasks(TasksConfigurer... tasks) { + steps.add(t -> t.doTasks(DSL.tasks(tasks))); + return this; + } + + public TryCatchSpec errors(URI errType, int status) { + steps.add(t -> t.errorsWith(e -> e.type(errType.toString()).status(status))); + return this; + } + + public TryCatchSpec errors(Errors.Standard errType, int status) { + steps.add(t -> t.errorsWith(e -> e.type(errType.toString()).status(status))); + return this; + } + + public TryCatchSpec errors(Consumer errors) { + steps.add(t -> t.errorsWith(errors)); + return this; + } + + public RetrySpec retry() { + return retry; + } + + public TrySpec done() { + return trySpec; + } + + @Override + public void accept( + TryTaskBuilder.TryTaskCatchBuilder + taskItemListBuilderTryTaskCatchBuilder) { + taskItemListBuilderTryTaskCatchBuilder.retry(retry); + steps.forEach(step -> step.accept(taskItemListBuilderTryTaskCatchBuilder)); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TrySpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TrySpec.java new file mode 100644 index 00000000..dc032958 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TrySpec.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.spec.dsl; + +import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.TryTaskBuilder; +import io.serverlessworkflow.fluent.spec.configurers.TasksConfigurer; +import io.serverlessworkflow.fluent.spec.configurers.TryConfigurer; + +public final class TrySpec implements TryConfigurer { + private TryConfigurer tryConfigurer; + private final TryCatchSpec tryCatchSpec = new TryCatchSpec(this); + + public TrySpec tasks(TasksConfigurer... tasks) { + tryConfigurer = t -> t.tryHandler(DSL.tasks(tasks)); + return this; + } + + public TryCatchSpec catches() { + return tryCatchSpec; + } + + @Override + public void accept(TryTaskBuilder taskItemListBuilderTryTaskBuilder) { + taskItemListBuilderTryTaskBuilder.catchHandler(tryCatchSpec); + tryConfigurer.accept(taskItemListBuilderTryTaskBuilder); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.java index 5ee72399..91ba41e0 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.java @@ -27,7 +27,9 @@ public interface EventConsumptionStrategyFluent< SELF one(Consumer c); - SELF all(Consumer c); + SELF all(Consumer... c); + + SELF any(Consumer... c); SELF any(Consumer c); diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java index cfaeb260..38c751eb 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java @@ -15,7 +15,10 @@ */ package io.serverlessworkflow.fluent.spec; -import static io.serverlessworkflow.fluent.spec.WorkflowBuilderConsumers.authBasic; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.basic; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.cases; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.event; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.http; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -27,6 +30,7 @@ import io.serverlessworkflow.api.types.Document; import io.serverlessworkflow.api.types.ErrorFilter; import io.serverlessworkflow.api.types.EventFilter; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.HTTPArguments; import io.serverlessworkflow.api.types.HTTPHeaders; import io.serverlessworkflow.api.types.HTTPQuery; @@ -80,10 +84,7 @@ void testWorkflowDocumentExplicit() { void testUseAuthenticationsBasic() { Workflow wf = WorkflowBuilder.workflow("flowAuth") - .use( - u -> - u.authentications( - a -> a.authentication("basicAuth", authBasic("admin", "pass")))) + .use(u -> u.authentications(a -> a.authentication("basicAuth", basic("admin", "pass")))) .build(); Use use = wf.getUse(); @@ -128,11 +129,7 @@ void testDoTaskMultipleTypes() { d -> d.set("init", s -> s.expr("$.init = true")) .forEach("items", f -> f.each("item").in("$.list")) - .switchCase( - "choice", - sw -> { - // no-op configuration - }) + .switchCase("choice", cases().onDefault(FlowDirectiveEnum.CONTINUE)) .raise( "alert", r -> { @@ -217,15 +214,15 @@ void testDoTaskEmitEvent() { "emitEvent", e -> e.event( - p -> - p.source(URI.create("https://petstore.com")) - .type("com.petstore.order.placed.v1") - .data( - Map.of( - "client", + event() + .type("com.petstore.order.placed.v1") + .source(URI.create("https://petstore.com")) + .jsonData( + Map.of( + "client", Map.of( "firstName", "Cruella", "lastName", "de Vil"), - "items", + "items", List.of( Map.of( "breed", "dalmatian", "quantity", 101))))))) @@ -412,10 +409,10 @@ void testDoTaskCallHTTPBasic() { d -> d.callHTTP( "basicCall", - c -> - c.method("POST") - .endpoint(URI.create("http://example.com/api")) - .body(Map.of("foo", "bar")))) + http() + .POST() + .uri(URI.create("http://example.com/api")) + .andThen(b -> b.body(Map.of("foo", "bar"))))) .build(); List items = wf.getDo(); assertEquals(1, items.size(), "Should have one HTTP call task"); @@ -438,10 +435,7 @@ void testDoTaskCallHTTPHeadersConsumerAndMap() { d -> d.callHTTP( "hdrCall", - c -> - c.method("GET") - .endpoint("${uriExpr}") - .headers(h -> h.header("A", "1").header("B", "2")))) + http().GET().endpoint("${uriExpr}").headers(Map.of("A", "1", "B", "2")))) .build(); CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP(); HTTPHeaders hh = call.getWith().getHeaders().getHTTPHeaders(); @@ -452,9 +446,7 @@ void testDoTaskCallHTTPHeadersConsumerAndMap() { WorkflowBuilder.workflow() .tasks( d -> - d.callHTTP( - c -> - c.method("GET").endpoint("expr").headers(Map.of("X", "10", "Y", "20")))) + d.callHTTP(http().GET().endpoint("expr").headers(Map.of("X", "10", "Y", "20")))) .build(); CallHTTP call2 = wf2.getDo().get(0).getTask().getCallTask().getCallHTTP(); HTTPHeaders hh2 = call2.getWith().getHeaders().getHTTPHeaders(); @@ -470,10 +462,10 @@ void testDoTaskCallHTTPQueryConsumerAndMap() { d -> d.callHTTP( "qryCall", - c -> - c.method("GET") - .endpoint("exprUri") - .query(q -> q.query("k1", "v1").query("k2", "v2")))) + http() + .GET() + .endpoint("exprUri") + .andThen(q -> q.query(Map.of("k1", "v1", "k2", "v2"))))) .build(); HTTPQuery hq = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith().getQuery().getHTTPQuery(); diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java new file mode 100644 index 00000000..d9150b1c --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java @@ -0,0 +1,265 @@ +/* + * 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.basic; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.bearer; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.digest; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.http; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.oauth2; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.oidc; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import java.net.URI; +import org.junit.jupiter.api.Test; + +public class CallHttpAuthDslTest { + + private static final String EXPR_ENDPOINT = "${ \"https://api.example.com/v1/resource\" }"; + + @Test + void when_call_http_with_basic_auth_on_endpoint_expr() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks(t -> t.callHTTP(http().GET().endpoint(EXPR_ENDPOINT, basic("alice", "secret")))) + .build(); + + var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + + // Endpoint expression is set + assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + + // Auth populated: BASIC (others null) + var auth = + args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); + assertThat(auth).isNotNull(); + + assertThat(auth.getBasicAuthenticationPolicy()).isNotNull(); + assertThat(auth.getBasicAuthenticationPolicy().getBasic()).isNotNull(); + assertThat( + auth.getBasicAuthenticationPolicy() + .getBasic() + .getBasicAuthenticationProperties() + .getUsername()) + .isEqualTo("alice"); + assertThat( + auth.getBasicAuthenticationPolicy() + .getBasic() + .getBasicAuthenticationProperties() + .getPassword()) + .isEqualTo("secret"); + + assertThat(auth.getBearerAuthenticationPolicy()).isNull(); + assertThat(auth.getDigestAuthenticationPolicy()).isNull(); + assertThat(auth.getOpenIdConnectAuthenticationPolicy()).isNull(); + } + + @Test + void when_call_http_with_bearer_auth_on_endpoint_expr() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks(t -> t.callHTTP(http().GET().endpoint(EXPR_ENDPOINT, bearer("token-123")))) + .build(); + + var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + + assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + + var auth = + args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); + assertThat(auth).isNotNull(); + + assertThat(auth.getBearerAuthenticationPolicy()).isNotNull(); + assertThat(auth.getBearerAuthenticationPolicy().getBearer()).isNotNull(); + assertThat( + auth.getBearerAuthenticationPolicy() + .getBearer() + .getBearerAuthenticationProperties() + .getToken()) + .isEqualTo("token-123"); + + assertThat(auth.getBasicAuthenticationPolicy()).isNull(); + assertThat(auth.getDigestAuthenticationPolicy()).isNull(); + assertThat(auth.getOpenIdConnectAuthenticationPolicy()).isNull(); + } + + @Test + void when_call_http_with_digest_auth_on_endpoint_expr() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks(t -> t.callHTTP(http().GET().endpoint(EXPR_ENDPOINT, digest("bob", "p@ssw0rd")))) + .build(); + + var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + + assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + + var auth = + args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); + assertThat(auth).isNotNull(); + + assertThat(auth.getDigestAuthenticationPolicy()).isNotNull(); + assertThat(auth.getDigestAuthenticationPolicy().getDigest()).isNotNull(); + assertThat( + auth.getDigestAuthenticationPolicy() + .getDigest() + .getDigestAuthenticationProperties() + .getUsername()) + .isEqualTo("bob"); + assertThat( + auth.getDigestAuthenticationPolicy() + .getDigest() + .getDigestAuthenticationProperties() + .getPassword()) + .isEqualTo("p@ssw0rd"); + + assertThat(auth.getBasicAuthenticationPolicy()).isNull(); + assertThat(auth.getBearerAuthenticationPolicy()).isNull(); + assertThat(auth.getOpenIdConnectAuthenticationPolicy()).isNull(); + } + + @Test + void when_call_http_with_oidc_auth_on_endpoint_expr_with_client() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks( + t -> + t.callHTTP( + http() + .POST() + .endpoint( + EXPR_ENDPOINT, + oidc( + "https://auth.example.com/", + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant + .CLIENT_CREDENTIALS, + "client-id", + "client-secret")))) + .build(); + + var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + + assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + + var auth = + args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); + assertThat(auth).isNotNull(); + + assertThat(auth.getOpenIdConnectAuthenticationPolicy()).isNotNull(); + var oidc = auth.getOpenIdConnectAuthenticationPolicy().getOidc(); + assertThat(oidc).isNotNull(); + + var props = oidc.getOpenIdConnectAuthenticationProperties(); + assertThat(props).isNotNull(); + assertThat(props.getAuthority().getLiteralUri()) + .isEqualTo(URI.create("https://auth.example.com/")); + assertThat(props.getGrant()) + .isEqualTo(OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS); + + var client = props.getClient(); + assertThat(client).isNotNull(); + assertThat(client.getId()).isEqualTo("client-id"); + assertThat(client.getSecret()).isEqualTo("client-secret"); + + assertThat(auth.getBasicAuthenticationPolicy()).isNull(); + assertThat(auth.getBearerAuthenticationPolicy()).isNull(); + assertThat(auth.getDigestAuthenticationPolicy()).isNull(); + } + + @Test + void when_call_http_with_oauth2_alias_on_endpoint_expr_without_client() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks( + t -> + t.callHTTP( + http() + .POST() + .endpoint( + EXPR_ENDPOINT, + oauth2( + "https://auth.example.com/", + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant + .CLIENT_CREDENTIALS)))) + .build(); + + var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + + assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + + var auth = + args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); + assertThat(auth).isNotNull(); + + assertThat(auth.getOpenIdConnectAuthenticationPolicy()).isNotNull(); + var oidc = auth.getOpenIdConnectAuthenticationPolicy().getOidc(); + assertThat(oidc).isNotNull(); + + var props = oidc.getOpenIdConnectAuthenticationProperties(); + assertThat(props).isNotNull(); + assertThat(props.getAuthority().getLiteralUri()) + .isEqualTo(URI.create("https://auth.example.com/")); + assertThat(props.getGrant()) + .isEqualTo(OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS); + + // no client provided + assertThat(props.getClient()).isNull(); + + assertThat(auth.getBasicAuthenticationPolicy()).isNull(); + assertThat(auth.getBearerAuthenticationPolicy()).isNull(); + assertThat(auth.getDigestAuthenticationPolicy()).isNull(); + } + + @Test + void when_call_http_with_basic_auth_on_uri_string() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks( + t -> + t.callHTTP( + http().GET().uri("https://api.example.com/v1/resource", basic("u", "p")))) + .build(); + + var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + + assertThat(args.getEndpoint().getUriTemplate().getLiteralUri().toString()) + .isEqualTo("https://api.example.com/v1/resource"); + + var auth = + args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); + assertThat(auth).isNotNull(); + assertThat(auth.getBasicAuthenticationPolicy()).isNotNull(); + assertThat( + auth.getBasicAuthenticationPolicy() + .getBasic() + .getBasicAuthenticationProperties() + .getUsername()) + .isEqualTo("u"); + assertThat( + auth.getBasicAuthenticationPolicy() + .getBasic() + .getBasicAuthenticationProperties() + .getPassword()) + .isEqualTo("p"); + + assertThat(auth.getBearerAuthenticationPolicy()).isNull(); + assertThat(auth.getDigestAuthenticationPolicy()).isNull(); + assertThat(auth.getOAuth2AuthenticationPolicy()).isNull(); + } +} diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java new file mode 100644 index 00000000..894b9a6b --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLTest.java @@ -0,0 +1,263 @@ +/* + * 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.error; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.event; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.http; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.to; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.HTTPArguments; +import io.serverlessworkflow.api.types.ListenTaskConfiguration; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import io.serverlessworkflow.types.Errors; +import java.net.URI; +import org.junit.jupiter.api.Test; + +public class DSLTest { + + @Test + public void when_new_call_http_task() { + Workflow wf = + WorkflowBuilder.workflow("myFlow", "myNs", "1.2.3") + .tasks( + t -> + t.callHTTP( + http() + .acceptJSON() + .header("CustomKey", "CustomValue") + .POST() + .endpoint("${ \"https://petstore.swagger.io/v2/pet/\\(.petId)\" }"))) + .build(); + + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args).isNotNull(); + assertThat(args.getMethod()).isEqualTo("POST"); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept")) + .isEqualTo("application/json"); + assertThat(args.getEndpoint().getRuntimeExpression()) + .isEqualTo("${ \"https://petstore.swagger.io/v2/pet/\\(.petId)\" }"); + } + + @Test + public void when_listen_all_then_emit() { + Workflow wf = + WorkflowBuilder.workflow("myFlow", "myNs", "1.2.3") + .tasks( + t -> + t.listen( + to().all( + event().type("org.acme.listen"), + event().type("org.example.listen"))) + .emit(e -> e.event(event().type("org.example.emit")))) + .build(); + + // Sanity + assertThat(wf).isNotNull(); + assertThat(wf.getDo()).hasSize(2); + + // --- First task: LISTEN with ALL --- + var first = wf.getDo().get(0).getTask(); + assertThat(first.getListenTask()).isNotNull(); + + var listen = first.getListenTask().getListen(); + assertThat(listen).isNotNull(); + var to = listen.getTo(); + assertThat(to).isNotNull(); + + // Exclusivity: ALL set; ONE/ANY not set + assertThat(to.getAllEventConsumptionStrategy()).isNotNull(); + assertThat(to.getOneEventConsumptionStrategy()).isNull(); + assertThat(to.getAnyEventConsumptionStrategy()).isNull(); + + // ALL has exactly 2 EventFilters, in insertion order + var all = to.getAllEventConsumptionStrategy().getAll(); + assertThat(all).hasSize(2); + assertThat(all.get(0).getWith()).isNotNull(); + assertThat(all.get(1).getWith()).isNotNull(); + assertThat(all.get(0).getWith().getType()).isEqualTo("org.acme.listen"); + assertThat(all.get(1).getWith().getType()).isEqualTo("org.example.listen"); + + // --- Second task: EMIT --- + var second = wf.getDo().get(1).getTask(); + assertThat(second.getEmitTask()).isNotNull(); + + var emit = second.getEmitTask().getEmit(); + assertThat(emit).isNotNull(); + // Event type set; id/time/contentType are unset unless explicitly configured + var evProps = emit.getEvent().getWith(); + assertThat(evProps.getType()).isEqualTo("org.example.emit"); + assertThat(evProps.getId()).isNull(); + assertThat(evProps.getTime()).isNull(); + assertThat(evProps.getDatacontenttype()).isNull(); + } + + @Test + public void when_listen_any_with_until() { + final String untilExpr = "$.count > 0"; + + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks( + t -> + t.listen( + to().any(event().type("A"), event().type("B")) + .until(untilExpr) + .andThen(c -> c.read(ListenTaskConfiguration.ListenAndReadAs.RAW)))) + .build(); + + var to = wf.getDo().get(0).getTask().getListenTask().getListen().getTo(); + + assertThat(to.getAnyEventConsumptionStrategy()).isNotNull(); + assertThat(to.getOneEventConsumptionStrategy()).isNull(); + assertThat(to.getAllEventConsumptionStrategy()).isNull(); + + assertThat(wf.getDo().get(0).getTask().getListenTask().getListen().getRead()) + .isEqualTo(ListenTaskConfiguration.ListenAndReadAs.RAW); + + var any = to.getAnyEventConsumptionStrategy(); + assertThat(any.getAny()).hasSize(2); + assertThat(any.getAny().get(0).getWith().getType()).isEqualTo("A"); + assertThat(any.getAny().get(1).getWith().getType()).isEqualTo("B"); + assertThat(any.getUntil().getAnyEventUntilCondition()).isEqualTo(untilExpr); + } + + @Test + public void when_listen_one() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks(t -> t.listen(to().one(event().type("only-once")))) + .build(); + + var to = wf.getDo().get(0).getTask().getListenTask().getListen().getTo(); + + assertThat(to.getOneEventConsumptionStrategy()).isNotNull(); + assertThat(to.getAllEventConsumptionStrategy()).isNull(); + assertThat(to.getAnyEventConsumptionStrategy()).isNull(); + + var one = to.getOneEventConsumptionStrategy().getOne(); + assertThat(one.getWith()).isNotNull(); + assertThat(one.getWith().getType()).isEqualTo("only-once"); + } + + @Test + public void when_raise_with_string_type_and_status() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks( + t -> + t.raise( + error("org.acme.Error", 422).title("Unprocessable").detail("Bad input"))) + .build(); + + var err = wf.getDo().get(0).getTask().getRaiseTask().getRaise().getError(); + + assertThat(err).isNotNull(); + assertThat(err.getRaiseErrorDefinition()).isNotNull(); + assertThat(err.getRaiseErrorDefinition().getType().getExpressionErrorType()) + .isEqualTo("org.acme.Error"); + assertThat(err.getRaiseErrorDefinition().getStatus()).isEqualTo(422); + assertThat(err.getRaiseErrorDefinition().getTitle().getExpressionErrorTitle()) + .isEqualTo("Unprocessable"); + assertThat(err.getRaiseErrorDefinition().getDetail().getExpressionErrorDetails()) + .isEqualTo("Bad input"); + } + + @Test + public void when_raise_with_string_type_only() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks(t -> t.raise(error("org.acme.MinorError"))) + .build(); + + var err = wf.getDo().get(0).getTask().getRaiseTask().getRaise().getError(); + + assertThat(err).isNotNull(); + assertThat(err.getRaiseErrorDefinition()).isNotNull(); + // type as expression + assertThat(err.getRaiseErrorDefinition().getType().getExpressionErrorType()) + .isEqualTo("org.acme.MinorError"); + // status/title/detail not set + assertThat(err.getRaiseErrorDefinition().getStatus()).isEqualTo(0); + assertThat(err.getRaiseErrorDefinition().getTitle()).isNull(); + assertThat(err.getRaiseErrorDefinition().getDetail()).isNull(); + } + + @Test + public void when_raise_with_uri_type_and_status() throws Exception { + URI type = new URI("urn:example:error:bad-request"); + + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks(t -> t.raise(error(type, 400).title("Bad Request").detail("Missing field"))) + .build(); + + var err = wf.getDo().get(0).getTask().getRaiseTask().getRaise().getError(); + + assertThat(err).isNotNull(); + assertThat(err.getRaiseErrorDefinition()).isNotNull(); + // type as URI + assertThat(err.getRaiseErrorDefinition().getType().getLiteralErrorType().getLiteralUri()) + .isEqualTo(type); + assertThat(err.getRaiseErrorDefinition().getStatus()).isEqualTo(400); + assertThat(err.getRaiseErrorDefinition().getTitle().getExpressionErrorTitle()) + .isEqualTo("Bad Request"); + assertThat(err.getRaiseErrorDefinition().getDetail().getExpressionErrorDetails()) + .isEqualTo("Missing field"); + } + + @Test + public void when_raise_with_uri_type_only() throws Exception { + URI type = new URI("urn:example:error:temporary"); + + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks(t -> t.raise(error(type).title("Temporary"))) + .build(); + + var err = wf.getDo().get(0).getTask().getRaiseTask().getRaise().getError(); + + assertThat(err).isNotNull(); + assertThat(err.getRaiseErrorDefinition()).isNotNull(); + // type as URI + assertThat(err.getRaiseErrorDefinition().getType().getLiteralErrorType().getLiteralUri()) + .isEqualTo(type); + // status not set + assertThat(err.getRaiseErrorDefinition().getStatus()).isEqualTo(0); + // title set, detail not set + assertThat(err.getRaiseErrorDefinition().getTitle().getExpressionErrorTitle()) + .isEqualTo("Temporary"); + assertThat(err.getRaiseErrorDefinition().getDetail()).isNull(); + } + + @Test + void serverError_uses_versioned_uri_and_default_code() { + Errors.setSpecVersion("1.1.0"); // flip version globally + var spec = DSL.serverError().title("Boom").detail("x"); + + var wf = WorkflowBuilder.workflow("f", "ns", "1").tasks(t -> t.raise(spec)).build(); + var def = + wf.getDo().get(0).getTask().getRaiseTask().getRaise().getError().getRaiseErrorDefinition(); + + assertThat(def.getType().getLiteralErrorType().getLiteralUri().toString()) + .isEqualTo("https://serverlessworkflow.io/spec/1.1.0/errors/runtime"); + assertThat(def.getStatus()).isEqualTo(500); + assertThat(def.getTitle().getExpressionErrorTitle()).isEqualTo("Boom"); + assertThat(def.getDetail().getExpressionErrorDetails()).isEqualTo("x"); + } +} diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java new file mode 100644 index 00000000..185b65aa --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java @@ -0,0 +1,212 @@ +/* + * 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.emit; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.event; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.http; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.set; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.tryCatch; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import io.serverlessworkflow.types.Errors; +import java.net.URI; +import org.junit.jupiter.api.Test; + +public class TryCatchDslTest { + + private static final String EXPR_ENDPOINT = "${ \"https://api.example.com/v1/resource\" }"; + + @Test + void when_try_with_tasks_and_catch_when_with_retry_and_tasks() { + Workflow wf = + WorkflowBuilder.workflow("f", "ns", "1") + .tasks( + t -> + t.tryCatch( + tryCatch() + // try block (one HTTP call) + .tasks(call(http().GET().endpoint(EXPR_ENDPOINT))) + // catch block + .catches() + .when("$.error == true") + .errors(Errors.RUNTIME, 500) + .tasks(emit(e -> e.event(event().type("org.acme.failed")))) + .retry() + .when("$.retries < 3") + .limit("PT5S") + .jitter("PT0.1S", "PT0.5S") + .done() // back to TryCatchSpec + .done() // back to TrySpec + )) + .build(); + + // --- locate Try task --- + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + + // --- assert try/do tasks --- + var tryDo = tryTask.getTry(); + assertThat(tryDo).isNotNull(); + assertThat(tryDo).hasSize(1); + + // First inner task: Call HTTP GET to EXPR_ENDPOINT + var callArgs = tryDo.get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(callArgs.getMethod()).isEqualTo("GET"); + assertThat(callArgs.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + + // --- assert catch (singular) --- + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + + // when condition + assertThat(cat.getWhen()).isEqualTo("$.error == true"); + + // errors filter (Errors.RUNTIME + 500) + var errors = cat.getErrors(); + assertThat(errors).isNotNull(); + assertThat(errors.getWith().getType()).isEqualTo(Errors.RUNTIME.toString()); + assertThat(errors.getWith().getStatus()).isEqualTo(500); + + // catch/do tasks + var catchDo = cat.getDo(); + assertThat(catchDo).isNotNull(); + assertThat(catchDo).hasSize(1); + var emitted = catchDo.get(0).getTask().getEmitTask().getEmit().getEvent().getWith(); + assertThat(emitted.getType()).isEqualTo("org.acme.failed"); + + // retry policy (attached to this catch) + var retry = cat.getRetry().getRetryPolicyDefinition(); + assertThat(retry).isNotNull(); + assertThat(retry.getWhen()).isEqualTo("$.retries < 3"); + assertThat(retry.getLimit().getDuration().getDurationExpression()).isEqualTo("PT5S"); + + // jitter range if modeled with from/to + if (retry.getJitter() != null) { + assertThat(retry.getJitter().getFrom().getDurationExpression()).isEqualTo("PT0.1S"); + assertThat(retry.getJitter().getTo().getDurationExpression()).isEqualTo("PT0.5S"); + } + } + + @Test + void when_try_with_multiple_tasks_and_catch_except_when_with_uri_error_filter() throws Exception { + URI errType = new URI("urn:example:error:downstream"); + + Workflow wf = + WorkflowBuilder.workflow("g", "ns", "1") + .tasks( + t -> + t.tryCatch( + tryCatch() + // try with two tasks + .tasks( + call(http().GET().endpoint(EXPR_ENDPOINT)), + set("$.status = \"IN_FLIGHT\"")) + // catch with exceptWhen + explicit URI error filter + status + .catches() + .exceptWhen("$.code == 502") + .errors(errType, 502) + .tasks(emit(e -> e.event(event().type("org.acme.recover")))) + .done() // back to TrySpec + )) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + + // try has two tasks + var tryDo = tryTask.getTry(); + assertThat(tryDo).isNotNull(); + assertThat(tryDo).hasSize(2); + + // First is HTTP call (GET + endpoint) + var args0 = tryDo.get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args0.getMethod()).isEqualTo("GET"); + assertThat(args0.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + + // Second is set (presence check) + var setTask = tryDo.get(1).getTask().getSetTask(); + assertThat(setTask).isNotNull(); + + // catch (singular) + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getExceptWhen()).isEqualTo("$.code == 502"); + + var errors = cat.getErrors(); + assertThat(errors).isNotNull(); + assertThat(errors.getWith().getType()).isEqualTo(errType.toString()); + assertThat(errors.getWith().getStatus()).isEqualTo(502); + + var catchDo = cat.getDo(); + assertThat(catchDo).hasSize(1); + var ev = catchDo.get(0).getTask().getEmitTask().getEmit().getEvent().getWith(); + assertThat(ev.getType()).isEqualTo("org.acme.recover"); + + // no retry configured here + assertThat(cat.getRetry().get()).isNull(); + } + + @Test + void when_try_with_catch_and_simple_retry_limit_only() { + Workflow wf = + WorkflowBuilder.workflow("r", "ns", "1") + .tasks( + t -> + t.tryCatch( + tryCatch() + .tasks(call(http().GET().endpoint(EXPR_ENDPOINT))) + .catches() + .when("$.fail == true") + .errors(Errors.COMMUNICATION, 503) + .retry() + .limit("PT2S") + .done() + .tasks(emit(e -> e.event(event().type("org.acme.retrying")))) + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + + var tryDo = tryTask.getTry(); + assertThat(tryDo).isNotNull(); + assertThat(tryDo).hasSize(1); + assertThat(tryDo.get(0).getTask().getCallTask()).isNotNull(); + + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getWhen()).isEqualTo("$.fail == true"); + + var err = cat.getErrors().getWith(); + assertThat(err.getType()).isEqualTo(Errors.COMMUNICATION.toString()); + assertThat(err.getStatus()).isEqualTo(503); + + var retryDef = cat.getRetry().getRetryPolicyDefinition(); + assertThat(retryDef).isNotNull(); + assertThat(retryDef.getLimit().getDuration().getDurationExpression()).isEqualTo("PT2S"); + // 'when' not set in this case + assertThat(retryDef.getWhen()).isNull(); + + var catchDo = cat.getDo(); + assertThat(catchDo).hasSize(1); + var ev = catchDo.get(0).getTask().getEmitTask().getEmit().getEvent().getWith(); + assertThat(ev.getType()).isEqualTo("org.acme.retrying"); + } +} diff --git a/impl/core/pom.xml b/impl/core/pom.xml index fcbca070..7c507613 100644 --- a/impl/core/pom.xml +++ b/impl/core/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 io.serverlessworkflow @@ -8,17 +9,17 @@ serverlessworkflow-impl-core Serverless Workflow :: Impl :: Core - - io.serverlessworkflow - serverlessworkflow-types + + io.serverlessworkflow + serverlessworkflow-types io.cloudevents cloudevents-core - org.slf4j - slf4j-api + org.slf4j + slf4j-api com.github.f4b6a3 diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java index 7381d4d9..c0952c01 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java @@ -15,12 +15,13 @@ */ package io.serverlessworkflow.impl; +import io.serverlessworkflow.types.Errors; + public record WorkflowError( String type, int status, String instance, String title, String details) { - private static final String ERROR_FORMAT = "https://serverlessworkflow.io/spec/1.0.0/errors/%s"; - public static final String RUNTIME_TYPE = String.format(ERROR_FORMAT, "runtime"); - public static final String COMM_TYPE = String.format(ERROR_FORMAT, "communication"); + public static final Errors.Standard RUNTIME_TYPE = Errors.RUNTIME; + public static final Errors.Standard COMM_TYPE = Errors.COMMUNICATION; public static Builder error(String type, int status) { return new Builder(type, status); @@ -31,15 +32,25 @@ public static Builder communication(int status, TaskContext context, Exception e } public static Builder communication(int status, TaskContext context, String title) { - return new Builder(COMM_TYPE, status).instance(context.position().jsonPointer()).title(title); + return new Builder(COMM_TYPE.toString(), status) + .instance(context.position().jsonPointer()) + .title(title); + } + + public static Builder communication(TaskContext context, String title) { + return communication(COMM_TYPE.status(), context, title); } public static Builder runtime(int status, TaskContext context, Exception ex) { - return new Builder(RUNTIME_TYPE, status) + return new Builder(RUNTIME_TYPE.toString(), status) .instance(context.position().jsonPointer()) .title(ex.getMessage()); } + public static Builder runtime(TaskContext context, Exception ex) { + return runtime(RUNTIME_TYPE.status(), context, ex); + } + public static class Builder { private final String type; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java index 685fc077..59b16d51 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java @@ -19,7 +19,7 @@ public class WorkflowException extends RuntimeException { private static final long serialVersionUID = 1L; - private final WorkflowError worflowError; + private final WorkflowError workflowError; public WorkflowException(WorkflowError error) { this(error, null); @@ -27,10 +27,10 @@ public WorkflowException(WorkflowError error) { public WorkflowException(WorkflowError error, Throwable cause) { super(error.toString(), cause); - this.worflowError = error; + this.workflowError = error; } - public WorkflowError getWorflowError() { - return worflowError; + public WorkflowError getWorkflowError() { + return workflowError; } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java index 13d0500b..5d303aff 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java @@ -106,7 +106,7 @@ private CompletableFuture handleException( } if (e instanceof WorkflowException) { WorkflowException exception = (WorkflowException) e; - if (errorFilter.map(f -> f.test(exception.getWorflowError())).orElse(true) + if (errorFilter.map(f -> f.test(exception.getWorkflowError())).orElse(true) && whenFilter.map(w -> w.test(workflow, taskContext, taskContext.input())).orElse(true) && exceptFilter .map(w -> !w.test(workflow, taskContext, taskContext.input())) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/lifecycle/ce/WorkflowErrorCEData.java b/impl/core/src/main/java/io/serverlessworkflow/impl/lifecycle/ce/WorkflowErrorCEData.java index 776a0185..d74a6c12 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/lifecycle/ce/WorkflowErrorCEData.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/lifecycle/ce/WorkflowErrorCEData.java @@ -47,7 +47,7 @@ private static WorkflowErrorCEData commonError(Throwable cause) { } private static WorkflowErrorCEData error(WorkflowException ex) { - WorkflowError error = ex.getWorflowError(); + WorkflowError error = ex.getWorkflowError(); return new WorkflowErrorCEData( error.type(), error.status(), error.instance(), error.title(), error.details()); } diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowDefinitionTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowDefinitionTest.java index d47dd4c0..856d8dc0 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowDefinitionTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WorkflowDefinitionTest.java @@ -177,12 +177,12 @@ private static JsonNode createObjectNode( } private static void checkWorkflowException(WorkflowException ex) { - assertThat(ex.getWorflowError().type()) + assertThat(ex.getWorkflowError().type()) .isEqualTo("https://serverlessworkflow.io/errors/not-implemented"); - assertThat(ex.getWorflowError().status()).isEqualTo(500); - assertThat(ex.getWorflowError().title()).isEqualTo("Not Implemented"); - assertThat(ex.getWorflowError().details()).contains("raise-not-implemented"); - assertThat(ex.getWorflowError().instance()).isEqualTo("do/0/notImplemented"); + assertThat(ex.getWorkflowError().status()).isEqualTo(500); + assertThat(ex.getWorkflowError().title()).isEqualTo("Not Implemented"); + assertThat(ex.getWorkflowError().details()).contains("raise-not-implemented"); + assertThat(ex.getWorkflowError().instance()).isEqualTo("do/0/notImplemented"); } private static void checkSpecialKeywords(Object obj) { diff --git a/types/src/main/java/io/serverlessworkflow/types/Errors.java b/types/src/main/java/io/serverlessworkflow/types/Errors.java new file mode 100644 index 00000000..e7b09056 --- /dev/null +++ b/types/src/main/java/io/serverlessworkflow/types/Errors.java @@ -0,0 +1,96 @@ +/* + * 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.types; + +import java.net.URI; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * Standard error types with a configurable spec version. + * + * @see DSL + * Reference - Standard Error Types + */ +public final class Errors { + + private Errors() {} + + private static final AtomicReference> VERSION_SUPPLIER = + new AtomicReference<>(() -> "1.0.0"); + + private static final AtomicReference BASE_PATTERN = + new AtomicReference<>("https://serverlessworkflow.io/spec/%s/errors/"); + + public static void setSpecVersion(String version) { + Objects.requireNonNull(version, "version"); + VERSION_SUPPLIER.set(() -> version); + } + + public static void setVersionSupplier(Supplier supplier) { + VERSION_SUPPLIER.set(Objects.requireNonNull(supplier, "supplier")); + } + + public static void setBasePattern(String basePattern) { + if (basePattern == null || !basePattern.contains("%s")) { + throw new IllegalArgumentException("basePattern must include %s placeholder for the version"); + } + BASE_PATTERN.set(basePattern); + } + + private static URI uriFor(String slug) { + String base = String.format(BASE_PATTERN.get(), VERSION_SUPPLIER.get().get()); + return URI.create(base + slug); + } + + public static final class Standard { + private final String slug; + private final int defaultStatus; + + private Standard(String slug, int defaultStatus) { + this.slug = slug; + this.defaultStatus = defaultStatus; + } + + public URI uri() { + return uriFor(slug); + } + + public int status() { + return defaultStatus; + } + + public Standard withStatus(int override) { + return new Standard(slug, override); + } + + @Override + public String toString() { + return uri().toString(); + } + } + + // ---- Standard catalog (defaults are conventional HTTP mappings; override if you prefer) ---- + public static final Standard RUNTIME = new Standard("runtime", 500); + public static final Standard COMMUNICATION = new Standard("communication", 502); + public static final Standard AUTHENTICATION = new Standard("authentication", 401); + public static final Standard AUTHORIZATION = new Standard("authorization", 403); + public static final Standard DATA = new Standard("data", 422); + public static final Standard TIMEOUT = new Standard("timeout", 408); + public static final Standard NOT_IMPLEMENTED = new Standard("not-implemented", 501); +}