Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
import io.cloudevents.core.builder.CloudEventBuilder;
import io.serverlessworkflow.api.types.Workflow;
import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder;
import io.serverlessworkflow.fluent.func.dsl.ListenStep;
import io.serverlessworkflow.impl.TaskContextData;
import io.serverlessworkflow.impl.WorkflowApplication;
import io.serverlessworkflow.impl.WorkflowContextData;
import io.serverlessworkflow.impl.WorkflowDefinition;
import io.serverlessworkflow.impl.WorkflowInstance;
import io.serverlessworkflow.impl.WorkflowModel;
import io.serverlessworkflow.impl.WorkflowModelCollection;
import io.serverlessworkflow.impl.WorkflowStatus;
import io.serverlessworkflow.impl.events.EventPublisher;
import java.net.URI;
Expand Down Expand Up @@ -107,6 +109,36 @@ void testListenToOneArray() {
.build());
}

@Test
void testPrimitiveArray() {
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
Workflow workflow =
FuncWorkflowBuilder.workflow("doubleArray")
.tasks(function(FuncEventFilterTest::doubleArray))
.build();
WorkflowModelCollection col = app.modelFactory().createCollection();
col.add(app.modelFactory().from(1));
col.add(app.modelFactory().from(2));
col.add(app.modelFactory().from(3));
assertThat(
app.workflowDefinition(workflow)
.instance(col)
.start()
.join()
.as(int[].class)
.orElseThrow())
.isEqualTo(new int[] {2, 4, 6});
}
}

private static int[] doubleArray(int[] input) {
int[] output = new int[input.length];
for (int i = 0; i < input.length; i++) {
output[i] = input[i] << 1;
}
return output;
}

