Skip to content

Commit e174638

Browse files
committed
[Fix #1372] conversion to no collection type forWorkflowModelCollection
Signed-off-by: fjtirado <ftirados@redhat.com>
1 parent 64e52ce commit e174638

3 files changed

Lines changed: 140 additions & 60 deletions

File tree

experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncEventFilterTest.java

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -141,41 +141,105 @@ void sendEmail(NewsletterDraft draft) {
141141
}
142142

143143
@Test
144-
void testJacksonAutomagicalConversion() throws Exception {
145-
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
144+
void testAutomaticConversion() throws Exception {
145+
testConversionWorkflow(
146+
FuncWorkflowBuilder.workflow("intelligent-newsletter")
147+
.tasks(
148+
function("draftAgent", this::writeDraft).exportAsTaskOutput(),
149+
emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class),
150+
listen(
151+
"waitHumanReview",
152+
to().one(
153+
consumed("org.acme.newsletter.review.done")
154+
.extensionByInstanceId("instanceid"))),
155+
switchWhenOrElse(
156+
h -> HumanReview.NEEDS_REVISION.equals(h.status()),
157+
"humanEditorAgent",
158+
"sendNewsletter",
159+
HumanReview.class),
160+
function("humanEditorAgent", this::editDraft)
161+
.exportAsTaskOutput()
162+
.then("draftReady"),
163+
consume("sendNewsletter", this::sendEmail)
164+
// Because we are in Jackson, the payload at this evaluation stage can be a
165+
// Map.
166+
// We simply check for the "status" field to know if it's the review payload.
167+
.inputFrom(
168+
(Map<String, Object> payload,
169+
WorkflowContextData wfc,
170+
TaskContextData tfc) ->
171+
payload.containsKey("status") ? wfc.context() : payload))
172+
.build());
173+
}
146174

147-
Workflow workflow =
148-
FuncWorkflowBuilder.workflow("intelligent-newsletter")
149-
.tasks(
150-
function("draftAgent", this::writeDraft).exportAsTaskOutput(),
151-
emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class),
152-
listen(
153-
"waitHumanReview",
154-
to().one(
155-
consumed("org.acme.newsletter.review.done")
156-
.extensionByInstanceId("instanceid")))
157-
.outputAs((Collection<?> events) -> events.iterator().next()),
158-
// The engine sees the incoming JsonNode, sees this task expects
159-
// HumanReview.class,
160-
// and natively deserializes it for you before executing the lambda!
161-
switchWhenOrElse(
162-
h -> HumanReview.NEEDS_REVISION.equals(h.status()),
163-
"humanEditorAgent",
164-
"sendNewsletter",
165-
HumanReview.class),
166-
function("humanEditorAgent", this::editDraft)
167-
.exportAsTaskOutput()
168-
.then("draftReady"),
169-
consume("sendNewsletter", this::sendEmail)
170-
// Because we are in Jackson, the payload at this evaluation stage can be a
171-
// Map.
172-
// We simply check for the "status" field to know if it's the review payload.
173-
.inputFrom(
174-
(Map<String, Object> payload,
175-
WorkflowContextData wfc,
176-
TaskContextData tfc) ->
177-
payload.containsKey("status") ? wfc.context() : payload))
178-
.build();
175+
@Test
176+
void testCollectionConversion() throws Exception {
177+
testConversionWorkflow(
178+
FuncWorkflowBuilder.workflow("intelligent-newsletter")
179+
.tasks(
180+
function("draftAgent", this::writeDraft).exportAsTaskOutput(),
181+
emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class),
182+
listen(
183+
"waitHumanReview",
184+
to().one(
185+
consumed("org.acme.newsletter.review.done")
186+
.extensionByInstanceId("instanceid")))
187+
.outputAs((Collection col) -> col.iterator().next()),
188+
switchWhenOrElse(
189+
h -> HumanReview.NEEDS_REVISION.equals(h.status()),
190+
"humanEditorAgent",
191+
"sendNewsletter",
192+
HumanReview.class),
193+
function("humanEditorAgent", this::editDraft)
194+
.exportAsTaskOutput()
195+
.then("draftReady"),
196+
consume("sendNewsletter", this::sendEmail)
197+
// Because we are in Jackson, the payload at this evaluation stage can be a
198+
// Map.
199+
// We simply check for the "status" field to know if it's the review payload.
200+
.inputFrom(
201+
(Map<String, Object> payload,
202+
WorkflowContextData wfc,
203+
TaskContextData tfc) ->
204+
payload.containsKey("status") ? wfc.context() : payload))
205+
.build());
206+
}
207+
208+
@Test
209+
void testNodeConversion() throws Exception {
210+
testConversionWorkflow(
211+
FuncWorkflowBuilder.workflow("intelligent-newsletter")
212+
.tasks(
213+
function("draftAgent", this::writeDraft).exportAsTaskOutput(),
214+
emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class),
215+
listen(
216+
"waitHumanReview",
217+
to().one(
218+
consumed("org.acme.newsletter.review.done")
219+
.extensionByInstanceId("instanceid")))
220+
.outputAs((ArrayNode col) -> col.get(0)),
221+
switchWhenOrElse(
222+
h -> HumanReview.NEEDS_REVISION.equals(h.status()),
223+
"humanEditorAgent",
224+
"sendNewsletter",
225+
HumanReview.class),
226+
function("humanEditorAgent", this::editDraft)
227+
.exportAsTaskOutput()
228+
.then("draftReady"),
229+
consume("sendNewsletter", this::sendEmail)
230+
// Because we are in Jackson, the payload at this evaluation stage can be a
231+
// Map.
232+
// We simply check for the "status" field to know if it's the review payload.
233+
.inputFrom(
234+
(Map<String, Object> payload,
235+
WorkflowContextData wfc,
236+
TaskContextData tfc) ->
237+
payload.containsKey("status") ? wfc.context() : payload))
238+
.build());
239+
}
240+
241+
private void testConversionWorkflow(Workflow workflow) throws Exception {
242+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
179243

180244
WorkflowDefinition definition = app.workflowDefinition(workflow);
181245
WorkflowInstance instance = definition.instance(new NewsletterRequest("Tech Stocks"));

impl/core/src/main/java/io/serverlessworkflow/impl/CollectionConversionUtils.java

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Collection;
2121
import java.util.HashSet;
22+
import java.util.Iterator;
2223
import java.util.List;
2324
import java.util.Optional;
2425
import java.util.Set;
@@ -28,39 +29,57 @@ public final class CollectionConversionUtils {
2829
private CollectionConversionUtils() {}
2930

3031
/**
31-
* Safely converts a base Collection into the requested List, Set, or Array type.
32+
* Safely converts an Iterable into the requested type.
3233
*
33-
* @param elements The base collection of elements.
34+
* @param elements Iterable containing the elements to be converted.
3435
* @param clazz The target class to convert to.
35-
* @param primitiveConverter Strategy for converting items to primitives if an array is requested.
36+
* @param converter Convert items to class if requested.
3637
*/
3738
public static <T> Optional<T> as(
38-
Collection<?> elements,
39-
Class<T> clazz,
40-
BiFunction<Object, Class<?>, Object> primitiveConverter) {
39+
Iterable<?> elements, Class<T> clazz, BiFunction<Object, Class<?>, Object> converter) {
4140
if (clazz.isAssignableFrom(List.class))
42-
return Optional.of(clazz.cast(new ArrayList<>(elements)));
41+
return Optional.of(clazz.cast(iterableToCollection(elements, new ArrayList<>())));
4342
else if (clazz.isAssignableFrom(Set.class))
44-
return Optional.of(clazz.cast(new HashSet<>(elements)));
45-
46-
if (clazz.isArray()) {
43+
return Optional.of(clazz.cast(iterableToCollection(elements, new HashSet<>())));
44+
else if (clazz.isArray()) {
4745
Class<?> componentType = clazz.getComponentType();
48-
49-
if (!componentType.isPrimitive()) {
50-
Object[] typedArray = (Object[]) Array.newInstance(componentType, 0);
51-
return Optional.of(clazz.cast(elements.toArray(typedArray)));
52-
}
53-
54-
Object primitiveArray = Array.newInstance(componentType, elements.size());
55-
46+
Collection<?> collection = iterableToCollection(elements);
47+
Object primitiveArray = Array.newInstance(componentType, collection.size());
5648
int i = 0;
57-
for (Object item : elements)
58-
Array.set(primitiveArray, i++, primitiveConverter.apply(item, componentType));
59-
49+
for (Object item : collection) {
50+
Array.set(
51+
primitiveArray,
52+
i++,
53+
convert(item, componentType, converter)
54+
.orElseThrow(
55+
() ->
56+
new IllegalArgumentException(
57+
"Cannot convert " + item + " into class " + componentType)));
58+
}
6059
return Optional.of(clazz.cast(primitiveArray));
60+
} else {
61+
Iterator<?> iter = elements.iterator();
62+
return iter.hasNext() ? convert(iter.next(), clazz, converter) : Optional.empty();
6163
}
64+
}
65+
66+
private static <T> Optional<T> convert(
67+
Object obj, Class<T> clazz, BiFunction<Object, Class<?>, Object> converter) {
68+
if (obj instanceof WorkflowModel model) {
69+
return model.as(clazz);
70+
} else {
71+
Object converted = converter.apply(obj, clazz);
72+
return clazz.isInstance(converted) ? Optional.of(clazz.cast(converted)) : Optional.empty();
73+
}
74+
}
75+
76+
private static <T> Collection<T> iterableToCollection(Iterable<T> t, Collection<T> c) {
77+
t.forEach(c::add);
78+
return c;
79+
}
6280

63-
return Optional.empty();
81+
private static <T> Collection<T> iterableToCollection(Iterable<T> t) {
82+
return t instanceof Collection col ? col : iterableToCollection(t, new ArrayList<>());
6483
}
6584

6685
/**

impl/model/src/main/java/io/serverlessworkflow/impl/model/jackson/JacksonModelCollection.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ public <T> Optional<T> as(Class<T> clazz) {
5151
if (clazz.isInstance(node)) return Optional.of(clazz.cast(node));
5252
if (clazz.isInstance(this)) return Optional.of(clazz.cast(this));
5353

54-
List<JsonNode> elements = new ArrayList<>(node.size());
55-
node.forEach(elements::add);
56-
57-
return CollectionConversionUtils.as(elements, clazz, JsonUtils::convertValue);
54+
return CollectionConversionUtils.as(node, clazz, JsonUtils::convertValue);
5855
}
5956

6057
@Override

0 commit comments

Comments
 (0)