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