Skip to content

Commit 7e4ef4e

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

4 files changed

Lines changed: 165 additions & 60 deletions

File tree

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

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
import io.cloudevents.core.builder.CloudEventBuilder;
3333
import io.serverlessworkflow.api.types.Workflow;
3434
import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder;
35+
import io.serverlessworkflow.fluent.func.dsl.ListenStep;
3536
import io.serverlessworkflow.impl.TaskContextData;
3637
import io.serverlessworkflow.impl.WorkflowApplication;
3738
import io.serverlessworkflow.impl.WorkflowContextData;
3839
import io.serverlessworkflow.impl.WorkflowDefinition;
3940
import io.serverlessworkflow.impl.WorkflowInstance;
4041
import io.serverlessworkflow.impl.WorkflowModel;
42+
import io.serverlessworkflow.impl.WorkflowModelCollection;
4143
import io.serverlessworkflow.impl.WorkflowStatus;
4244
import io.serverlessworkflow.impl.events.EventPublisher;
4345
import java.net.URI;
@@ -107,6 +109,36 @@ void testListenToOneArray() {
107109
.build());
108110
}
109111

112+
@Test
113+
void testPrimitiveArray() {
114+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
115+
Workflow workflow =
116+
FuncWorkflowBuilder.workflow("doubleArray")
117+
.tasks(function(FuncEventFilterTest::doubleArray))
118+
.build();
119+
WorkflowModelCollection col = app.modelFactory().createCollection();
120+
col.add(app.modelFactory().from(1));
121+
col.add(app.modelFactory().from(2));
122+
col.add(app.modelFactory().from(3));
123+
assertThat(
124+
app.workflowDefinition(workflow)
125+
.instance(col)
126+
.start()
127+
.join()
128+
.as(int[].class)
129+
.orElseThrow())
130+
.isEqualTo(new int[] {2, 4, 6});
131+
}
132+
}
133+
134+
private static int[] doubleArray(int[] input) {
135+
int[] output = new int[input.length];
136+
for (int i = 0; i < input.length; i++) {
137+
output[i] = input[i] << 1;
138+
}
139+
return output;
140+
}
141+
110142
private Workflow reviewEmitter() {
111143
return FuncWorkflowBuilder.workflow("emitReview")
112144
.tasks(emitJson("draftReady", "org.acme.test.review", Review.class))
@@ -141,42 +173,63 @@ void sendEmail(NewsletterDraft draft) {
141173
}
142174

143175
@Test
144-
void testJacksonAutomagicalConversion() throws Exception {
145-
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
176+
void testAutomaticConversion() throws Exception {
177+
testConversionWorkflow(
178+
listen(
179+
"waitHumanReview",
180+
to().one(
181+
consumed("org.acme.newsletter.review.done")
182+
.extensionByInstanceId("instanceid"))));
183+
}
146184

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();
185+
@Test
186+
void testCollectionConversion() throws Exception {
187+
testConversionWorkflow(
188+
listen(
189+
to().one(
190+
consumed("org.acme.newsletter.review.done")
191+
.extensionByInstanceId("instanceid")))
192+
.outputAs((Collection<?> col) -> col.iterator().next()));
193+
}
179194

195+
@Test
196+
void testNodeConversion() throws Exception {
197+
testConversionWorkflow(
198+
listen(
199+
"waitHumanReview",
200+
to().one(
201+
consumed("org.acme.newsletter.review.done")
202+
.extensionByInstanceId("instanceid")))
203+
.outputAs((ArrayNode col) -> col.get(0)));
204+
}
205+
206+
private void testConversionWorkflow(ListenStep listen) throws Exception {
207+
Workflow workflow =
208+
FuncWorkflowBuilder.workflow("intelligent-newsletter")
209+
.tasks(
210+
function("draftAgent", this::writeDraft).exportAsTaskOutput(),
211+
emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class),
212+
listen,
213+
switchWhenOrElse(
214+
h -> HumanReview.NEEDS_REVISION.equals(h.status()),
215+
"humanEditorAgent",
216+
"sendNewsletter",
217+
HumanReview.class),
218+
function("humanEditorAgent", this::editDraft)
219+
.exportAsTaskOutput()
220+
.then("draftReady"),
221+
consume("sendNewsletter", this::sendEmail)
222+
// Because we are in Jackson, the payload at this evaluation stage can be a
223+
// Map.
224+
// We simply check for the "status" field to know if it's the review payload.
225+
.inputFrom(
226+
(Map<String, Object> payload,
227+
WorkflowContextData wfc,
228+
TaskContextData tfc) ->
229+
payload.containsKey("status") ? wfc.context() : payload))
230+
.build();
231+
232+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
180233
WorkflowDefinition definition = app.workflowDefinition(workflow);
181234
WorkflowInstance instance = definition.instance(new NewsletterRequest("Tech Stocks"));
182235
CompletableFuture<WorkflowModel> future = instance.start();

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

Lines changed: 44 additions & 21 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,61 @@ 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());
46+
Collection<?> collection = iterableToCollection(elements);
47+
Object primitiveArray = Array.newInstance(componentType, collection.size());
5548

5649
int i = 0;
57-
for (Object item : elements)
58-
Array.set(primitiveArray, i++, primitiveConverter.apply(item, componentType));
59-
50+
for (Object item : collection) {
51+
Array.set(
52+
primitiveArray,
53+
i++,
54+
convert(item, componentType, converter)
55+
.orElseThrow(
56+
() ->
57+
new IllegalArgumentException(
58+
"Cannot convert " + item + " into class " + componentType)));
59+
}
6060
return Optional.of(clazz.cast(primitiveArray));
61+
} else {
62+
Iterator<?> iter = elements.iterator();
63+
return iter.hasNext() ? convert(iter.next(), clazz, converter) : Optional.empty();
6164
}
65+
}
66+
67+
private static <T> Optional<T> convert(
68+
Object obj, Class<T> clazz, BiFunction<Object, Class<?>, Object> converter) {
69+
if (obj instanceof WorkflowModel model) {
70+
return model.as(clazz);
71+
} else {
72+
Object converted = converter.apply(obj, clazz);
73+
if (clazz.isPrimitive()) {
74+
return (Optional<T>) Optional.of(converted);
75+
}
76+
return clazz.isInstance(converted) ? Optional.of(clazz.cast(converted)) : Optional.empty();
77+
}
78+
}
79+
80+
private static <T> Collection<T> iterableToCollection(Iterable<T> t, Collection<T> c) {
81+
t.forEach(c::add);
82+
return c;
83+
}
6284

