mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-01-13 19:55:24 +00:00
Merge commit 'cc1903387bf02a9d0ee847e31ce568f759b06a0c' into main
This commit is contained in:
commit
0963102a88
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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']]
|
||||
|
@ -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)
|
||||
|
37
SpiffWorkflow/tests/SpiffWorkflow/dmn/HitPolicyTest.py
Normal file
37
SpiffWorkflow/tests/SpiffWorkflow/dmn/HitPolicyTest.py
Normal 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())
|
78
SpiffWorkflow/tests/SpiffWorkflow/dmn/data/collect_hit.dmn
Normal file
78
SpiffWorkflow/tests/SpiffWorkflow/dmn/data/collect_hit.dmn
Normal 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 "type"">
|
||||
<inputExpression id="LiteralExpression_0yhfcz3" typeRef="string" expressionLanguage="python">
|
||||
<text>type</text>
|
||||
</inputExpression>
|
||||
</input>
|
||||
<output id="output_1" label="Output "result"" name="result" typeRef="integer" />
|
||||
<output id="OutputClause_1yrcvgl" label="Output "name"" 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>
|
54
SpiffWorkflow/tests/SpiffWorkflow/dmn/data/unique_hit.dmn
Normal file
54
SpiffWorkflow/tests/SpiffWorkflow/dmn/data/unique_hit.dmn
Normal 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 "name"" biodi:width="192">
|
||||
<inputExpression id="inputExpression_1" typeRef="string" expressionLanguage="python">
|
||||
<text>name</text>
|
||||
</inputExpression>
|
||||
</input>
|
||||
<output id="output_1" label="Output "result"" 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>
|
Loading…
x
Reference in New Issue
Block a user