Repository: StackStorm/st2

Issue:
Failed rendering payload value into pattern
Hi,

Following is a rule's example. In this snippet, I would like to compare two elements from payload. In this specific case, **host** and **master**. Log shows `UndefinedError: 'trigger' is undefined`. It looks like trigger is not being rendered.

```
trigger:
  type: "core.st2.webhook"
  parameters:
    url: "serf"
criteria:
  trigger.body.event:
    type: "equals"
    pattern: "member-failed"
  trigger.body.host:
    type: "equals"
    pattern: "{{ trigger.body.master }}"
```

Thanks!


Discussion:

Resolving patch:
diff --git a/st2common/st2common/constants/rules.py b/st2common/st2common/constants/rules.py
--- a/st2common/st2common/constants/rules.py
+++ b/st2common/st2common/constants/rules.py
@@ -17,3 +17,5 @@
 
 RULE_TYPE_STANDARD = 'standard'
 RULE_TYPE_BACKSTOP = 'backstop'
+
+MATCH_CRITERIA = r'({{)\s*(.*)\s*(}})'
diff --git a/st2reactor/st2reactor/rules/filter.py b/st2reactor/st2reactor/rules/filter.py
--- a/st2reactor/st2reactor/rules/filter.py
+++ b/st2reactor/st2reactor/rules/filter.py
@@ -14,11 +14,13 @@
 # limitations under the License.
 
 import six
+import json
+import re
 from jsonpath_rw import parse
 
 from st2common import log as logging
 import st2common.operators as criteria_operators
-from st2common.constants.rules import TRIGGER_PAYLOAD_PREFIX, RULE_TYPE_BACKSTOP
+from st2common.constants.rules import TRIGGER_PAYLOAD_PREFIX, RULE_TYPE_BACKSTOP, MATCH_CRITERIA
 from st2common.constants.keyvalue import SYSTEM_SCOPES
 from st2common.services.keyvalues import KeyValueLookup
 from st2common.util.templating import render_template_with_system_context
@@ -79,7 +81,10 @@ def filter(self):
         for criterion_k in criteria.keys():
             criterion_v = criteria[criterion_k]
             is_rule_applicable, payload_value, criterion_pattern = self._check_criterion(
-                criterion_k, criterion_v, payload_lookup)
+                criterion_k,
+                criterion_v,
+                payload_lookup
+            )
             if not is_rule_applicable:
                 if self.extra_info:
                     criteria_extra_info = '\n'.join([
@@ -102,18 +107,21 @@ def filter(self):
     def _check_criterion(self, criterion_k, criterion_v, payload_lookup):
         if 'type' not in criterion_v:
             # Comparison operator type not specified, can't perform a comparison
-            return False
+            return (False, None, None)
 
         criteria_operator = criterion_v['type']
         criteria_pattern = criterion_v.get('pattern', None)
 
         # Render the pattern (it can contain a jinja expressions)
         try:
-            criteria_pattern = self._render_criteria_pattern(criteria_pattern=criteria_pattern)
+            criteria_pattern = self._render_criteria_pattern(
+                criteria_pattern=criteria_pattern,
+                criteria_context=payload_lookup.context
+            )
         except Exception:
             LOG.exception('Failed to render pattern value "%s" for key "%s"' %
                           (criteria_pattern, criterion_k), extra=self._base_logger_context)
-            return False
+            return (False, None, None)
 
         try:
             matches = payload_lookup.get_value(criterion_k)
@@ -125,7 +133,7 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup):
         except:
             LOG.exception('Failed transforming criteria key %s', criterion_k,
                           extra=self._base_logger_context)
-            return False
+            return (False, None, None)
 
         op_func = criteria_operators.get_operator(criteria_operator)
 
@@ -134,11 +142,11 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup):
         except:
             LOG.exception('There might be a problem with the criteria in rule %s.', self.rule,
                           extra=self._base_logger_context)
-            return False
+            return (False, None, None)
 
         return result, payload_value, criteria_pattern
 