private Workflow reviewEmitter() {
return FuncWorkflowBuilder.workflow("emitReview")
.tasks(emitJson("draftReady", "org.acme.test.review", Review.class))
Expand Down Expand Up @@ -141,42 +173,63 @@ void sendEmail(NewsletterDraft draft) {
}

@Test
void testJacksonAutomagicalConversion() throws Exception {
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
void testAutomaticConversion() throws Exception {
testConversionWorkflow(
listen(
"waitHumanReview",
to().one(
consumed("org.acme.newsletter.review.done")
.extensionByInstanceId("instanceid"))));
}

Workflow workflow =
FuncWorkflowBuilder.workflow("intelligent-newsletter")
.tasks(
function("draftAgent", this::writeDraft).exportAsTaskOutput(),
emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class),
listen(
"waitHumanReview",
to().one(
consumed("org.acme.newsletter.review.done")
.extensionByInstanceId("instanceid")))
.outputAs((Collection<?> events) -> events.iterator().next()),
// The engine sees the incoming JsonNode, sees this task expects
// HumanReview.class,
// and natively deserializes it for you before executing the lambda!
switchWhenOrElse(
h -> HumanReview.NEEDS_REVISION.equals(h.status()),
"humanEditorAgent",
"sendNewsletter",
HumanReview.class),
function("humanEditorAgent", this::editDraft)
.exportAsTaskOutput()
.then("draftReady"),
consume("sendNewsletter", this::sendEmail)
// Because we are in Jackson, the payload at this evaluation stage can be a
// Map.
// We simply check for the "status" field to know if it's the review payload.
.inputFrom(
(Map<String, Object> payload,
WorkflowContextData wfc,
TaskContextData tfc) ->
payload.containsKey("status") ? wfc.context() : payload))
.build();
@Test
void testCollectionConversion() throws Exception {
testConversionWorkflow(
listen(
to().one(
consumed("org.acme.newsletter.review.done")
.extensionByInstanceId("instanceid")))
.outputAs((Collection<?> col) -> col.iterator().next()));
}

@Test
void testNodeConversion() throws Exception {
testConversionWorkflow(
listen(
"waitHumanReview",
to().one(
consumed("org.acme.newsletter.review.done")
.extensionByInstanceId("instanceid")))
.outputAs((ArrayNode col) -> col.get(0)));
}

private void testConversionWorkflow(ListenStep listen) throws Exception {
Workflow workflow =
FuncWorkflowBuilder.workflow("intelligent-newsletter")
.tasks(
function("draftAgent", this::writeDraft).exportAsTaskOutput(),
emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class),
listen,
switchWhenOrElse(
h -> HumanReview.NEEDS_REVISION.equals(h.status()),
"humanEditorAgent",
"sendNewsletter",
HumanReview.class),
function("humanEditorAgent", this::editDraft)
.exportAsTaskOutput()
.then("draftReady"),
consume("sendNewsletter", this::sendEmail)
// Because we are in Jackson, the payload at this evaluation stage can be a
// Map.
// We simply check for the "status" field to know if it's the review payload.
.inputFrom(
(Map<String, Object> payload,
WorkflowContextData wfc,
TaskContextData tfc) ->
payload.containsKey("status") ? wfc.context() : payload))
.build();

try (WorkflowApplication app = WorkflowApplication.builder().build()) {
WorkflowDefinition definition = app.workflowDefinition(workflow);
WorkflowInstance instance = definition.instance(new NewsletterRequest("Tech Stocks"));
CompletableFuture<WorkflowModel> future = instance.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
Expand All @@ -28,39 +29,61 @@ public final class CollectionConversionUtils {
private CollectionConversionUtils() {}

/**
* Safely converts a base Collection into the requested List, Set, or Array type.
* Safely converts an Iterable into the requested type.
*
* @param elements The base collection of elements.
* @param elements Iterable containing the elements to be converted.
* @param clazz The target class to convert to.
* @param primitiveConverter Strategy for converting items to primitives if an array is requested.
* @param converter Convert items to class if requested.
*/
Comment thread
fjtirado marked this conversation as resolved.
public static <T> Optional<T> as(
Collection<?> elements,
Class<T> clazz,
BiFunction<Object, Class<?>, Object> primitiveConverter) {
Iterable<?> elements, Class<T> clazz, BiFunction<Object, Class<?>, Object> converter) {
if (clazz.isAssignableFrom(List.class))
return Optional.of(clazz.cast(new ArrayList<>(elements)));
return Optional.of(clazz.cast(iterableToCollection(elements, new ArrayList<>())));
else if (clazz.isAssignableFrom(Set.class))
return Optional.of(clazz.cast(new HashSet<>(elements)));

if (clazz.isArray()) {
return Optional.of(clazz.cast(iterableToCollection(elements, new HashSet<>())));
else if (clazz.isArray()) {
Class<?> componentType = clazz.getComponentType();

if (!componentType.isPrimitive()) {
Object[] typedArray = (Object[]) Array.newInstance(componentType, 0);
return Optional.of(clazz.cast(elements.toArray(typedArray)));
}

Object primitiveArray = Array.newInstance(componentType, elements.size());
Collection<?> collection = iterableToCollection(elements);
Object primitiveArray = Array.newInstance(componentType, collection.size());

int i = 0;
for (Object item : elements)
Array.set(primitiveArray, i++, primitiveConverter.apply(item, componentType));

for (Object item : collection) {
Array.set(
primitiveArray,
i++,
convert(item, componentType, converter)
.orElseThrow(
() ->
new IllegalArgumentException(
"Cannot convert " + item + " into class " + componentType)));
}
Comment thread
fjtirado marked this conversation as resolved.
return Optional.of(clazz.cast(primitiveArray));
} else {
Iterator<?> iter = elements.iterator();
return iter.hasNext() ? convert(iter.next(), clazz, converter) : Optional.empty();
}
}

private static <T> Optional<T> convert(
Object obj, Class<T> clazz, BiFunction<Object, Class<?>, Object> converter) {
if (obj instanceof WorkflowModel model) {
return model.as(clazz);
} else {
Object converted = converter.apply(obj, clazz);
if (clazz.isPrimitive()) {
return (Optional<T>) Optional.of(converted);
}
return clazz.isInstance(converted) ? Optional.of(clazz.cast(converted)) : Optional.empty();
}
}

Comment thread
fjtirado marked this conversation as resolved.
private static <T> Collection<T> iterableToCollection(Iterable<T> t, Collection<T> c) {
t.forEach(c::add);
return c;
}

return Optional.empty();
private static <T> Collection<T> iterableToCollection(Iterable<T> t) {
return t instanceof Collection col ? col : iterableToCollection(t, new ArrayList<>());
Comment thread
fjtirado marked this conversation as resolved.
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,43 @@ public static <T> T convertValue(JsonNode jsonNode, Class<T> returnType) {
obj = JacksonCloudEventUtils.toCloudEvent(jsonNode);
} else if (CloudEventData.class.isAssignableFrom(returnType)) {
obj = JacksonCloudEventUtils.toCloudEventData(jsonNode);
} else if (returnType.isPrimitive()) {
return (T) convertPrimitive(jsonNode, returnType);
} else if (Short.class.isAssignableFrom(returnType)) {
obj = Short.valueOf((short) jsonNode.asInt());
} else if (Float.class.isAssignableFrom(returnType)) {
obj = Float.valueOf((float) jsonNode.asDouble());
} else if (Character.class.isAssignableFrom(returnType)) {
obj = Character.valueOf((char) jsonNode.asInt());
} else if (Byte.class.isAssignableFrom(returnType)) {
obj = Byte.valueOf((byte) jsonNode.asInt());
} else {
obj = mapper().convertValue(jsonNode, returnType);
}
return returnType.cast(obj);
}

private static Object convertPrimitive(JsonNode jsonNode, Class<?> returnType) {
if (boolean.class.equals(returnType)) {
return jsonNode.asBoolean();
} else if (int.class.isAssignableFrom(returnType)) {
return jsonNode.asInt();
} else if (double.class.isAssignableFrom(returnType)) {
return jsonNode.asDouble();
} else if (long.class.isAssignableFrom(returnType)) {
return jsonNode.asLong();
Comment thread
fjtirado marked this conversation as resolved.
} else if (short.class.isAssignableFrom(returnType)) {
return (short) jsonNode.asInt();
} else if (float.class.isAssignableFrom(returnType)) {
return (float) jsonNode.asDouble();
} else if (char.class.isAssignableFrom(returnType)) {
return (char) jsonNode.asInt();
} else if (byte.class.isAssignableFrom(returnType)) {
return (byte) jsonNode.asInt();
}
throw new IllegalStateException("There is a unknown primitive!!!" + returnType);
}

public static Object simpleToJavaValue(JsonNode jsonNode) {
return internalToJavaValue(jsonNode, node -> node, node -> node);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ public <T> Optional<T> as(Class<T> clazz) {
if (clazz.isInstance(node)) return Optional.of(clazz.cast(node));
if (clazz.isInstance(this)) return Optional.of(clazz.cast(this));

List<JsonNode> elements = new ArrayList<>(node.size());
node.forEach(elements::add);

return CollectionConversionUtils.as(elements, clazz, JsonUtils::convertValue);
return CollectionConversionUtils.as(node, clazz, JsonUtils::convertValue);
}

@Override
Expand Down
Loading