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 0ff056dac..0b097f996 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 @@ -31,4 +31,16 @@ protected CallHttpTaskBuilder() { public CallHttpTaskBuilder self() { return this; } + + /** + * Sets the output expression for this task (equivalent to {@code output.as} in YAML). + * + *

Uses jq expression syntax (e.g., {@code $.field} to select a field from the result). + * + * @param expr jq expression to extract the desired output value from the task result + * @return this builder for chaining + */ + public CallHttpTaskBuilder outputAs(String expr) { + return this.output(b -> b.as(expr)); + } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java index bd58b3e58..39f56e3d8 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java @@ -122,6 +122,64 @@ default SELF query(String name, String value) { return self(); } + default SELF PUT() { + steps().add(c -> c.method("PUT")); + return self(); + } + + default SELF DELETE() { + steps().add(c -> c.method("DELETE")); + return self(); + } + + default SELF PATCH() { + steps().add(c -> c.method("PATCH")); + return self(); + } + + default SELF HEAD() { + steps().add(c -> c.method("HEAD")); + return self(); + } + + default SELF OPTIONS() { + steps().add(c -> c.method("OPTIONS")); + return self(); + } + + default SELF redirect(boolean redirect) { + steps().add(c -> c.redirect(redirect)); + return self(); + } + + default SELF acceptXML() { + return header("Accept", "application/xml"); + } + + default SELF acceptForm() { + return header("Accept", "application/x-www-form-urlencoded"); + } + + default SELF acceptText() { + return header("Accept", "text/plain"); + } + + default SELF contentTypeJSON() { + return header("Content-Type", "application/json"); + } + + default SELF contentTypeXML() { + return header("Content-Type", "application/xml"); + } + + default SELF contentTypeForm() { + return header("Content-Type", "application/x-www-form-urlencoded"); + } + + default SELF contentTypeText() { + return header("Content-Type", "text/plain"); + } + default void accept(CallHttpTaskFluent b) { for (var s : steps()) { s.accept(b); 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 index 0cc53fa21..11f9b69b3 100644 --- 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 @@ -24,7 +24,7 @@ public final class CallHttpSpec implements BaseCallHttpSpec, CallHttpConfigurer { - private final List>> steps = new ArrayList<>(); + private final List> steps = new ArrayList<>(); public CallHttpSpec() {} @@ -34,12 +34,21 @@ public CallHttpSpec self() { } @Override + @SuppressWarnings("unchecked") public List>> steps() { - return steps; + // Safe cast because we control that all added consumers accept CallHttpTaskBuilder + return (List) steps; } @Override public void accept(CallHttpTaskBuilder builder) { BaseCallHttpSpec.super.accept(builder); } + + @Override + public CallHttpSpec andThen(Consumer after) { + // Convert Consumer to Consumer for storage + steps.add(builder -> after.accept(builder)); + return this; + } } diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpDslExtensionsTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpDslExtensionsTest.java new file mode 100644 index 000000000..d159f633d --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpDslExtensionsTest.java @@ -0,0 +1,276 @@ +/* + * 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.http; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.CallHTTP; +import io.serverlessworkflow.api.types.HTTPArguments; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class CallHttpDslExtensionsTest { + + @Test + void when_put_method_shortcut() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().PUT().uri("http://test.com"))) + .build(); + CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP(); + assertThat(call.getWith().getMethod()).isEqualTo("PUT"); + } + + @Test + void when_delete_method_shortcut() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().DELETE().uri("http://test.com"))) + .build(); + CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP(); + assertThat(call.getWith().getMethod()).isEqualTo("DELETE"); + } + + @Test + void when_patch_method_shortcut() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().PATCH().uri("http://test.com"))) + .build(); + CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP(); + assertThat(call.getWith().getMethod()).isEqualTo("PATCH"); + } + + @Test + void when_head_method_shortcut() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().HEAD().uri("http://test.com"))) + .build(); + CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP(); + assertThat(call.getWith().getMethod()).isEqualTo("HEAD"); + } + + @Test + void when_options_method_shortcut() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().OPTIONS().uri("http://test.com"))) + .build(); + CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP(); + assertThat(call.getWith().getMethod()).isEqualTo("OPTIONS"); + } + + @Test + void when_redirect_true() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().redirect(true).POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.isRedirect()).isTrue(); + } + + @Test + void when_redirect_false() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().redirect(false).POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.isRedirect()).isFalse(); + } + + @Test + void when_acceptXML() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().acceptXML().POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept")) + .isEqualTo("application/xml"); + } + + @Test + void when_acceptForm() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().acceptForm().POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept")) + .isEqualTo("application/x-www-form-urlencoded"); + } + + @Test + void when_acceptText() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().acceptText().POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept")) + .isEqualTo("text/plain"); + } + + @Test + void when_contentTypeXML() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().contentTypeXML().POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type")) + .isEqualTo("application/xml"); + } + + @Test + void when_contentTypeForm() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().contentTypeForm().POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type")) + .isEqualTo("application/x-www-form-urlencoded"); + } + + @Test + void when_contentTypeText() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks(call(http().contentTypeText().POST().uri("http://test.com"))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type")) + .isEqualTo("text/plain"); + } + + @Test + void when_andThen_adds_query() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks( + call( + http() + .GET() + .uri("http://test.com") + .andThen(b -> b.query(Map.of("key", "value"))))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getQuery().getHTTPQuery().getAdditionalProperties().get("key")) + .isEqualTo("value"); + } + + @Test + void when_andThen_adds_body() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks( + call( + http() + .POST() + .uri("http://test.com") + .andThen(b -> b.body(Map.of("foo", "bar"))))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.getBody()).isInstanceOf(Map.class); + @SuppressWarnings("unchecked") + Map body = (Map) args.getBody(); + assertThat(body).containsEntry("foo", "bar"); + } + + @Test + void when_andThen_chained_multiple_times() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks( + call( + http() + .GET() + .uri("http://test.com") + .andThen(b -> b.redirect(false)) + .andThen(b -> b.query(Map.of("q", "1"))) + .andThen(b -> b.body(Map.of("key", "value"))))) + .build(); + HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); + assertThat(args.isRedirect()).isFalse(); + assertThat(args.getQuery().getHTTPQuery().getAdditionalProperties().get("q")).isEqualTo("1"); + assertThat(args.getBody()).isInstanceOf(Map.class); + @SuppressWarnings("unchecked") + Map body = (Map) args.getBody(); + assertThat(body).containsEntry("key", "value"); + } + + @Test + void when_outputAs_with_expression() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks( + call(http().POST().uri("http://test.com").andThen(b -> b.outputAs("$.firstName")))) + .build(); + // output.as is set on the CallHTTP task itself + CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP(); + assertThat(call.getOutput().getAs().getString()).isEqualTo("$.firstName"); + } + + @Test + void when_http_call_with_all_features() { + var wf = + WorkflowBuilder.workflow("test", "ns", "1") + .tasks( + call( + "myTask", + http() + .contentTypeJSON() + .acceptJSON() + .redirect(true) + .POST() + .uri("http://localhost:9876/api/v1/authors") + .body(Map.of("firstName", "John", "lastName", "Doe")) + .query(Map.of("sort", "asc")) + .header("X-Custom", "value") + .andThen(b -> b.outputAs("$.id")))) + .build(); + + var taskItem = wf.getDo().get(0); + assertThat(taskItem.getName()).isEqualTo("myTask"); + + CallHTTP call = taskItem.getTask().getCallTask().getCallHTTP(); + HTTPArguments args = call.getWith(); + + assertThat(args.getMethod()).isEqualTo("POST"); + assertThat(args.isRedirect()).isTrue(); + assertThat(args.getBody()).isInstanceOf(Map.class); + @SuppressWarnings("unchecked") + Map body = (Map) args.getBody(); + assertThat(body).containsEntry("firstName", "John").containsEntry("lastName", "Doe"); + assertThat(args.getQuery().getHTTPQuery().getAdditionalProperties().get("sort")) + .isEqualTo("asc"); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type")) + .isEqualTo("application/json"); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept")) + .isEqualTo("application/json"); + assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("X-Custom")) + .isEqualTo("value"); + + assertThat(call.getOutput().getAs().getString()).isEqualTo("$.id"); + } +}