-    def _render_criteria_pattern(self, criteria_pattern):
+    def _render_criteria_pattern(self, criteria_pattern, criteria_context):
         # Note: Here we want to use strict comparison to None to make sure that
         # other falsy values such as integer 0 are handled correctly.
         if criteria_pattern is None:
@@ -149,8 +157,45 @@ def _render_criteria_pattern(self, criteria_pattern):
             # makes no sense
             return criteria_pattern
 
-        criteria_pattern = render_template_with_system_context(value=criteria_pattern)
-        return criteria_pattern
+        LOG.debug(
+            'Rendering criteria pattern (%s) with context: %s',
+            criteria_pattern,
+            criteria_context
+        )
+
+        to_complex = False
+
+        # Check if jinja variable is in criteria_pattern and if so lets ensure
+        # the proper type is applied to it using to_complex jinja filter
+        if len(re.findall(MATCH_CRITERIA, criteria_pattern)) > 0:
+            LOG.debug("Rendering Complex")
+            complex_criteria_pattern = re.sub(
+                MATCH_CRITERIA, r'\1\2 | to_complex\3',
+                criteria_pattern
+            )
+
+            try:
+                criteria_rendered = render_template_with_system_context(
+                    value=complex_criteria_pattern,
+                    context=criteria_context
+                )
+                criteria_rendered = json.loads(criteria_rendered)
+                to_complex = True
+            except ValueError, error:
+                LOG.debug('Criteria pattern not valid JSON: %s', error)
+
+        if not to_complex:
+            criteria_rendered = render_template_with_system_context(
+                value=criteria_pattern,
+                context=criteria_context
+            )
+
+        LOG.debug(
+            'Rendered criteria pattern: %s',
+            criteria_rendered
+        )
+
+        return criteria_rendered
 
 
 class SecondPassRuleFilter(RuleFilter):
@@ -188,16 +233,16 @@ def _is_backstop_rule(self):
 class PayloadLookup(object):
 
     def __init__(self, payload):
