Skip to content

Commit be067c4

Browse files
committed
Add HTTP method shortcuts and output expression support in DSL
Signed-off-by: Matheus André <matheusandr2@gmail.com>
1 parent 5fc29d6 commit be067c4

4 files changed

Lines changed: 352 additions & 2 deletions

File tree

fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallHttpTaskBuilder.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,16 @@ protected CallHttpTaskBuilder() {
3131
public CallHttpTaskBuilder self() {
3232
return this;
3333
}
34+
35+
/**
36+
* Sets the output expression for this task (equivalent to {@code output.as} in YAML).
37+
*
38+
* <p>Uses jq expression syntax (e.g., {@code $.field} to select a field from the result).
39+
*
40+
* @param expr jq expression to extract the desired output value from the task result
41+
* @return this builder for chaining
42+
*/
43+
public CallHttpTaskBuilder outputAs(String expr) {
44+
return this.output(b -> b.as(expr));
45+
}
3446
}

fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,59 @@ default SELF query(String name, String value) {
122122
return self();
123123
}
124124

125+
default SELF PUT() {
126+
steps().add(c -> c.method("PUT"));
127+
return self();
128+
}
129+
130+
default SELF DELETE() {
131+
steps().add(c -> c.method("DELETE"));
132+
return self();
133+
}
134+
135+
default SELF PATCH() {
136+
steps().add(c -> c.method("PATCH"));
137+
return self();
138+
}
139+
140+
default SELF HEAD() {
141+
steps().add(c -> c.method("HEAD"));
142+
return self();
143+
}
144+
145+
default SELF OPTIONS() {
146+
steps().add(c -> c.method("OPTIONS"));
147+
return self();
148+
}
149+
150+
default SELF acceptXML() {
151+
return header("Accept", "application/xml");
152+
}
153+
154+
default SELF acceptForm() {
155+
return header("Accept", "application/x-www-form-urlencoded");
156+
}
157+
158+
default SELF acceptText() {
159+
return header("Accept", "text/plain");
160+
}
161+
162+
default SELF contentTypeJSON() {
163+
return header("Content-Type", "application/json");
164+
}
165+
166+
default SELF contentTypeXML() {
167+
return header("Content-Type", "application/xml");
168+
}
169+
170+
default SELF contentTypeForm() {
171+
return header("Content-Type", "application/x-www-form-urlencoded");
172+
}
173+
174+
default SELF contentTypeText() {
175+
return header("Content-Type", "text/plain");
176+
}
177+
125178
default SELF redirect(boolean redirect) {
126179
steps().add(c -> c.redirect(redirect));
127180
return self();

fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpSpec.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
public final class CallHttpSpec implements BaseCallHttpSpec<CallHttpSpec>, CallHttpConfigurer {
2626

27-
private final List<Consumer<CallHttpTaskFluent<?>>> steps = new ArrayList<>();
27+
private final List<Consumer<CallHttpTaskBuilder>> steps = new ArrayList<>();
2828

2929
public CallHttpSpec() {}
3030

@@ -34,12 +34,21 @@ public CallHttpSpec self() {
3434
}
3535

3636
@Override
37+
@SuppressWarnings("unchecked")
3738
public List<Consumer<CallHttpTaskFluent<?>>> steps() {
38-
return steps;
39+
// Safe cast because we control that all added consumers accept CallHttpTaskBuilder
40+
return (List) steps;
3941
}
4042

4143
@Override
4244
public void accept(CallHttpTaskBuilder builder) {
4345
BaseCallHttpSpec.super.accept(builder);
4446
}
47+
48+
@Override
49+
public CallHttpSpec andThen(Consumer<? super CallHttpTaskBuilder> after) {
50+
// Convert Consumer<? super CallHttpTaskBuilder> to Consumer<CallHttpTaskBuilder> for storage
51+
steps.add(builder -> after.accept(builder));
52+
return this;
53+
}
4554
}
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.fluent.spec.dsl;
17+
18+
import static io.serverlessworkflow.fluent.spec.dsl.DSL.call;
19+
import static io.serverlessworkflow.fluent.spec.dsl.DSL.http;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
import io.serverlessworkflow.api.types.CallHTTP;
23+
import io.serverlessworkflow.api.types.HTTPArguments;
24+
import io.serverlessworkflow.fluent.spec.WorkflowBuilder;
25+
import java.util.Map;
26+
import org.junit.jupiter.api.Test;
27+
28+
class CallHttpDslExtensionsTest {
29+
30+
@Test
31+
void when_put_method_shortcut() {
32+
var wf =
33+
WorkflowBuilder.workflow("test", "ns", "1")
34+
.tasks(call(http().PUT().uri("http://test.com")))
35+
.build();
36+
CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP();
37+
assertThat(call.getWith().getMethod()).isEqualTo("PUT");
38+
}
39+
40+
@Test
41+
void when_delete_method_shortcut() {
42+
var wf =
43+
WorkflowBuilder.workflow("test", "ns", "1")
44+
.tasks(call(http().DELETE().uri("http://test.com")))
45+
.build();
46+
CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP();
47+
assertThat(call.getWith().getMethod()).isEqualTo("DELETE");
48+
}
49+
50+
@Test
51+
void when_patch_method_shortcut() {
52+
var wf =
53+
WorkflowBuilder.workflow("test", "ns", "1")
54+
.tasks(call(http().PATCH().uri("http://test.com")))
55+
.build();
56+
CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP();
57+
assertThat(call.getWith().getMethod()).isEqualTo("PATCH");
58+
}
59+
60+
@Test
61+
void when_head_method_shortcut() {
62+
var wf =
63+
WorkflowBuilder.workflow("test", "ns", "1")
64+
.tasks(call(http().HEAD().uri("http://test.com")))
65+
.build();
66+
CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP();
67+
assertThat(call.getWith().getMethod()).isEqualTo("HEAD");
68+
}
69+
70+
@Test
71+
void when_options_method_shortcut() {
72+
var wf =
73+
WorkflowBuilder.workflow("test", "ns", "1")
74+
.tasks(call(http().OPTIONS().uri("http://test.com")))
75+
.build();
76+
CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP();
77+
assertThat(call.getWith().getMethod()).isEqualTo("OPTIONS");
78+
}
79+
80+
@Test
81+
void when_redirect_true() {
82+
var wf =
83+
WorkflowBuilder.workflow("test", "ns", "1")
84+
.tasks(call(http().redirect(true).POST().uri("http://test.com")))
85+
.build();
86+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
87+
assertThat(args.isRedirect()).isTrue();
88+
}
89+
90+
@Test
91+
void when_redirect_false() {
92+
var wf =
93+
WorkflowBuilder.workflow("test", "ns", "1")
94+
.tasks(call(http().redirect(false).POST().uri("http://test.com")))
95+
.build();
96+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
97+
assertThat(args.isRedirect()).isFalse();
98+
}
99+
100+
@Test
101+
void when_acceptXML() {
102+
var wf =
103+
WorkflowBuilder.workflow("test", "ns", "1")
104+
.tasks(call(http().acceptXML().POST().uri("http://test.com")))
105+
.build();
106+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
107+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept"))
108+
.isEqualTo("application/xml");
109+
}
110+
111+
@Test
112+
void when_acceptForm() {
113+
var wf =
114+
WorkflowBuilder.workflow("test", "ns", "1")
115+
.tasks(call(http().acceptForm().POST().uri("http://test.com")))
116+
.build();
117+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
118+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept"))
119+
.isEqualTo("application/x-www-form-urlencoded");
120+
}
121+
122+
@Test
123+
void when_acceptText() {
124+
var wf =
125+
WorkflowBuilder.workflow("test", "ns", "1")
126+
.tasks(call(http().acceptText().POST().uri("http://test.com")))
127+
.build();
128+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
129+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept"))
130+
.isEqualTo("text/plain");
131+
}
132+
133+
@Test
134+
void when_contentTypeXML() {
135+
var wf =
136+
WorkflowBuilder.workflow("test", "ns", "1")
137+
.tasks(call(http().contentTypeXML().POST().uri("http://test.com")))
138+
.build();
139+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
140+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type"))
141+
.isEqualTo("application/xml");
142+
}
143+
144+
@Test
145+
void when_contentTypeForm() {
146+
var wf =
147+
WorkflowBuilder.workflow("test", "ns", "1")
148+
.tasks(call(http().contentTypeForm().POST().uri("http://test.com")))
149+
.build();
150+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
151+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type"))
152+
.isEqualTo("application/x-www-form-urlencoded");
153+
}
154+
155+
@Test
156+
void when_contentTypeText() {
157+
var wf =
158+
WorkflowBuilder.workflow("test", "ns", "1")
159+
.tasks(call(http().contentTypeText().POST().uri("http://test.com")))
160+
.build();
161+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
162+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type"))
163+
.isEqualTo("text/plain");
164+
}
165+
166+
@Test
167+
void when_andThen_adds_query() {
168+
var wf =
169+
WorkflowBuilder.workflow("test", "ns", "1")
170+
.tasks(
171+
call(
172+
http()
173+
.GET()
174+
.uri("http://test.com")
175+
.andThen(b -> b.query(Map.of("key", "value")))))
176+
.build();
177+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
178+
assertThat(args.getQuery().getHTTPQuery().getAdditionalProperties().get("key"))
179+
.isEqualTo("value");
180+
}
181+
182+
@Test
183+
void when_andThen_adds_body() {
184+
var wf =
185+
WorkflowBuilder.workflow("test", "ns", "1")
186+
.tasks(
187+
call(
188+
http()
189+
.POST()
190+
.uri("http://test.com")
191+
.andThen(b -> b.body(Map.of("foo", "bar")))))
192+
.build();
193+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
194+
assertThat(args.getBody()).isInstanceOf(Map.class);
195+
@SuppressWarnings("unchecked")
196+
Map<String, Object> body = (Map<String, Object>) args.getBody();
197+
assertThat(body).containsEntry("foo", "bar");
198+
}
199+
200+
@Test
201+
void when_andThen_chained_multiple_times() {
202+
var wf =
203+
WorkflowBuilder.workflow("test", "ns", "1")
204+
.tasks(
205+
call(
206+
http()
207+
.GET()
208+
.uri("http://test.com")
209+
.andThen(b -> b.redirect(false))
210+
.andThen(b -> b.query(Map.of("q", "1")))
211+
.andThen(b -> b.body(Map.of("key", "value")))))
212+
.build();
213+
HTTPArguments args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith();
214+
assertThat(args.isRedirect()).isFalse();
215+
assertThat(args.getQuery().getHTTPQuery().getAdditionalProperties().get("q")).isEqualTo("1");
216+
assertThat(args.getBody()).isInstanceOf(Map.class);
217+
@SuppressWarnings("unchecked")
218+
Map<String, Object> body = (Map<String, Object>) args.getBody();
219+
assertThat(body).containsEntry("key", "value");
220+
}
221+
222+
@Test
223+
void when_outputAs_with_expression() {
224+
var wf =
225+
WorkflowBuilder.workflow("test", "ns", "1")
226+
.tasks(
227+
call(http().POST().uri("http://test.com").andThen(b -> b.outputAs("$.firstName"))))
228+
.build();
229+
// output.as is set on the CallHTTP task itself
230+
CallHTTP call = wf.getDo().get(0).getTask().getCallTask().getCallHTTP();
231+
assertThat(call.getOutput().getAs().getString()).isEqualTo("$.firstName");
232+
}
233+
234+
@Test
235+
void when_http_call_with_all_features() {
236+
var wf =
237+
WorkflowBuilder.workflow("test", "ns", "1")
238+
.tasks(
239+
call(
240+
"myTask",
241+
http()
242+
.contentTypeJSON()
243+
.acceptJSON()
244+
.redirect(true)
245+
.POST()
246+
.uri("http://localhost:9876/api/v1/authors")
247+
.body(Map.of("firstName", "John", "lastName", "Doe"))
248+
.query(Map.of("sort", "asc"))
249+
.header("X-Custom", "value")
250+
.andThen(b -> b.outputAs("$.id"))))
251+
.build();
252+
253+
var taskItem = wf.getDo().get(0);
254+
assertThat(taskItem.getName()).isEqualTo("myTask");
255+
256+
CallHTTP call = taskItem.getTask().getCallTask().getCallHTTP();
257+
HTTPArguments args = call.getWith();
258+
259+
assertThat(args.getMethod()).isEqualTo("POST");
260+
assertThat(args.isRedirect()).isTrue();
261+
assertThat(args.getBody()).isInstanceOf(Map.class);
262+
@SuppressWarnings("unchecked")
263+
Map<String, Object> body = (Map<String, Object>) args.getBody();
264+
assertThat(body).containsEntry("firstName", "John").containsEntry("lastName", "Doe");
265+
assertThat(args.getQuery().getHTTPQuery().getAdditionalProperties().get("sort"))
266+
.isEqualTo("asc");
267+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Content-Type"))
268+
.isEqualTo("application/json");
269+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("Accept"))
270+
.isEqualTo("application/json");
271+
assertThat(args.getHeaders().getHTTPHeaders().getAdditionalProperties().get("X-Custom"))
272+
.isEqualTo("value");
273+
274+
assertThat(call.getOutput().getAs().getString()).isEqualTo("$.id");
275+
}
276+
}

0 commit comments

Comments
 (0)