diff --git a/SpiffWorkflow/bpmn/serializer/dict.py b/SpiffWorkflow/bpmn/serializer/dict.py
index ca727c97..237b21a1 100644
--- a/SpiffWorkflow/bpmn/serializer/dict.py
+++ b/SpiffWorkflow/bpmn/serializer/dict.py
@@ -177,7 +177,7 @@ class BPMNDictionarySerializer(DictionarySerializer):
return s_state
def deserialize_business_rule_task(self, wf_spec, s_state):
- dt = DecisionTable(None,None)
+ dt = DecisionTable(None, None, None)
dt.deserialize(s_state['dmn'])
dmn_engine = DMNEngine(dt)
spec = BusinessRuleTask(wf_spec, s_state['name'], dmn_engine)
@@ -224,7 +224,7 @@ class BPMNDictionarySerializer(DictionarySerializer):
if s_state.get('expanded',None):
cls.expanded = self.deserialize_arg(s_state['expanded'])
if isinstance(cls,BusinessRuleTask):
- dt = DecisionTable(None,None)
+ dt = DecisionTable(None,None,None)
dt.deserialize(s_state['dmn'])
dmn_engine = DMNEngine(dt)
cls.dmnEngine=dmn_engine
diff --git a/SpiffWorkflow/dmn/engine/DMNEngine.py b/SpiffWorkflow/dmn/engine/DMNEngine.py
index d2b10532..68ef38bc 100644
--- a/SpiffWorkflow/dmn/engine/DMNEngine.py
+++ b/SpiffWorkflow/dmn/engine/DMNEngine.py
@@ -1,6 +1,7 @@
import logging
import re
+from ..specs.model import HitPolicy
from ...util import levenshtein
from ...workflow import WorkflowException
@@ -16,9 +17,32 @@ class DMNEngine:
self.decision_table = decision_table
def decide(self, task):
+ rules = []
for rule in self.decision_table.rules:
if self.__check_rule(rule, task):
- return rule
+ rules.append(rule)
+ if self.decision_table.hit_policy == HitPolicy.UNIQUE.value:
+ return rules
+ return rules
+
+ def result(self, task):
+ """Returns the results of running this decision table against
+ a given task."""
+ result = {}
+ matched_rules = self.decide(task)
+ if len(matched_rules) == 1:
+ result = matched_rules[0].output_as_dict(task)
+ elif len(matched_rules) > 1: # This must be a multi-output
+ # each output will be an array of values, all outputs will
+ # be placed in a dict, which we will then merge.
+ for rule in matched_rules:
+ rule_output = rule.output_as_dict(task)
+ for key in rule_output.keys():
+ if not key in result:
+ result[key] = []
+ result[key].append(rule_output[key])
+ return result
+
def __check_rule(self, rule, task):
for input_entry in rule.inputEntries:
diff --git a/SpiffWorkflow/dmn/parser/DMNParser.py b/SpiffWorkflow/dmn/parser/DMNParser.py
index 303fe64d..02d7ae56 100644
--- a/SpiffWorkflow/dmn/parser/DMNParser.py
+++ b/SpiffWorkflow/dmn/parser/DMNParser.py
@@ -93,9 +93,10 @@ class DMNParser(NodeParser):
def _parse_decision_tables(self, decision, decisionElement):
for decision_table_element in decisionElement.findall('{*}decisionTable'):
+ name = decision_table_element.attrib.get('name', '')
+ hitPolicy = decision_table_element.attrib.get('hitPolicy', 'UNIQUE').upper()
decision_table = DecisionTable(decision_table_element.attrib['id'],
- decision_table_element.attrib.get(
- 'name', ''))
+ name, hitPolicy)
decision.decisionTables.append(decision_table)
# parse inputs
diff --git a/SpiffWorkflow/dmn/serializer/task_spec_converters.py b/SpiffWorkflow/dmn/serializer/task_spec_converters.py
index 2f523e36..4e3cb183 100644
--- a/SpiffWorkflow/dmn/serializer/task_spec_converters.py
+++ b/SpiffWorkflow/dmn/serializer/task_spec_converters.py
@@ -1,7 +1,7 @@
from ...bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter
from ..specs.BusinessRuleTask import BusinessRuleTask
-from ..specs.model import DecisionTable, Rule
+from ..specs.model import DecisionTable, Rule, HitPolicy
from ..specs.model import Input, InputEntry, Output, OutputEntry
from ..engine.DMNEngine import DMNEngine
@@ -57,7 +57,8 @@ class BusinessRuleTaskConverter(BpmnTaskSpecConverter):
return self.task_spec_from_dict(dct)
def decision_table_from_dict(self, dct):
- table = DecisionTable(dct['id'], dct['name'])
+ hit_policy = dct.get('hit_policy', HitPolicy.UNIQUE.value)
+ table = DecisionTable(dct['id'], dct['name'], hit_policy)
table.inputs = [ Input(**val) for val in dct['inputs'] ]
table.outputs = [ Output(**val) for val in dct['outputs'] ]
table.rules = [ self.rule_from_dict(rule, table.inputs, table.outputs)
diff --git a/SpiffWorkflow/dmn/specs/BusinessRuleTask.py b/SpiffWorkflow/dmn/specs/BusinessRuleTask.py
index 5825c023..8486def2 100644
--- a/SpiffWorkflow/dmn/specs/BusinessRuleTask.py
+++ b/SpiffWorkflow/dmn/specs/BusinessRuleTask.py
@@ -18,7 +18,6 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
super().__init__(wf_spec, name, **kwargs)
self.dmnEngine = dmnEngine
- self.res = None
self.resDict = None
@property
@@ -27,10 +26,8 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
def _on_complete_hook(self, my_task):
try:
- self.res = self.dmnEngine.decide(my_task)
- if self.res is not None: # it is conceivable that no rules fire.
- self.resDict = self.res.output_as_dict(my_task)
- my_task.data = DeepMerge.merge(my_task.data,self.resDict)
+ my_task.data = DeepMerge.merge(my_task.data,
+ self.dmnEngine.result(my_task))
super(BusinessRuleTask, self)._on_complete_hook(my_task)
except Exception as e:
raise WorkflowTaskExecException(my_task, str(e))
diff --git a/SpiffWorkflow/dmn/specs/model.py b/SpiffWorkflow/dmn/specs/model.py
index 11ea1a27..a4d847b5 100644
--- a/SpiffWorkflow/dmn/specs/model.py
+++ b/SpiffWorkflow/dmn/specs/model.py
@@ -1,8 +1,24 @@
from collections import OrderedDict
+from enum import Enum
from ...util.deep_merge import DeepMerge
+class HitPolicy(Enum):
+ UNIQUE = "UNIQUE"
+ COLLECT = "COLLECT"
+ # ANY = "ANY"
+ # PRIORITY = "PRIORITY"
+ # FIRST = "FIRST"
+ # OUTPUT_ORDER = "OUTPUT ORDER"
+ # RULE_ORDER = "RULE ORDER"
+
+# class Aggregation(Enum):
+ # SUM = "SUM"
+ # COUNT = "COUNT"
+ # MIN = "MIN"
+ # MAX = "MAX"
+
class Decision:
def __init__(self, id, name):
self.id = id
@@ -11,9 +27,10 @@ class Decision:
self.decisionTables = []
class DecisionTable:
- def __init__(self, id, name):
+ def __init__(self, id, name, hit_policy):
self.id = id
self.name = name
+ self.hit_policy = hit_policy
self.inputs = []
self.outputs = []
@@ -23,6 +40,7 @@ class DecisionTable:
out = {}
out['id'] = self.id
out['name'] = self.name
+ out['hit_policy'] = self.hit_policy
out['inputs'] = [x.serialize() for x in self.inputs]
out['outputs'] = [x.serialize() for x in self.outputs]
out['rules'] = [x.serialize() for x in self.rules]
@@ -31,6 +49,10 @@ class DecisionTable:
def deserialize(self,indict):
self.id = indict['id']
self.name = indict['name']
+ if 'hit_policy' in indict:
+ self.hit_policy = indict['hit_policy']
+ else:
+ self.hit_policy = HitPolicy.UNIQUE.value
self.inputs = [Input(**x) for x in indict['inputs']]
list(map(lambda x, y: x.deserialize(y), self.inputs, indict['inputs']))
self.outputs = [Output(**x) for x in indict['outputs']]
diff --git a/tests/SpiffWorkflow/dmn/DecisionRunner.py b/tests/SpiffWorkflow/dmn/DecisionRunner.py
index e69c5db8..133f1292 100644
--- a/tests/SpiffWorkflow/dmn/DecisionRunner.py
+++ b/tests/SpiffWorkflow/dmn/DecisionRunner.py
@@ -41,11 +41,17 @@ class DecisionRunner:
'Exactly one decision table should exist! (%s)' \
% (len(decision.decisionTables))
- self.dmnEngine = DMNEngine(decision.decisionTables[0])
+ self.decision_table = decision.decisionTables[0]
+ self.dmnEngine = DMNEngine(self.decision_table)
def decide(self, context):
-
+ """Makes the rather ugly assumption that there is only one
+ rule match for a decision - which was previously the case"""
if not isinstance(context, dict):
context = {'input': context}
task = Task(self.script_engine, context)
- return self.dmnEngine.decide(task)
+ return self.dmnEngine.decide(task)[0]
+
+ def result(self, context):
+ task = Task(self.script_engine, context)
+ return self.dmnEngine.result(task)
diff --git a/tests/SpiffWorkflow/dmn/HitPolicyTest.py b/tests/SpiffWorkflow/dmn/HitPolicyTest.py
new file mode 100644
index 00000000..a7503732
--- /dev/null
+++ b/tests/SpiffWorkflow/dmn/HitPolicyTest.py
@@ -0,0 +1,37 @@
+import os
+import unittest
+
+from SpiffWorkflow.dmn.engine.DMNEngine import DMNEngine
+from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
+from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
+from tests.SpiffWorkflow.dmn.DecisionRunner import DecisionRunner
+from tests.SpiffWorkflow.dmn.python_engine.PythonDecisionRunner import \
+ PythonDecisionRunner
+
+
+class HitPolicyTest(BpmnWorkflowTestCase):
+ PARSER_CLASS = BpmnDmnParser
+
+ def testHitPolicyUnique(self):
+ file_name = os.path.join(os.path.dirname(__file__), 'data', 'unique_hit.dmn')
+ runner = PythonDecisionRunner(file_name)
+ decision_table = runner.decision_table
+ self.assertEqual('UNIQUE', decision_table.hit_policy)
+ res = runner.result({'name': 'Larry'})
+ self.assertEqual(1, res['result'])
+
+ def testHitPolicyCollect(self):
+ file_name = os.path.join(os.path.dirname(__file__), 'data', 'collect_hit.dmn')
+ runner = PythonDecisionRunner(file_name)
+ decision_table = runner.decision_table
+ self.assertEqual('COLLECT', decision_table.hit_policy)
+ res = runner.result({'type': 'stooge'})
+ self.assertEqual(4, len(res['name']))
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(HitPolicyTest)
+
+
+if __name__ == '__main__':
+ unittest.TextTestRunner(verbosity=2).run(suite())
diff --git a/tests/SpiffWorkflow/dmn/data/collect_hit.dmn b/tests/SpiffWorkflow/dmn/data/collect_hit.dmn
new file mode 100644
index 00000000..b918c3e5
--- /dev/null
+++ b/tests/SpiffWorkflow/dmn/data/collect_hit.dmn
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+ type
+
+
+
+
+
+
+
+ "stooge"
+
+
+ 1
+
+
+ "Larry"
+
+
+
+
+
+ "stooge"
+
+
+ 2
+
+
+ "Mo"
+
+
+
+
+ "stooge"
+
+
+ 3
+
+
+ "Curly"
+
+
+
+
+ "stooge"
+
+
+ 4
+
+
+ "Shemp"
+
+
+
+
+ "farmer"
+
+
+ 5
+
+
+ "Elmer Fudd"
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/SpiffWorkflow/dmn/data/unique_hit.dmn b/tests/SpiffWorkflow/dmn/data/unique_hit.dmn
new file mode 100644
index 00000000..f4d86264
--- /dev/null
+++ b/tests/SpiffWorkflow/dmn/data/unique_hit.dmn
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+ name
+
+
+
+
+
+
+ "Larry"
+
+
+ 1
+
+
+
+
+
+ "Mo"
+
+
+ 2
+
+
+
+
+ "Curly"
+
+
+ 3
+
+
+
+
+ "Shemp"
+
+
+ 4
+
+
+
+
+
+
+
+
+
+
+
+