-        self._context = {
+        self.context = {
             TRIGGER_PAYLOAD_PREFIX: payload
         }
 
         for system_scope in SYSTEM_SCOPES:
-            self._context[system_scope] = KeyValueLookup(scope=system_scope)
+            self.context[system_scope] = KeyValueLookup(scope=system_scope)
 
     def get_value(self, lookup_key):
         expr = parse(lookup_key)
-        matches = [match.value for match in expr.find(self._context)]
+        matches = [match.value for match in expr.find(self.context)]
         if not matches:
             return None
         return matches


Test patch:
diff --git a/st2reactor/tests/unit/test_filter.py b/st2reactor/tests/unit/test_filter.py
--- a/st2reactor/tests/unit/test_filter.py
+++ b/st2reactor/tests/unit/test_filter.py
@@ -28,14 +28,18 @@
 
 MOCK_TRIGGER = TriggerDB(pack='dummy_pack_1', name='trigger-test.name', type='system.test')
 
-MOCK_TRIGGER_INSTANCE = TriggerInstanceDB(trigger=MOCK_TRIGGER.get_reference().ref,
-                                          occurrence_time=date_utils.get_datetime_utc_now(),
-                                          payload={
-                                              'p1': 'v1',
-                                              'p2': 'preYYYpost',
-                                              'bool': True,
-                                              'int': 1,
-                                              'float': 0.8})
+MOCK_TRIGGER_INSTANCE = TriggerInstanceDB(
+    trigger=MOCK_TRIGGER.get_reference().ref,
+    occurrence_time=date_utils.get_datetime_utc_now(),
+    payload={
+        'p1': 'v1',
+        'p2': 'preYYYpost',
+        'bool': True,
+        'int': 1,
+        'float': 0.8,
+        'list': ['v1', True, 1]
+    }
+)
 
 MOCK_ACTION = ActionDB(id=bson.ObjectId(), pack='wolfpack', name='action-test-1.name')
 
@@ -89,7 +93,39 @@ def test_equals_operator_pass_criteria(self):
         rule = MOCK_RULE_1
         rule.criteria = {'trigger.p1': {'type': 'equals', 'pattern': 'v1'}}
         f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
-        self.assertTrue(f.filter(), 'regex check should have failed.')
+        self.assertTrue(f.filter(), 'equals check should have passed.')
+
+        rule = MOCK_RULE_1
+        rule.criteria = {'trigger.p1': {'type': 'equals', 'pattern': '{{trigger.p1}}'}}
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertTrue(f.filter(), 'equals check should have passed.')
+
+        rule = MOCK_RULE_1
+        rule.criteria = {
+            'trigger.p1': {
+                'type': 'equals',
+                'pattern': "{{'%s' % trigger.p1 if trigger.int}}"
+            }
+        }
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertTrue(f.filter(), 'equals check should have passed.')
+
+        # Test our filter works if proper JSON is returned from user pattern
+        rule = MOCK_RULE_1
+        rule.criteria = {
+            'trigger.list': {
+                'type': 'equals',
+                'pattern': """
+                    [
+                        {% for item in trigger.list %}
+                        {{item}}{% if not loop.last %},{% endif %}
+                        {% endfor %}
+                    ]
+                """
+            }
+        }
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertTrue(f.filter(), 'equals check should have passed.')
 
     def test_equals_operator_fail_criteria(self):
         rule = MOCK_RULE_1
@@ -97,24 +133,65 @@ def test_equals_operator_fail_criteria(self):
         f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
         self.assertFalse(f.filter(), 'equals check should have failed.')
 
+        rule = MOCK_RULE_1
+        rule.criteria = {'trigger.p1': {'type': 'equals', 'pattern': '{{trigger.p2}}'}}
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertFalse(f.filter(), 'equals check should have failed.')
+
+        rule = MOCK_RULE_1
+        rule.criteria = {
+            'trigger.list': {
+                'type': 'equals',
+                'pattern': """
+                    [
+                        {% for item in trigger.list %}
+                        {{item}}
+                        {% endfor %}
+                    ]
+                """
+            }
+        }
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertFalse(f.filter(), 'equals check should have failed.')
+
     def test_equals_bool_value(self):
         rule = MOCK_RULE_1
         rule.criteria = {'trigger.bool': {'type': 'equals', 'pattern': True}}
         f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
         self.assertTrue(f.filter(), 'equals check should have passed.')
 
+        rule = MOCK_RULE_1
+        rule.criteria = {'trigger.bool': {'type': 'equals', 'pattern': '{{trigger.bool}}'}}
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertTrue(f.filter(), 'equals check should have passed.')
+
+        rule = MOCK_RULE_1
+        rule.criteria = {'trigger.bool': {'type': 'equals', 'pattern': '{{ trigger.bool }}'}}
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertTrue(f.filter(), 'equals check should have passed.')
+
     def test_equals_int_value(self):
         rule = MOCK_RULE_1
         rule.criteria = {'trigger.int': {'type': 'equals', 'pattern': 1}}
         f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
         self.assertTrue(f.filter(), 'equals check should have passed.')
 
+        rule = MOCK_RULE_1
+        rule.criteria = {'trigger.int': {'type': 'equals', 'pattern': '{{trigger.int}}'}}
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertTrue(f.filter(), 'equals check should have passed.')
+
     def test_equals_float_value(self):
         rule = MOCK_RULE_1
         rule.criteria = {'trigger.float': {'type': 'equals', 'pattern': 0.8}}
         f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
         self.assertTrue(f.filter(), 'equals check should have passed.')
 
+        rule = MOCK_RULE_1
+        rule.criteria = {'trigger.float': {'type': 'equals', 'pattern': '{{trigger.float}}'}}
+        f = RuleFilter(MOCK_TRIGGER_INSTANCE, MOCK_TRIGGER, rule)
+        self.assertTrue(f.filter(), 'equals check should have passed.')
+
     def test_exists(self):
         rule = MOCK_RULE_1
         rule.criteria = {'trigger.float': {'type': 'exists'}}