diff --git a/pkg/bpmn_engine/conditions.go b/pkg/bpmn_engine/conditions.go index 3995c8cc..8a22c35d 100644 --- a/pkg/bpmn_engine/conditions.go +++ b/pkg/bpmn_engine/conditions.go @@ -29,6 +29,28 @@ func exclusivelyFilterByConditionExpression(flows []BPMN20.TSequenceFlow, variab return ret, nil } +func inclusivelyFilterByConditionExpression(flows []BPMN20.TSequenceFlow, variableContext map[string]interface{}) ([]BPMN20.TSequenceFlow, error) { + var ret []BPMN20.TSequenceFlow + for _, flow := range flows { + if flow.HasConditionExpression() { + expression := flow.GetConditionExpression() + out, err := evaluateExpression(expression, variableContext) + if err != nil { + return nil, &ExpressionEvaluationError{ + Msg: fmt.Sprintf("Error evaluating expression in flow element id='%s' name='%s'", flow.Id, flow.Name), + Err: err, + } + } + if out == true { + ret = append(ret, flow) + return ret, nil + } + } + } + ret = append(ret, findDefaultFlow(flows)...) + return ret, nil +} + func findDefaultFlow(flows []BPMN20.TSequenceFlow) (ret []BPMN20.TSequenceFlow) { for _, flow := range flows { if !flow.HasConditionExpression() { diff --git a/pkg/bpmn_engine/conditions_test.go b/pkg/bpmn_engine/conditions_test.go index f23ad3c8..9ab0cc89 100644 --- a/pkg/bpmn_engine/conditions_test.go +++ b/pkg/bpmn_engine/conditions_test.go @@ -101,3 +101,45 @@ func Test_evaluation_error_percolates_up(t *testing.T) { then.AssertThat(t, err, is.Not(is.Nil())) then.AssertThat(t, err.Error(), has.Prefix("Error evaluating expression in flow element id=")) } + +func Test_inclusive_gateway_with_expressions_selects_one_and_not_the_other(t *testing.T) { + // setup + bpmnEngine := New() + cp := CallPath{} + + // given + process, _ := bpmnEngine.LoadFromFile("../../test-cases/inclusive-gateway-with-condition.bpmn") + bpmnEngine.NewTaskHandler().Id("task-a").Handler(cp.TaskHandler) + bpmnEngine.NewTaskHandler().Id("task-b").Handler(cp.TaskHandler) + variables := map[string]interface{}{ + "price": -50, + } + + // when + _, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, variables) + then.AssertThat(t, err, is.Nil()) + + // then + then.AssertThat(t, cp.CallPath, is.EqualTo("task-b")) +} + +func Test_inclusive_gateway_with_expressions_selects_default(t *testing.T) { + // setup + bpmnEngine := New() + cp := CallPath{} + + // given + process, _ := bpmnEngine.LoadFromFile("../../test-cases/inclusive-gateway-with-condition-and-default.bpmn") + bpmnEngine.NewTaskHandler().Id("task-a").Handler(cp.TaskHandler) + bpmnEngine.NewTaskHandler().Id("task-b").Handler(cp.TaskHandler) + variables := map[string]interface{}{ + "price": -1, + } + + // when + _, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, variables) + then.AssertThat(t, err, is.Nil()) + + // then + then.AssertThat(t, cp.CallPath, is.EqualTo("task-b")) +} diff --git a/pkg/bpmn_engine/engine.go b/pkg/bpmn_engine/engine.go index 114a1b74..609e9a49 100644 --- a/pkg/bpmn_engine/engine.go +++ b/pkg/bpmn_engine/engine.go @@ -291,6 +291,13 @@ func (state *BpmnEngineState) handleElement(process *ProcessInfo, instance *proc } instance.appendActivity(activity) createFlowTransitions = true + case BPMN20.InclusiveGateway: + activity = elementActivity{ + key: state.generateKey(), + state: Active, + element: element, + } + createFlowTransitions = true default: panic(fmt.Sprintf("[invariant check] unsupported element: id=%s, type=%s", (*element).GetId(), (*element).GetType())) } @@ -313,7 +320,8 @@ func createCheckExclusiveGatewayDoneCommand(originActivity activity) (cmds []com func createNextCommands(process *ProcessInfo, instance *processInstanceInfo, element *BPMN20.BaseElement, activity activity) (cmds []command) { nextFlows := BPMN20.FindSequenceFlows(&process.definitions.Process.SequenceFlows, (*element).GetOutgoingAssociation()) var err error - if (*element).GetType() == BPMN20.ExclusiveGateway { + switch (*element).GetType() { + case BPMN20.ExclusiveGateway: nextFlows, err = exclusivelyFilterByConditionExpression(nextFlows, instance.VariableHolder.Variables()) if err != nil { instance.State = Failed @@ -324,6 +332,18 @@ func createNextCommands(process *ProcessInfo, instance *processInstanceInfo, ele }) return cmds } + case BPMN20.InclusiveGateway: + nextFlows, err = inclusivelyFilterByConditionExpression(nextFlows, instance.VariableHolder.Variables()) + if err != nil { + instance.State = Failed + return []command{ + errorCommand{ + elementId: (*element).GetId(), + elementName: (*element).GetName(), + err: err, + }, + } + } } for _, flow := range nextFlows { cmds = append(cmds, flowTransitionCommand{ diff --git a/pkg/spec/BPMN20/bpmn_structs.go b/pkg/spec/BPMN20/bpmn_structs.go index 865c9040..1a02f664 100644 --- a/pkg/spec/BPMN20/bpmn_structs.go +++ b/pkg/spec/BPMN20/bpmn_structs.go @@ -31,6 +31,7 @@ type TProcess struct { IntermediateCatchEvent []TIntermediateCatchEvent `xml:"intermediateCatchEvent"` IntermediateTrowEvent []TIntermediateThrowEvent `xml:"intermediateThrowEvent"` EventBasedGateway []TEventBasedGateway `xml:"eventBasedGateway"` + InclusiveGateway []TInclusiveGateway `xml:"inclusiveGateway"` } type TSequenceFlow struct { @@ -150,3 +151,10 @@ type TMessage struct { type TTimeDuration struct { XMLText string `xml:",innerxml"` } + +type TInclusiveGateway struct { + Id string `xml:"id,attr"` + Name string `xml:"name,attr"` + IncomingAssociation []string `xml:"incoming"` + OutgoingAssociation []string `xml:"outgoing"` +} diff --git a/pkg/spec/BPMN20/elements.go b/pkg/spec/BPMN20/elements.go index 87169105..8dd91156 100644 --- a/pkg/spec/BPMN20/elements.go +++ b/pkg/spec/BPMN20/elements.go @@ -15,6 +15,7 @@ const ( IntermediateCatchEvent ElementType = "INTERMEDIATE_CATCH_EVENT" IntermediateThrowEvent ElementType = "INTERMEDIATE_THROW_EVENT" EventBasedGateway ElementType = "EVENT_BASED_GATEWAY" + InclusiveGateway ElementType = "INCLUSIVE_GATEWAY" SequenceFlow ElementType = "SEQUENCE_FLOW" @@ -45,6 +46,7 @@ type GatewayElement interface { BaseElement IsParallel() bool IsExclusive() bool + IsInclusive() bool } func (startEvent TStartEvent) GetId() string { @@ -194,6 +196,10 @@ func (parallelGateway TParallelGateway) IsExclusive() bool { return false } +func (parallelGateway TParallelGateway) IsInclusive() bool { + return false +} + func (exclusiveGateway TExclusiveGateway) GetId() string { return exclusiveGateway.Id } @@ -221,6 +227,10 @@ func (exclusiveGateway TExclusiveGateway) IsExclusive() bool { return true } +func (exclusiveGateway TExclusiveGateway) IsInclusive() bool { + return false +} + func (intermediateCatchEvent TIntermediateCatchEvent) GetId() string { return intermediateCatchEvent.Id } @@ -271,6 +281,10 @@ func (eventBasedGateway TEventBasedGateway) IsExclusive() bool { return true } +func (eventBasedGateway TEventBasedGateway) IsInclusive() bool { + return false +} + // ------------------------------------------------------------------------- func (intermediateThrowEvent TIntermediateThrowEvent) GetId() string { @@ -293,3 +307,35 @@ func (intermediateThrowEvent TIntermediateThrowEvent) GetOutgoingAssociation() [ func (intermediateThrowEvent TIntermediateThrowEvent) GetType() ElementType { return IntermediateThrowEvent } + +func (inclusiveGateway TInclusiveGateway) GetId() string { + return inclusiveGateway.Id +} + +func (inclusiveGateway TInclusiveGateway) GetName() string { + return inclusiveGateway.Name +} + +func (inclusiveGateway TInclusiveGateway) GetIncomingAssociation() []string { + return inclusiveGateway.IncomingAssociation +} + +func (inclusiveGateway TInclusiveGateway) GetOutgoingAssociation() []string { + return inclusiveGateway.OutgoingAssociation +} + +func (inclusiveGateway TInclusiveGateway) GetType() ElementType { + return InclusiveGateway +} + +func (inclusiveGateway TInclusiveGateway) IsParallel() bool { + return false +} + +func (inclusiveGateway TInclusiveGateway) IsExclusive() bool { + return false +} + +func (inclusiveGateway TInclusiveGateway) IsInclusive() bool { + return true +} diff --git a/pkg/spec/BPMN20/elements_test.go b/pkg/spec/BPMN20/elements_test.go index 38b6085b..e3b1e08e 100644 --- a/pkg/spec/BPMN20/elements_test.go +++ b/pkg/spec/BPMN20/elements_test.go @@ -17,4 +17,5 @@ func Test_all_interfaces_implemented(t *testing.T) { var _ BaseElement = &TIntermediateCatchEvent{} var _ BaseElement = &TIntermediateThrowEvent{} var _ BaseElement = &TEventBasedGateway{} + var _ BaseElement = &TInclusiveGateway{} } diff --git a/pkg/spec/BPMN20/helper.go b/pkg/spec/BPMN20/helper.go index 8e576274..bacf503d 100644 --- a/pkg/spec/BPMN20/helper.go +++ b/pkg/spec/BPMN20/helper.go @@ -69,6 +69,10 @@ func FindBaseElementsById(definitions *TDefinitions, id string) (elements []*Bas var be BaseElement = intermediateCatchEvent appender(&be) } + for _, inclusiveGateway := range definitions.Process.InclusiveGateway { + var be BaseElement = inclusiveGateway + appender(&be) + } return elements } diff --git a/test-cases/inclusive-gateway-with-condition-and-default.bpmn b/test-cases/inclusive-gateway-with-condition-and-default.bpmn new file mode 100644 index 00000000..94bd92cd --- /dev/null +++ b/test-cases/inclusive-gateway-with-condition-and-default.bpmn @@ -0,0 +1,90 @@ + + + + + Flow_1y8jegt + + + + price > 0 + + + + + + price-gt-zero + Flow_1moyr7v + + + + + + + default + Flow_1dekydz + + + Flow_1dekydz + + + + Flow_1moyr7v + + + + Flow_1y8jegt + price-gt-zero + default + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-cases/inclusive-gateway-with-condition.bpmn b/test-cases/inclusive-gateway-with-condition.bpmn new file mode 100644 index 00000000..5b414e58 --- /dev/null +++ b/test-cases/inclusive-gateway-with-condition.bpmn @@ -0,0 +1,92 @@ + + + + + Flow_1y8jegt + + + + price > 0 + + + + + + price-gt-zero + Flow_1moyr7v + + + price < 0 + + + + + + price-lt-zero + Flow_1dekydz + + + Flow_1dekydz + + + + Flow_1moyr7v + + + + Flow_1y8jegt + price-gt-zero + price-lt-zero + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +