Merge pull request #1056 from cantino/trigger_agent_supports_partial_matches

Allow the TriggerAgent to perform partial matches

Andrew Cantino 8 years ago
parent
commit
2646645552
2 changed files with 94 additions and 18 deletions
  1. 23 4
      app/models/agents/trigger_agent.rb
  2. 71 14
      spec/models/agents/trigger_agent_spec.rb

+ 23 - 4
app/models/agents/trigger_agent.rb

@@ -13,7 +13,10 @@ module Agents
13 13
 
14 14
       The `value` can be a single value or an array of values. In the case of an array, if one or more values match then the rule matches.
15 15
 
16
-      All rules must match for the Agent to match.  The resulting Event will have a payload message of `message`.  You can use liquid templating in the `message, have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details.
16
+      By default, all rules must match for the Agent to trigger. You can switch this so that only one rule must match by
17
+      setting `must_match` to `1`.
18
+
19
+      The resulting Event will have a payload message of `message`.  You can use liquid templating in the `message, have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details.
17 20
 
18 21
       Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided.
19 22
 
@@ -35,6 +38,14 @@ module Agents
35 38
       errors.add(:base, "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event?
36 39
 
37 40
       errors.add(:base, "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[true false].include?(options['keep_event'])
41
+
42
+      if options['must_match'].present?
43
+        if options['must_match'].to_i < 1
44
+          errors.add(:base, "If used, the 'must_match' option must be a positive integer")
45
+        elsif options['must_match'].to_i > options['rules'].length
46
+          errors.add(:base, "If used, the 'must_match' option must be equal to or less than the number of rules")
47
+        end
48
+      end
38 49
     end
39 50
 
40 51
     def default_options
@@ -59,12 +70,12 @@ module Agents
59 70
 
60 71
         opts = interpolated(event)
61 72
 
62
-        match = opts['rules'].all? do |rule|
73
+        match_results = opts['rules'].map do |rule|
63 74
           value_at_path = Utils.value_at(event['payload'], rule['path'])
64 75
           rule_values = rule['value']
65 76
           rule_values = [rule_values] unless rule_values.is_a?(Array)
66 77
 
67
-          match_found = rule_values.any? do |rule_value|
78
+          rule_values.any? do |rule_value|
68 79
             case rule['type']
69 80
             when "regex"
70 81
               value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE)
@@ -88,7 +99,7 @@ module Agents
88 99
           end
89 100
         end
90 101
 
91
-        if match
102
+        if matches?(match_results)
92 103
           if keep_event?
93 104
             payload = event.payload.dup
94 105
             payload['message'] = opts['message'] if opts['message'].present?
@@ -101,6 +112,14 @@ module Agents
101 112
       end
102 113
     end
103 114
 
115
+    def matches?(matches)
116
+      if options['must_match'].present?
117
+        matches.select { |match| match }.length >= options['must_match'].to_i
118
+      else
119
+        matches.all?
120
+      end
121
+    end
122
+
104 123
     def keep_event?
105 124
       boolify(interpolated['keep_event'])
106 125
     end

+ 71 - 14
spec/models/agents/trigger_agent_spec.rb

@@ -58,6 +58,27 @@ describe Agents::TriggerAgent do
58 58
       expect(@checker).not_to be_valid
59 59
     end
60 60
 
61
+    it "validates that 'must_match' is a positive integer, not greater than the number of rules, if provided" do
62
+      @checker.options['must_match'] = '1'
63
+      expect(@checker).to be_valid
64
+
65
+      @checker.options['must_match'] = '0'
66
+      expect(@checker).not_to be_valid
67
+
68
+      @checker.options['must_match'] = 'wrong'
69
+      expect(@checker).not_to be_valid
70
+
71
+      @checker.options['must_match'] = ''
72
+      expect(@checker).to be_valid
73
+
74
+      @checker.options.delete('must_match')
75
+      expect(@checker).to be_valid
76
+
77
+      @checker.options['must_match'] = '2'
78
+      expect(@checker).not_to be_valid
79
+      expect(@checker.errors[:base].first).to match(/equal to or less than the number of rules/)
80
+    end
81
+
61 82
     it "should validate the three fields in each rule" do
62 83
       @checker.options['rules'] << { 'path' => "foo", 'type' => "fake", 'value' => "6" }
63 84
       expect(@checker).not_to be_valid
@@ -283,23 +304,59 @@ describe Agents::TriggerAgent do
283 304
       }.to change { Event.count }.by(2)
284 305
     end
285 306
 
286
-    it "handles ANDing rules together" do
287
-      @checker.options['rules'] << {
288
-        'type' => "field>=value",
289
-        'value' => "4",
290
-        'path' => "foo.bing"
291
-      }
307
+    describe "with multiple rules" do
308
+      before do
309
+        @checker.options['rules'] << {
310
+          'type' => "field>=value",
311
+          'value' => "4",
312
+          'path' => "foo.bing"
313
+        }
314
+      end
292 315
 
293
-      @event.payload['foo']["bing"] = "5"
316
+      it "handles ANDing rules together" do
317
+        @event.payload['foo']["bing"] = "5"
294 318
 
295
-      expect {
296
-        @checker.receive([@event])
297
-      }.to change { Event.count }.by(1)
319
+        expect {
320
+          @checker.receive([@event])
321
+        }.to change { Event.count }.by(1)
298 322
 
299
-      @checker.options['rules'].last['value'] = 6
300
-      expect {
301
-        @checker.receive([@event])
302
-      }.not_to change { Event.count }
323
+        @event.payload['foo']["bing"] = "2"
324
+
325
+        expect {
326
+          @checker.receive([@event])
327
+        }.not_to change { Event.count }
328
+      end
329
+
330
+      it "can accept a partial rule set match when 'must_match' is present and less than the total number of rules" do
331
+        @checker.options['must_match'] = "1"
332
+
333
+        @event.payload['foo']["bing"] = "5" # 5 > 4
334
+
335
+        expect {
336
+          @checker.receive([@event])
337
+        }.to change { Event.count }.by(1)
338
+
339
+        @event.payload['foo']["bing"] = "2" # 2 !> 4
340
+
341
+        expect {
342
+          @checker.receive([@event])
343
+        }.to change { Event.count }         # but the first one matches
344
+
345
+
346
+        @checker.options['must_match'] = "2"
347
+
348
+        @event.payload['foo']["bing"] = "5" # 5 > 4
349
+
350
+        expect {
351
+          @checker.receive([@event])
352
+        }.to change { Event.count }.by(1)
353
+
354
+        @event.payload['foo']["bing"] = "2" # 2 !> 4
355
+
356
+        expect {
357
+          @checker.receive([@event])
358
+        }.not_to change { Event.count }     # only 1 matches, we needed 2
359
+      end
303 360
     end
304 361
 
305 362
     describe "when 'keep_event' is true" do