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