@@ -12,6 +12,10 @@ module Agents |
||
12 | 12 |
"celsius": "18", |
13 | 13 |
"fahreinheit": "64" |
14 | 14 |
}, |
15 |
+ "date": { |
|
16 |
+ "epoch": "1357959600", |
|
17 |
+ "pretty": "10:00 PM EST on January 11, 2013" |
|
18 |
+ }, |
|
15 | 19 |
"conditions": "Rain showers", |
16 | 20 |
"data": "This is some data" |
17 | 21 |
} |
@@ -33,6 +37,33 @@ module Agents |
||
33 | 37 |
"subject": "This is some data" |
34 | 38 |
} |
35 | 39 |
|
40 |
+ In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting. Here is an example: |
|
41 |
+ |
|
42 |
+ { |
|
43 |
+ "matchers": [ |
|
44 |
+ { |
|
45 |
+ "path": "$.date.pretty", |
|
46 |
+ "regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
|
47 |
+ "to": "pretty_date", |
|
48 |
+ } |
|
49 |
+ ] |
|
50 |
+ } |
|
51 |
+ |
|
52 |
+ This virtually merges the following hash into the original event hash: |
|
53 |
+ |
|
54 |
+ "pretty_date": { |
|
55 |
+ "time": "10:00 PM EST", |
|
56 |
+ "0": "10:00 PM EST on January 11, 2013" |
|
57 |
+ "1": "10:00 PM EST", |
|
58 |
+ } |
|
59 |
+ |
|
60 |
+ So you can use it in `instructions` like this: |
|
61 |
+ |
|
62 |
+ "instructions": { |
|
63 |
+ "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.", |
|
64 |
+ "subject": "$.data" |
|
65 |
+ } |
|
66 |
+ |
|
36 | 67 |
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. |
37 | 68 |
|
38 | 69 |
By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time. You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`. |
@@ -46,8 +77,12 @@ module Agents |
||
46 | 77 |
|
47 | 78 |
event_description "User defined" |
48 | 79 |
|
80 |
+ after_save :clear_matchers |
|
81 |
+ |
|
49 | 82 |
def validate_options |
50 | 83 |
errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present? |
84 |
+ |
|
85 |
+ validate_matchers |
|
51 | 86 |
end |
52 | 87 |
|
53 | 88 |
def default_options |
@@ -56,6 +91,7 @@ module Agents |
||
56 | 91 |
'message' => "You received a text <$.text> from <$.fields.from>", |
57 | 92 |
'some_other_field' => "Looks like the weather is going to be <$.fields.weather>" |
58 | 93 |
}, |
94 |
+ 'matchers' => [], |
|
59 | 95 |
'mode' => "clean", |
60 | 96 |
'skip_agent' => "false", |
61 | 97 |
'skip_created_at' => "false" |
@@ -68,12 +104,92 @@ module Agents |
||
68 | 104 |
|
69 | 105 |
def receive(incoming_events) |
70 | 106 |
incoming_events.each do |event| |
71 |
- formatted_event = options['mode'].to_s == "merge" ? event.payload : {} |
|
72 |
- options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) } |
|
107 |
+ formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {} |
|
108 |
+ payload = perform_matching(event.payload) |
|
109 |
+ options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) } |
|
73 | 110 |
formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true" |
74 | 111 |
formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true" |
75 | 112 |
create_event :payload => formatted_event |
76 | 113 |
end |
77 | 114 |
end |
115 |
+ |
|
116 |
+ private |
|
117 |
+ |
|
118 |
+ def validate_matchers |
|
119 |
+ matchers = options['matchers'] or return |
|
120 |
+ |
|
121 |
+ unless matchers.is_a?(Array) |
|
122 |
+ errors.add(:base, "matchers must be an array if present") |
|
123 |
+ return |
|
124 |
+ end |
|
125 |
+ |
|
126 |
+ matchers.each do |matcher| |
|
127 |
+ unless matcher.is_a?(Hash) |
|
128 |
+ errors.add(:base, "each matcher must be a hash") |
|
129 |
+ next |
|
130 |
+ end |
|
131 |
+ |
|
132 |
+ regexp, path, to = matcher.values_at(*%w[regexp path to]) |
|
133 |
+ |
|
134 |
+ if regexp.present? |
|
135 |
+ begin |
|
136 |
+ Regexp.new(regexp) |
|
137 |
+ rescue |
|
138 |
+ errors.add(:base, "bad regexp found in matchers: #{regexp}") |
|
139 |
+ end |
|
140 |
+ else |
|
141 |
+ errors.add(:base, "regexp is mandatory for a matcher and must be a string") |
|
142 |
+ end |
|
143 |
+ |
|
144 |
+ errors.add(:base, "path is mandatory for a matcher and must be a string") if !path.present? |
|
145 |
+ |
|
146 |
+ errors.add(:base, "to must be a string if present in a matcher") if to.present? && !to.is_a?(String) |
|
147 |
+ end |
|
148 |
+ end |
|
149 |
+ |
|
150 |
+ def perform_matching(payload) |
|
151 |
+ matchers.inject(payload.dup) { |hash, matcher| |
|
152 |
+ matcher[hash] |
|
153 |
+ } |
|
154 |
+ end |
|
155 |
+ |
|
156 |
+ def matchers |
|
157 |
+ @matchers ||= |
|
158 |
+ if matchers = options['matchers'] |
|
159 |
+ matchers.map { |matcher| |
|
160 |
+ regexp, path, to = matcher.values_at(*%w[regexp path to]) |
|
161 |
+ re = Regexp.new(regexp) |
|
162 |
+ proc { |hash| |
|
163 |
+ mhash = {} |
|
164 |
+ value = Utils.value_at(hash, path) |
|
165 |
+ if value.is_a?(String) && (m = re.match(value)) |
|
166 |
+ m.to_a.each_with_index { |s, i| |
|
167 |
+ mhash[i.to_s] = s |
|
168 |
+ } |
|
169 |
+ m.names.each do |name| |
|
170 |
+ mhash[name] = m[name] |
|
171 |
+ end if m.respond_to?(:names) |
|
172 |
+ end |
|
173 |
+ if to |
|
174 |
+ case value = hash[to] |
|
175 |
+ when Hash |
|
176 |
+ value.update(mhash) |
|
177 |
+ else |
|
178 |
+ hash[to] = mhash |
|
179 |
+ end |
|
180 |
+ else |
|
181 |
+ hash.update(mhash) |
|
182 |
+ end |
|
183 |
+ hash |
|
184 |
+ } |
|
185 |
+ } |
|
186 |
+ else |
|
187 |
+ [] |
|
188 |
+ end |
|
189 |
+ end |
|
190 |
+ |
|
191 |
+ def clear_matchers |
|
192 |
+ @matchers = nil |
|
193 |
+ end |
|
78 | 194 |
end |
79 |
-end |
|
195 |
+end |
@@ -7,9 +7,16 @@ describe Agents::EventFormattingAgent do |
||
7 | 7 |
:options => { |
8 | 8 |
:instructions => { |
9 | 9 |
:message => "Received <$.content.text.*> from <$.content.name> .", |
10 |
- :subject => "Weather looks like <$.conditions>" |
|
10 |
+ :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>" |
|
11 | 11 |
}, |
12 | 12 |
:mode => "clean", |
13 |
+ :matchers => [ |
|
14 |
+ { |
|
15 |
+ :path => "$.date.pretty", |
|
16 |
+ :regexp => "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
|
17 |
+ :to => "pretty_date", |
|
18 |
+ }, |
|
19 |
+ ], |
|
13 | 20 |
:skip_agent => "false", |
14 | 21 |
:skip_created_at => "false" |
15 | 22 |
} |
@@ -24,7 +31,11 @@ describe Agents::EventFormattingAgent do |
||
24 | 31 |
@event.payload = { |
25 | 32 |
:content => { |
26 | 33 |
:text => "Some Lorem Ipsum", |
27 |
- :name => "somevalue" |
|
34 |
+ :name => "somevalue", |
|
35 |
+ }, |
|
36 |
+ :date => { |
|
37 |
+ :epoch => "1357959600", |
|
38 |
+ :pretty => "10:00 PM EST on January 11, 2013" |
|
28 | 39 |
}, |
29 | 40 |
:conditions => "someothervalue" |
30 | 41 |
} |
@@ -61,7 +72,11 @@ describe Agents::EventFormattingAgent do |
||
61 | 72 |
it "should handle JSONPaths in instructions" do |
62 | 73 |
@checker.receive([@event]) |
63 | 74 |
Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ." |
64 |
- Event.last.payload[:subject].should == "Weather looks like someothervalue" |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ it "should handle matchers and JSONPaths in instructions" do |
|
78 |
+ @checker.receive([@event]) |
|
79 |
+ Event.last.payload[:subject].should == "Weather looks like someothervalue according to the forecast at 10:00 PM EST" |
|
65 | 80 |
end |
66 | 81 |
|
67 | 82 |
it "should allow escaping" do |
@@ -110,6 +125,28 @@ describe Agents::EventFormattingAgent do |
||
110 | 125 |
@checker.should_not be_valid |
111 | 126 |
end |
112 | 127 |
|
128 |
+ it "should validate type of matchers" do |
|
129 |
+ @checker.options[:matchers] = "" |
|
130 |
+ @checker.should_not be_valid |
|
131 |
+ @checker.options[:matchers] = {} |
|
132 |
+ @checker.should_not be_valid |
|
133 |
+ end |
|
134 |
+ |
|
135 |
+ it "should validate the contents of matchers" do |
|
136 |
+ @checker.options[:matchers] = [ |
|
137 |
+ {} |
|
138 |
+ ] |
|
139 |
+ @checker.should_not be_valid |
|
140 |
+ @checker.options[:matchers] = [ |
|
141 |
+ { :regexp => "(not closed", :path => "text" } |
|
142 |
+ ] |
|
143 |
+ @checker.should_not be_valid |
|
144 |
+ @checker.options[:matchers] = [ |
|
145 |
+ { :regexp => "(closed)", :path => "text", :to => "foo" } |
|
146 |
+ ] |
|
147 |
+ @checker.should be_valid |
|
148 |
+ end |
|
149 |
+ |
|
113 | 150 |
it "should validate presence of mode" do |
114 | 151 |
@checker.options[:mode] = "" |
115 | 152 |
@checker.should_not be_valid |
@@ -125,4 +162,4 @@ describe Agents::EventFormattingAgent do |
||
125 | 162 |
@checker.should_not be_valid |
126 | 163 |
end |
127 | 164 |
end |
128 |
-end |
|
165 |
+end |