Merge commit 'cc1903387bf02a9d0ee847e31ce568f759b06a0c' into main

This commit is contained in:
Dan 2022-11-25 11:07:31 -05:00
commit 0963102a88
10 changed files with 236 additions and 16 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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']]

View File

@ -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)

View File

@ -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())

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" id="Definitions_06veek1" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
<decision id="Decision_ExclusiveAMCheck" name="UniqueHit">
<decisionTable id="decisionTable_1" hitPolicy="COLLECT">
<input id="InputClause_1ley07z" label="Input &#34;type&#34;">
<inputExpression id="LiteralExpression_0yhfcz3" typeRef="string" expressionLanguage="python">
<text>type</text>
</inputExpression>
</input>
<output id="output_1" label="Output &#34;result&#34;" name="result" typeRef="integer" />
<output id="OutputClause_1yrcvgl" label="Output &#34;name&#34;" name="name" typeRef="string" />
<rule id="DecisionRule_07162mr">
<description></description>
<inputEntry id="UnaryTests_079zee3">
<text>"stooge"</text>
</inputEntry>
<outputEntry id="LiteralExpression_16l50ps">
<text>1</text>
</outputEntry>
<outputEntry id="LiteralExpression_1lgpxjl">
<text>"Larry"</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0ifa4wu">
<description></description>
<inputEntry id="UnaryTests_1fj8ed0">
<text>"stooge"</text>
</inputEntry>
<outputEntry id="LiteralExpression_0td8sa6">
<text>2</text>
</outputEntry>
<outputEntry id="LiteralExpression_13y7kcx">
<text>"Mo"</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0x9z2jm">
<inputEntry id="UnaryTests_1nsph49">
<text>"stooge"</text>
</inputEntry>
<outputEntry id="LiteralExpression_04v8i8e">
<text>3</text>
</outputEntry>
<outputEntry id="LiteralExpression_1f70eo7">
<text>"Curly"</text>
</outputEntry>
</rule>
<rule id="DecisionRule_16ur1y7">
<inputEntry id="UnaryTests_0etcimx">
<text>"stooge"</text>
</inputEntry>
<outputEntry id="LiteralExpression_0yc7vs6">
<text>4</text>
</outputEntry>
<outputEntry id="LiteralExpression_1k2kclw">
<text>"Shemp"</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0fzx40q">
<inputEntry id="UnaryTests_1gzu54o">
<text>"farmer"</text>
</inputEntry>
<outputEntry id="LiteralExpression_11h0epz">
<text>5</text>
</outputEntry>
<outputEntry id="LiteralExpression_031adn2">
<text>"Elmer Fudd"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
<dmndi:DMNDI>
<dmndi:DMNDiagram id="DMNDiagram_1vu8ul5">
<dmndi:DMNShape id="DMNShape_0sl8kie" dmnElementRef="Decision_ExclusiveAMCheck">
<dc:Bounds height="80" width="180" x="150" y="150" />
</dmndi:DMNShape>
</dmndi:DMNDiagram>
</dmndi:DMNDI>
</definitions>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/2.0" xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" id="Definitions_06veek1" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
<decision id="Decision_ExclusiveAMCheck" name="UniqueHit">
<decisionTable id="decisionTable_1">
<input id="input_1" label="Input &#34;name&#34;" biodi:width="192">
<inputExpression id="inputExpression_1" typeRef="string" expressionLanguage="python">
<text>name</text>
</inputExpression>
</input>
<output id="output_1" label="Output &#34;result&#34;" name="result" typeRef="integer" />
<rule id="DecisionRule_07162mr">
<description></description>
<inputEntry id="UnaryTests_1jqxc3u">
<text>"Larry"</text>
</inputEntry>
<outputEntry id="LiteralExpression_16l50ps">
<text>1</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0ifa4wu">
<description></description>
<inputEntry id="UnaryTests_0szbwxc">
<text>"Mo"</text>
</inputEntry>
<outputEntry id="LiteralExpression_0td8sa6">
<text>2</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0x9z2jm">
<inputEntry id="UnaryTests_1ul57lx">
<text>"Curly"</text>
</inputEntry>
<outputEntry id="LiteralExpression_04v8i8e">
<text>3</text>
</outputEntry>
</rule>
<rule id="DecisionRule_16ur1y7">
<inputEntry id="UnaryTests_09q92u9">
<text>"Shemp"</text>
</inputEntry>
<outputEntry id="LiteralExpression_0yc7vs6">
<text>4</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
<dmndi:DMNDI>
<dmndi:DMNDiagram id="DMNDiagram_1vu8ul5">
<dmndi:DMNShape id="DMNShape_0sl8kie" dmnElementRef="Decision_ExclusiveAMCheck">
<dc:Bounds height="80" width="180" x="150" y="150" />
</dmndi:DMNShape>
</dmndi:DMNDiagram>
</dmndi:DMNDI>
</definitions>