diff --git a/api/src/main/java/io/serverlessworkflow/api/interfaces/State.java b/api/src/main/java/io/serverlessworkflow/api/interfaces/State.java index ec12b4ff..a8b0a519 100644 --- a/api/src/main/java/io/serverlessworkflow/api/interfaces/State.java +++ b/api/src/main/java/io/serverlessworkflow/api/interfaces/State.java @@ -48,5 +48,7 @@ public interface State { List getOnErrors(); + String getCompensatedBy(); + Map getMetadata(); } \ 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 9c1c5e35..ebf3ceed 100644 --- a/api/src/main/resources/schema/end/end.json +++ b/api/src/main/resources/schema/end/end.json @@ -20,6 +20,11 @@ "type": "object", "$ref": "../produce/produceevent.json" } + }, + "compensate": { + "type": "boolean", + "default": false, + "description": "If set to true, triggers workflow compensation when before workflow executin completes. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/states/callbackstate.json b/api/src/main/resources/schema/states/callbackstate.json index 599576a4..3cef4586 100644 --- a/api/src/main/resources/schema/states/callbackstate.json +++ b/api/src/main/resources/schema/states/callbackstate.json @@ -24,6 +24,11 @@ "eventDataFilter": { "description": "Callback event data filter definition", "$ref": "../filters/eventdatafilter.json" + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/states/defaultstate.json b/api/src/main/resources/schema/states/defaultstate.json index ec70d4a8..6d922e19 100644 --- a/api/src/main/resources/schema/states/defaultstate.json +++ b/api/src/main/resources/schema/states/defaultstate.json @@ -64,6 +64,11 @@ "type": "object", "$ref": "../error/error.json" } + }, + "compensatedBy": { + "type": "string", + "minLength": 1, + "description": "Unique Name of a workflow state which is responsible for compensation of this state" } }, "required": [ diff --git a/api/src/main/resources/schema/states/delaystate.json b/api/src/main/resources/schema/states/delaystate.json index 39e62518..3d0eab4c 100644 --- a/api/src/main/resources/schema/states/delaystate.json +++ b/api/src/main/resources/schema/states/delaystate.json @@ -12,6 +12,11 @@ "timeDelay": { "type": "string", "description": "Amount of time (ISO 8601 format) to delay" + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/states/foreachstate.json b/api/src/main/resources/schema/states/foreachstate.json index 1496c143..897dabe1 100644 --- a/api/src/main/resources/schema/states/foreachstate.json +++ b/api/src/main/resources/schema/states/foreachstate.json @@ -38,6 +38,11 @@ "workflowId": { "type": "string", "description": "Unique Id of a workflow to be executed for each of the elements of inputCollection" + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "oneOf": [ diff --git a/api/src/main/resources/schema/states/injectstate.json b/api/src/main/resources/schema/states/injectstate.json index 1101b6e1..d0d10589 100644 --- a/api/src/main/resources/schema/states/injectstate.json +++ b/api/src/main/resources/schema/states/injectstate.json @@ -13,6 +13,11 @@ "type": "object", "description": "JSON object which can be set as states data input and can be manipulated via filters", "existingJavaType": "com.fasterxml.jackson.databind.JsonNode" + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/states/operationstate.json b/api/src/main/resources/schema/states/operationstate.json index 1775a568..8d8211a9 100644 --- a/api/src/main/resources/schema/states/operationstate.json +++ b/api/src/main/resources/schema/states/operationstate.json @@ -24,6 +24,11 @@ "type": "object", "$ref": "../actions/action.json" } + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/states/parallelstate.json b/api/src/main/resources/schema/states/parallelstate.json index 2f82264a..824f3ff5 100644 --- a/api/src/main/resources/schema/states/parallelstate.json +++ b/api/src/main/resources/schema/states/parallelstate.json @@ -27,6 +27,11 @@ "type": "string", "default": "0", "description": "Used when completionType is set to 'n_of_m' to specify the 'N' value" + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/states/subflowstate.json b/api/src/main/resources/schema/states/subflowstate.json index 8c6da3b1..4a208691 100644 --- a/api/src/main/resources/schema/states/subflowstate.json +++ b/api/src/main/resources/schema/states/subflowstate.json @@ -17,6 +17,11 @@ "workflowId": { "type": "string", "description": "Sub-workflow unique id." + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/states/switchstate.json b/api/src/main/resources/schema/states/switchstate.json index 8ce641cb..e421397d 100644 --- a/api/src/main/resources/schema/states/switchstate.json +++ b/api/src/main/resources/schema/states/switchstate.json @@ -32,6 +32,11 @@ "default": { "description": "Default transition of the workflow if there is no matching data conditions. Can include a transition or end definition", "$ref": "../default/defaultdef.json" + }, + "usedForCompensation": { + "type": "boolean", + "default": false, + "description": "If true, this state is used to compensate another state. Default is false" } }, "required": [ diff --git a/api/src/main/resources/schema/transitions/transition.json b/api/src/main/resources/schema/transitions/transition.json index c3008fb9..5ba61b51 100644 --- a/api/src/main/resources/schema/transitions/transition.json +++ b/api/src/main/resources/schema/transitions/transition.json @@ -18,6 +18,11 @@ "type": "string", "description": "State to transition to next", "minLength": 1 + }, + "compensate": { + "type": "boolean", + "default": false, + "description": "If set to true, triggers workflow compensation before this transition is taken. Default is false" } }, "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 c4a0b36c..6d770361 100644 --- a/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java @@ -17,12 +17,14 @@ package io.serverlessworkflow.api.test; import io.serverlessworkflow.api.Workflow; +import io.serverlessworkflow.api.interfaces.State; +import io.serverlessworkflow.api.states.EventState; +import io.serverlessworkflow.api.states.OperationState; import io.serverlessworkflow.api.test.utils.WorkflowTestUtils; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class MarkupToWorkflowTest { @@ -74,7 +76,7 @@ public void testSpecFreatureFunctionRef(String workflowLocation) { } @ParameterizedTest - @ValueSource(strings = {"/features/vetappointment.json"}) + @ValueSource(strings = {"/features/vetappointment.json", "/features/vetappointment.yml"}) public void testSpecFreatureEventRef(String workflowLocation) { Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation)); @@ -90,4 +92,29 @@ public void testSpecFreatureEventRef(String workflowLocation) { assertNotNull(workflow.getRetries()); assertTrue(workflow.getRetries().getRetryDefs().size() == 1); } + + @ParameterizedTest + @ValueSource(strings = {"/features/compensationworkflow.json", "/features/compensationworkflow.yml"}) + public void testSpecFreatureCompensation(String workflowLocation) { + Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation)); + + assertNotNull(workflow); + assertNotNull(workflow.getId()); + assertNotNull(workflow.getName()); + assertNotNull(workflow.getStates()); + + assertNotNull(workflow.getStates()); + assertTrue(workflow.getStates().size() == 2); + + State firstState = workflow.getStates().get(0); + assertTrue(firstState instanceof EventState); + assertNotNull(firstState.getCompensatedBy()); + assertEquals("CancelPurchase", firstState.getCompensatedBy()); + + State secondState = workflow.getStates().get(1); + assertTrue(secondState instanceof OperationState); + OperationState operationState = (OperationState) secondState; + + assertTrue(operationState.isUsedForCompensation()); + } } diff --git a/api/src/test/resources/features/compensationworkflow.json b/api/src/test/resources/features/compensationworkflow.json new file mode 100644 index 00000000..5f979f68 --- /dev/null +++ b/api/src/test/resources/features/compensationworkflow.json @@ -0,0 +1,86 @@ +{ + "id": "CompensationWorkflow", + "name": "Compensation Workflow", + "version": "1.0", + "states": [ + { + "name": "NewItemPurchase", + "type": "event", + "onEvents": [ + { + "eventRefs": [ + "NewPurchase" + ], + "actions": [ + { + "functionRef": { + "refName": "DebitCustomerFunction", + "parameters": { + "customerid": "{{ $.purchase.customerid }}", + "amount": "{{ $.purchase.amount }}" + } + } + }, + { + "functionRef": { + "refName": "SendPurchaseConfirmationEmailFunction", + "parameters": { + "customerid": "{{ $.purchase.customerid }}" + } + } + } + ] + } + ], + "compensatedBy": "CancelPurchase", + "transition": { + "nextState": "SomeNextWorkflowState" + } + }, + { + "name": "CancelPurchase", + "type": "operation", + "usedForCompensation": true, + "actions": [ + { + "functionRef": { + "refName": "CreditCustomerFunction", + "parameters": { + "customerid": "{{ $.purchase.customerid }}", + "amount": "{{ $.purchase.amount }}" + } + } + }, + { + "functionRef": { + "refName": "SendPurchaseCancellationEmailFunction", + "parameters": { + "customerid": "{{ $.purchase.customerid }}" + } + } + } + ] + } + ], + "events": [ + { + "name": "NewItemPurchase", + "source": "purchasesource", + "type": "org.purchases" + } + ], + "functions": [ + { + "name": "DebitCustomerFunction", + "operation": "http://myapis.org/application.json#debit" + }, + { + "name": "SendPurchaseConfirmationEmailFunction", + "operation": "http://myapis.org/application.json#confirmationemail" + }, + { + "name": "SendPurchaseCancellationEmailFunction", + "operation": "http://myapis.org/application.json#cancellationemail" + } + ] +} \ No newline at end of file diff --git a/api/src/test/resources/features/compensationworkflow.yml b/api/src/test/resources/features/compensationworkflow.yml new file mode 100644 index 00000000..81506399 --- /dev/null +++ b/api/src/test/resources/features/compensationworkflow.yml @@ -0,0 +1,46 @@ +id: CompensationWorkflow +name: Compensation Workflow +version: '1.0' +states: + - name: NewItemPurchase + type: event + onEvents: + - eventRefs: + - NewPurchase + actions: + - functionRef: + refName: DebitCustomerFunction + parameters: + customerid: "{{ $.purchase.customerid }}" + amount: "{{ $.purchase.amount }}" + - functionRef: + refName: SendPurchaseConfirmationEmailFunction + parameters: + customerid: "{{ $.purchase.customerid }}" + compensatedBy: CancelPurchase + transition: + nextState: SomeNextWorkflowState + - name: CancelPurchase + type: operation + usedForCompensation: true + actions: + - functionRef: + refName: CreditCustomerFunction + parameters: + customerid: "{{ $.purchase.customerid }}" + amount: "{{ $.purchase.amount }}" + - functionRef: + refName: SendPurchaseCancellationEmailFunction + parameters: + customerid: "{{ $.purchase.customerid }}" +events: + - name: NewItemPurchase + source: purchasesource + type: org.purchases +functions: + - name: DebitCustomerFunction + operation: http://myapis.org/application.json#debit + - name: SendPurchaseConfirmationEmailFunction + operation: http://myapis.org/application.json#confirmationemail + - name: SendPurchaseCancellationEmailFunction + operation: http://myapis.org/application.json#cancellationemail