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 super CallHttpTaskBuilder> after) {
+ // Convert Consumer super CallHttpTaskBuilder> 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");
+ }
+}