63-
return Optional.empty();
85+
private static <T> Collection<T> iterableToCollection(Iterable<T> t) {
86+
return t instanceof Collection col ? col : iterableToCollection(t, new ArrayList<>());
6487
}
6588

6689
/**

impl/json-utils/src/main/java/io/serverlessworkflow/impl/jackson/JsonUtils.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ public static <T> T convertValue(JsonNode jsonNode, Class<T> returnType) {
196196
obj = jsonNode.asBoolean();
197197
} else if (Integer.class.isAssignableFrom(returnType)) {
198198
obj = jsonNode.asInt();
199-
} else if (Double.class.isAssignableFrom(returnType)) {
199+
} else if (Float.class.isAssignableFrom(returnType)
200+
|| Double.class.isAssignableFrom(returnType)) {
200201
obj = jsonNode.asDouble();
201202
} else if (Long.class.isAssignableFrom(returnType)) {
202203
obj = jsonNode.asLong();
@@ -206,12 +207,43 @@ public static <T> T convertValue(JsonNode jsonNode, Class<T> returnType) {
206207
obj = JacksonCloudEventUtils.toCloudEvent(jsonNode);
207208
} else if (CloudEventData.class.isAssignableFrom(returnType)) {
208209
obj = JacksonCloudEventUtils.toCloudEventData(jsonNode);
210+
} else if (returnType.isPrimitive()) {
211+
return (T) convertPrimitive(jsonNode, returnType);
212+
} else if (Short.class.isAssignableFrom(returnType)) {
213+
obj = Short.valueOf((short) jsonNode.asInt());
214+
} else if (Float.class.isAssignableFrom(returnType)) {
215+
obj = Float.valueOf((float) jsonNode.asDouble());
216+
} else if (Character.class.isAssignableFrom(returnType)) {
217+
obj = Character.valueOf((char) jsonNode.asInt());
218+
} else if (Byte.class.isAssignableFrom(returnType)) {
219+
obj = Byte.valueOf((byte) jsonNode.asInt());
209220
} else {
210221
obj = mapper().convertValue(jsonNode, returnType);
211222
}
212223
return returnType.cast(obj);
213224
}
214225

226+
private static Object convertPrimitive(JsonNode jsonNode, Class<?> returnType) {
227+
if (boolean.class.equals(returnType)) {
228+
return jsonNode.asBoolean();
229+
} else if (int.class.isAssignableFrom(returnType)) {
230+
return jsonNode.asInt();
231+
} else if (double.class.isAssignableFrom(returnType)) {
232+
return jsonNode.asDouble();
233+
} else if (long.class.isAssignableFrom(returnType)) {
234+
return jsonNode.asLong();
235+
} else if (short.class.isAssignableFrom(returnType)) {
236+
return (short) jsonNode.asInt();
237+
} else if (float.class.isAssignableFrom(returnType)) {
238+
return (float) jsonNode.asDouble();
239+
} else if (char.class.isAssignableFrom(returnType)) {
240+
return (char) jsonNode.asInt();
241+
} else if (byte.class.isAssignableFrom(returnType)) {
242+
return (byte) jsonNode.asInt();
243+
}
244+
throw new IllegalStateException("There is a unknown primitive!!!" + returnType);
245+
}
246+
215247
public static Object simpleToJavaValue(JsonNode jsonNode) {
216248
return internalToJavaValue(jsonNode, node -> node, node -> node);
217249
}

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)