diff --git a/api/src/main/java/io/serverlessworkflow/api/deserializers/ContinueAsDeserializer.java b/api/src/main/java/io/serverlessworkflow/api/deserializers/ContinueAsDeserializer.java new file mode 100644 index 00000000..b25b2b85 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/api/deserializers/ContinueAsDeserializer.java @@ -0,0 +1,85 @@ +/* + * 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.api.deserializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import io.serverlessworkflow.api.end.ContinueAs; +import io.serverlessworkflow.api.interfaces.WorkflowPropertySource; +import io.serverlessworkflow.api.timeouts.WorkflowExecTimeout; + +import java.io.IOException; + +public class ContinueAsDeserializer extends StdDeserializer { + + private static final long serialVersionUID = 510l; + + @SuppressWarnings("unused") + private WorkflowPropertySource context; + + public ContinueAsDeserializer() { + this(ContinueAs.class); + } + + public ContinueAsDeserializer(Class vc) { + super(vc); + } + + public ContinueAsDeserializer(WorkflowPropertySource context) { + this(ContinueAs.class); + this.context = context; + } + + @Override + public ContinueAs deserialize(JsonParser jp, + DeserializationContext ctxt) throws IOException { + + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode node = jp.getCodec().readTree(jp); + + ContinueAs continueAs = new ContinueAs(); + + if (!node.isObject()) { + continueAs.setWorkflowId(node.asText()); + continueAs.setVersion(null); + continueAs.setData(null); + continueAs.setWorkflowExecTimeout(null); + return continueAs; + } else { + if (node.get("workflowId") != null) { + continueAs.setWorkflowId(node.get("workflowId").asText()); + } + + if (node.get("version") != null) { + continueAs.setVersion(node.get("version").asText()); + } + + if (node.get("data") != null) { + continueAs.setData(node.get("data").asText()); + } + + if (node.get("workflowExecTimeout") != null) { + continueAs.setWorkflowExecTimeout(mapper.treeToValue(node.get("workflowExecTimeout"), WorkflowExecTimeout.class)); + } + + return continueAs; + } + } +} + diff --git a/api/src/main/java/io/serverlessworkflow/api/deserializers/EndDefinitionDeserializer.java b/api/src/main/java/io/serverlessworkflow/api/deserializers/EndDefinitionDeserializer.java index 2691acac..2bc33d1b 100644 --- a/api/src/main/java/io/serverlessworkflow/api/deserializers/EndDefinitionDeserializer.java +++ b/api/src/main/java/io/serverlessworkflow/api/deserializers/EndDefinitionDeserializer.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import io.serverlessworkflow.api.end.ContinueAs; import io.serverlessworkflow.api.end.End; import io.serverlessworkflow.api.interfaces.WorkflowPropertySource; import io.serverlessworkflow.api.produce.ProduceEvent; @@ -62,6 +63,7 @@ public End deserialize(JsonParser jp, end.setProduceEvents(null); end.setCompensate(false); end.setTerminate(false); + end.setContinueAs(null); return node.asBoolean() ? end : null; } else { if (node.get("produceEvents") != null) { @@ -84,6 +86,10 @@ public End deserialize(JsonParser jp, end.setCompensate(false); } + if(node.get("continueAs") != null) { + end.setContinueAs(mapper.treeToValue(node.get("continueAs"), ContinueAs.class)); + } + return end; } diff --git a/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java b/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java index d5d0e1bd..590d75f8 100644 --- a/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java +++ b/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java @@ -20,6 +20,7 @@ import io.serverlessworkflow.api.cron.Cron; import io.serverlessworkflow.api.datainputschema.DataInputSchema; import io.serverlessworkflow.api.deserializers.*; +import io.serverlessworkflow.api.end.ContinueAs; import io.serverlessworkflow.api.end.End; import io.serverlessworkflow.api.events.EventDefinition; import io.serverlessworkflow.api.events.OnEvents; @@ -79,6 +80,7 @@ private void addDefaultSerializers() { addSerializer(new SubFlowRefSerializer()); addSerializer(new AuthDefinitionSerializer()); addSerializer(new StateExecTimeoutSerializer()); + addSerializer(new ContinueAsSerializer()); addSerializer(extensionSerializer); } @@ -113,6 +115,7 @@ private void addDefaultDeserializers() { addDeserializer(AuthDefinition.class, new AuthDefinitionDeserializer(workflowPropertySource)); addDeserializer(StateExecTimeout.class, new StateExecTimeoutDeserializer(workflowPropertySource)); addDeserializer(Errors.class, new ErrorsDeserializer(workflowPropertySource)); + addDeserializer(ContinueAs.class, new ContinueAsDeserializer(workflowPropertySource)); } public ExtensionSerializer getExtensionSerializer() { diff --git a/api/src/main/java/io/serverlessworkflow/api/serializers/ContinueAsSerializer.java b/api/src/main/java/io/serverlessworkflow/api/serializers/ContinueAsSerializer.java new file mode 100644 index 00000000..7d74c508 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/api/serializers/ContinueAsSerializer.java @@ -0,0 +1,72 @@ +/* + * 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.api.serializers; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import io.serverlessworkflow.api.end.ContinueAs; + +import java.io.IOException; + +public class ContinueAsSerializer extends StdSerializer { + + public ContinueAsSerializer() { + this(ContinueAs.class); + } + + protected ContinueAsSerializer(Class t) { + super(t); + } + + @Override + public void serialize(ContinueAs continueAs, + JsonGenerator gen, + SerializerProvider provider) throws IOException { + + if (continueAs != null) { + if ((continueAs.getWorkflowId() != null && !continueAs.getWorkflowId().isEmpty()) + && (continueAs.getVersion() == null || continueAs.getVersion().isEmpty()) + && (continueAs.getData() == null || continueAs.getData().isEmpty()) + && continueAs.getWorkflowExecTimeout() == null ) { + gen.writeString(continueAs.getWorkflowId()); + } else { + gen.writeStartObject(); + + if (continueAs.getWorkflowId() != null && continueAs.getWorkflowId().length() > 0) { + gen.writeStringField("workflowId", continueAs.getWorkflowId()); + } + + if (continueAs.getVersion() != null && continueAs.getVersion().length() > 0) { + gen.writeStringField("version", continueAs.getVersion()); + } + + if (continueAs.getData() != null && continueAs.getData().length() > 0) { + gen.writeStringField("data", continueAs.getData()); + } + + if (continueAs.getWorkflowExecTimeout() != null) { + gen.writeObjectField("workflowExecTimeout", continueAs.getWorkflowExecTimeout()); + } + + + gen.writeEndObject(); + } + } + } +} + + diff --git a/api/src/main/java/io/serverlessworkflow/api/serializers/EndDefinitionSerializer.java b/api/src/main/java/io/serverlessworkflow/api/serializers/EndDefinitionSerializer.java index 76efe58a..36746bff 100644 --- a/api/src/main/java/io/serverlessworkflow/api/serializers/EndDefinitionSerializer.java +++ b/api/src/main/java/io/serverlessworkflow/api/serializers/EndDefinitionSerializer.java @@ -40,6 +40,7 @@ public void serialize(End end, if (end != null) { if ((end.getProduceEvents() == null || end.getProduceEvents().size() < 1) + && end.getContinueAs() == null && !end.isCompensate() && !end.isTerminate()) { gen.writeBoolean(true); } else { @@ -61,6 +62,10 @@ public void serialize(End end, gen.writeBooleanField("compensate", true); } + if(end.getContinueAs() != null) { + gen.writeObjectField("continueAs", end.getContinueAs()); + } + gen.writeEndObject(); } } diff --git a/api/src/main/java/io/serverlessworkflow/api/serializers/StateExecTimeoutSerializer.java b/api/src/main/java/io/serverlessworkflow/api/serializers/StateExecTimeoutSerializer.java index 7819af95..2ed5c7aa 100644 --- a/api/src/main/java/io/serverlessworkflow/api/serializers/StateExecTimeoutSerializer.java +++ b/api/src/main/java/io/serverlessworkflow/api/serializers/StateExecTimeoutSerializer.java @@ -38,7 +38,7 @@ public void serialize(StateExecTimeout stateExecTimeout, SerializerProvider provider) throws IOException { if (stateExecTimeout != null) { - if ((stateExecTimeout.getTotal() != null && stateExecTimeout.getTotal().isEmpty()) + if ((stateExecTimeout.getTotal() != null && !stateExecTimeout.getTotal().isEmpty()) && (stateExecTimeout.getSingle() == null || stateExecTimeout.getSingle().isEmpty())) { gen.writeString(stateExecTimeout.getTotal()); } else { diff --git a/api/src/main/resources/schema/end/continueas.json b/api/src/main/resources/schema/end/continueas.json new file mode 100644 index 00000000..94c10e86 --- /dev/null +++ b/api/src/main/resources/schema/end/continueas.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "javaType": "io.serverlessworkflow.api.end.ContinueAs", + "description": "End definition continue as", + "properties": { + "workflowId": { + "type": "string", + "description": "Unique id of the workflow to continue execution as" + }, + "version": { + "type": "string", + "description": "Version of the workflow to continue execution as", + "minLength": 1 + }, + "data": { + "type": [ + "string" + ], + "description": "Expression which selects parts of the states data output to become the workflow data input of continued execution" + }, + "workflowExecTimeout": { + "$ref": "../timeouts/workflowexectimeout.json" + } + }, + "required": [ + "kind" + ] +}s \ No newline at end of file diff --git a/api/src/main/resources/schema/end/end.json b/api/src/main/resources/schema/end/end.json index 7b959a2a..755ca929 100644 --- a/api/src/main/resources/schema/end/end.json +++ b/api/src/main/resources/schema/end/end.json @@ -20,6 +20,9 @@ "type": "boolean", "default": false, "description": "If set to true, triggers workflow compensation when before workflow executin completes. Default is false" + }, + "continueAs": { + "$ref": "continueas.json" } }, "required": [ diff --git a/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java b/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java index abb4ed7e..285d0b34 100644 --- a/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java @@ -22,6 +22,7 @@ import io.serverlessworkflow.api.branches.Branch; import io.serverlessworkflow.api.datainputschema.DataInputSchema; import io.serverlessworkflow.api.defaultdef.DefaultConditionDefinition; +import io.serverlessworkflow.api.end.End; import io.serverlessworkflow.api.functions.FunctionDefinition; import io.serverlessworkflow.api.functions.FunctionRef; import io.serverlessworkflow.api.functions.SubFlowRef; @@ -736,4 +737,52 @@ public void testErrorsParams(String workflowLocation) { assertNotNull(operationState.getOnErrors().get(0).getErrorRefs()); assertEquals(2, operationState.getOnErrors().get(0).getErrorRefs().size()); } + + @ParameterizedTest + @ValueSource(strings = {"/features/continueasstring.json", "/features/continueasstring.yml"}) + public void testContinueAsString(String workflowLocation) { + Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation)); + + assertNotNull(workflow); + assertNotNull(workflow.getId()); + assertNotNull(workflow.getName()); + assertNotNull(workflow.getStates()); + + assertNotNull(workflow.getStates()); + assertEquals(1, workflow.getStates().size()); + + OperationState operationState = (OperationState) workflow.getStates().get(0); + assertNotNull(operationState.getEnd()); + End end = operationState.getEnd(); + assertNotNull(end.getContinueAs()); + assertNotNull(end.getContinueAs().getWorkflowId()); + assertEquals("myworkflowid", end.getContinueAs().getWorkflowId()); + + } + + @ParameterizedTest + @ValueSource(strings = {"/features/continueasobject.json", "/features/continueasobject.yml"}) + public void testContinueAsObject(String workflowLocation) { + Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation)); + + assertNotNull(workflow); + assertNotNull(workflow.getId()); + assertNotNull(workflow.getName()); + assertNotNull(workflow.getStates()); + + assertNotNull(workflow.getStates()); + assertEquals(1, workflow.getStates().size()); + + OperationState operationState = (OperationState) workflow.getStates().get(0); + assertNotNull(operationState.getEnd()); + End end = operationState.getEnd(); + assertNotNull(end.getContinueAs()); + assertNotNull(end.getContinueAs().getWorkflowId()); + assertEquals("myworkflowid", end.getContinueAs().getWorkflowId()); + assertEquals("1.0", end.getContinueAs().getVersion()); + assertEquals("${ .data }", end.getContinueAs().getData()); + assertNotNull(end.getContinueAs().getWorkflowExecTimeout()); + assertEquals("PT1M", end.getContinueAs().getWorkflowExecTimeout().getDuration()); + + } } diff --git a/api/src/test/resources/features/continueasobject.json b/api/src/test/resources/features/continueasobject.json new file mode 100644 index 00000000..fbf851c8 --- /dev/null +++ b/api/src/test/resources/features/continueasobject.json @@ -0,0 +1,48 @@ +{ + "id": "functionrefs", + "version": "1.0", + "specVersion": "0.7", + "name": "Customer Credit Check Workflow", + "description": "Perform Customer Credit Check", + "start": "TestFunctionRef", + "functions": [ + { + "name": "creditCheckFunction", + "operation": "http://myapis.org/creditcheckapi.json#doCreditCheck" + }, + { + "name": "sendRejectionEmailFunction", + "operation": "http://myapis.org/creditcheckapi.json#rejectionEmail" + } + ], + "states": [ + { + "name": "TestFunctionRefs", + "type": "operation", + "actionMode": "sequential", + "actions": [ + { + "functionRef": "creditCheckFunction" + }, + { + "functionRef": { + "refName": "sendRejectionEmailFunction", + "arguments": { + "applicant": "${ .customer }" + } + } + } + ], + "end": { + "continueAs": { + "workflowId": "myworkflowid", + "version": "1.0", + "data": "${ .data }", + "workflowExecTimeout": { + "duration": "PT1M" + } + } + } + } + ] +} \ No newline at end of file diff --git a/api/src/test/resources/features/continueasobject.yml b/api/src/test/resources/features/continueasobject.yml new file mode 100644 index 00000000..8b5c347e --- /dev/null +++ b/api/src/test/resources/features/continueasobject.yml @@ -0,0 +1,28 @@ +id: functionrefs +version: '1.0' +specVersion: '0.7' +name: Customer Credit Check Workflow +description: Perform Customer Credit Check +start: TestFunctionRef +functions: + - name: creditCheckFunction + operation: http://myapis.org/creditcheckapi.json#doCreditCheck + - name: sendRejectionEmailFunction + operation: http://myapis.org/creditcheckapi.json#rejectionEmail +states: + - name: TestFunctionRefs + type: operation + actionMode: sequential + actions: + - functionRef: creditCheckFunction + - functionRef: + refName: sendRejectionEmailFunction + arguments: + applicant: "${ .customer }" + end: + continueAs: + workflowId: myworkflowid + version: '1.0' + data: "${ .data }" + workflowExecTimeout: + duration: PT1M diff --git a/api/src/test/resources/features/continueasstring.json b/api/src/test/resources/features/continueasstring.json new file mode 100644 index 00000000..62ff5f51 --- /dev/null +++ b/api/src/test/resources/features/continueasstring.json @@ -0,0 +1,41 @@ +{ + "id": "functionrefs", + "version": "1.0", + "specVersion": "0.7", + "name": "Customer Credit Check Workflow", + "description": "Perform Customer Credit Check", + "start": "TestFunctionRef", + "functions": [ + { + "name": "creditCheckFunction", + "operation": "http://myapis.org/creditcheckapi.json#doCreditCheck" + }, + { + "name": "sendRejectionEmailFunction", + "operation": "http://myapis.org/creditcheckapi.json#rejectionEmail" + } + ], + "states": [ + { + "name": "TestFunctionRefs", + "type": "operation", + "actionMode": "sequential", + "actions": [ + { + "functionRef": "creditCheckFunction" + }, + { + "functionRef": { + "refName": "sendRejectionEmailFunction", + "arguments": { + "applicant": "${ .customer }" + } + } + } + ], + "end": { + "continueAs": "myworkflowid" + } + } + ] +} \ No newline at end of file diff --git a/api/src/test/resources/features/continueasstring.yml b/api/src/test/resources/features/continueasstring.yml new file mode 100644 index 00000000..1fbd101c --- /dev/null +++ b/api/src/test/resources/features/continueasstring.yml @@ -0,0 +1,23 @@ +id: functionrefs +version: '1.0' +specVersion: '0.7' +name: Customer Credit Check Workflow +description: Perform Customer Credit Check +start: TestFunctionRef +functions: + - name: creditCheckFunction + operation: http://myapis.org/creditcheckapi.json#doCreditCheck + - name: sendRejectionEmailFunction + operation: http://myapis.org/creditcheckapi.json#rejectionEmail +states: + - name: TestFunctionRefs + type: operation + actionMode: sequential + actions: + - functionRef: creditCheckFunction + - functionRef: + refName: sendRejectionEmailFunction + arguments: + applicant: "${ .customer }" + end: + continueAs: myworkflowid