Merge pull request #285 from dsander/liquid-templating

Added liquid templating and migrated first agents

Andrew Cantino 10 年之前
父节点
当前提交
08b2f6c8f8

+ 1 - 0
Gemfile

@@ -31,6 +31,7 @@ gem 'json', '~> 1.8.1'
31 31
 gem 'jsonpath', '~> 0.5.3'
32 32
 gem 'twilio-ruby', '~> 3.11.5'
33 33
 gem 'ruby-growl', '~> 4.1.0'
34
+gem 'liquid', '~> 2.6.1'
34 35
 
35 36
 gem 'delayed_job', '~> 4.0.0'
36 37
 gem 'delayed_job_active_record', '~> 4.0.0'

+ 2 - 0
Gemfile.lock

@@ -148,6 +148,7 @@ GEM
148 148
       activesupport (>= 3.0.0)
149 149
     kramdown (1.3.3)
150 150
     libv8 (3.16.14.3)
151
+    liquid (2.6.1)
151 152
     macaddr (1.7.1)
152 153
       systemu (~> 2.6.2)
153 154
     mail (2.5.4)
@@ -338,6 +339,7 @@ DEPENDENCIES
338 339
   jsonpath (~> 0.5.3)
339 340
   kaminari (~> 0.15.1)
340 341
   kramdown (~> 1.3.3)
342
+  liquid (~> 2.6.1)
341 343
   mysql2 (~> 0.3.15)
342 344
   nokogiri (~> 1.6.1)
343 345
   protected_attributes (~> 1.0.7)

+ 0 - 39
app/concerns/json_path_options_overwritable.rb

@@ -1,39 +0,0 @@
1
-module JsonPathOptionsOverwritable
2
-  extend ActiveSupport::Concern
3
-  # Using this concern allows providing optional `<attribute>_path` options hash
4
-  # attributes which will then (if not blank) be interpolated using the provided JSONPath.
5
-  #
6
-  # Example options Hash:
7
-  # {
8
-  #   name: 'Huginn',
9
-  #   name_path: '$.name',
10
-  #   title: 'Hello from Huginn'
11
-  #   title_path: ''
12
-  # }
13
-  # Example event payload:
14
-  # {
15
-  #   name: 'dynamic huginn'
16
-  # }
17
-  # calling agent.merge_json_path_options(event) returns the following hash:
18
-  # {
19
-  #   name: 'dynamic huginn'
20
-  #   title: 'Hello from Huginn'
21
-  # }
22
-
23
-  private
24
-  def merge_json_path_options(event)
25
-    options.select { |k, v| options_with_path.include? k}.tap do |merged_options|
26
-      options_with_path.each do |a|
27
-        merged_options[a] = select_option(event, a)
28
-      end
29
-    end
30
-  end
31
-
32
-  def select_option(event, a)
33
-    if options[a.to_s + '_path'].present?
34
-      Utils.value_at(event.payload, options[a.to_s + '_path'])
35
-    else
36
-      options[a]
37
-    end
38
-  end
39
-end

+ 34 - 0
app/concerns/liquid_interpolatable.rb

@@ -0,0 +1,34 @@
1
+module LiquidInterpolatable
2
+  extend ActiveSupport::Concern
3
+
4
+  def interpolate_options(options, payload)
5
+    case options.class.to_s
6
+    when 'String'
7
+      Liquid::Template.parse(options).render(payload)
8
+    when 'ActiveSupport::HashWithIndifferentAccess', 'Hash'
9
+      duped_options = options.dup
10
+      duped_options.each do |key, value|
11
+        duped_options[key] = interpolate_options(value, payload)
12
+      end
13
+    when 'Array'
14
+      options.collect do |value|
15
+        interpolate_options(value, payload)
16
+      end
17
+    end
18
+  end
19
+
20
+  def interpolate_string(string, payload)
21
+    Liquid::Template.parse(string).render(payload)
22
+  end
23
+
24
+  require 'uri'
25
+  # Percent encoding for URI conforming to RFC 3986.
26
+  # Ref: http://tools.ietf.org/html/rfc3986#page-12
27
+  module Huginn
28
+    def uri_escape(string)
29
+      CGI::escape string
30
+    end
31
+  end
32
+
33
+  Liquid::Template.register_filter(LiquidInterpolatable::Huginn)
34
+end

+ 0 - 4
app/models/agent.rb

@@ -121,10 +121,6 @@ class Agent < ActiveRecord::Base
121 121
     end
122 122
   end
123 123
 
124
-  def make_message(payload, message = options[:message])
125
-    message.gsub(/<([^>]+)>/) { Utils.value_at(payload, $1) || "??" }
126
-  end
127
-
128 124
   def trigger_web_request(params, method, format)
129 125
     if respond_to?(:receive_webhook)
130 126
       Rails.logger.warn "DEPRECATED: The .receive_webhook method is deprecated, please switch your Agent to use .receive_web_request."

+ 7 - 5
app/models/agents/data_output_agent.rb

@@ -1,5 +1,7 @@
1 1
 module Agents
2 2
   class DataOutputAgent < Agent
3
+    include LiquidInterpolatable
4
+
3 5
     cannot_be_scheduled!
4 6
 
5 7
     description  do
@@ -19,7 +21,7 @@ module Agents
19 21
 
20 22
           * `secrets` - An array of tokens that the requestor must provide for light-weight authentication.
21 23
           * `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents.
22
-          * `template` - A JSON object representing a mapping between item output keys and incoming event JSONPath values.  JSONPath values must start with `$`, or can be interpolated between `<` and `>` characters.  The `item` key will be repeated for every Event.
24
+          * `template` - A JSON object representing a mapping between item output keys and incoming event values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. The `item` key will be repeated for every Event.
23 25
       MD
24 26
     end
25 27
 
@@ -31,9 +33,9 @@ module Agents
31 33
           "title" => "XKCD comics as a feed",
32 34
           "description" => "This is a feed of recent XKCD comics, generated by Huginn",
33 35
           "item" => {
34
-            "title" => "$.title",
35
-            "description" => "Secret hovertext: <$.hovertext>",
36
-            "link" => "$.url",
36
+            "title" => "{{title}}",
37
+            "description" => "Secret hovertext: {{hovertext}}",
38
+            "link" => "{{url}}",
37 39
           }
38 40
         }
39 41
       }
@@ -82,7 +84,7 @@ module Agents
82 84
     def receive_web_request(params, method, format)
83 85
       if options['secrets'].include?(params['secret'])
84 86
         items = received_events.order('id desc').limit(events_to_show).map do |event|
85
-          interpolated = Utils.recursively_interpolate_jsonpaths(options['template']['item'], event.payload, :leading_dollarsign_is_jsonpath => true)
87
+          interpolated = interpolate_options(options['template']['item'], event.payload)
86 88
           interpolated['guid'] = event.id
87 89
           interpolated['pubDate'] = event.created_at.rfc2822.to_s
88 90
           interpolated

+ 13 - 12
app/models/agents/event_formatting_agent.rb

@@ -1,5 +1,6 @@
1 1
 module Agents
2 2
   class EventFormattingAgent < Agent
3
+    include LiquidInterpolatable
3 4
     cannot_be_scheduled!
4 5
 
5 6
     description <<-MD
@@ -24,11 +25,11 @@ module Agents
24 25
       You can use an Event Formatting Agent's `instructions` setting to do this in the following way:
25 26
 
26 27
           "instructions": {
27
-            "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.",
28
-            "subject": "$.data"
28
+            "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.",
29
+            "subject": "{{data}}"
29 30
           }
30 31
 
31
-      JSONPaths must be between < and > . Make sure that you don't use these symbols anywhere else.
32
+      Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
32 33
 
33 34
       Events generated by this possible Event Formatting Agent will look like:
34 35
 
@@ -42,7 +43,7 @@ module Agents
42 43
           {
43 44
             "matchers": [
44 45
               {
45
-                "path": "$.date.pretty",
46
+                "path": "{{date.pretty}}",
46 47
                 "regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
47 48
                 "to": "pretty_date",
48 49
               }
@@ -60,18 +61,18 @@ module Agents
60 61
       So you can use it in `instructions` like this:
61 62
 
62 63
           "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"
64
+            "message": "Today's conditions look like <$.conditions> with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.",
65
+            "subject": "{{data}}"
65 66
           }
66 67
 
67 68
       If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.
68 69
 
69 70
       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`.
70 71
 
71
-      To CGI escape output (for example when creating a link), prefix with `escape`, like so:
72
+      To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so:
72 73
 
73 74
           {
74
-            "message": "A peak was on Twitter in <$.group_by>.  Search: https://twitter.com/search?q=<escape $.group_by>"
75
+            "message": "A peak was on Twitter in {{group_by}}.  Search: https://twitter.com/search?q={{group_by | uri_escape}}"
75 76
           }
76 77
     MD
77 78
 
@@ -88,8 +89,8 @@ module Agents
88 89
     def default_options
89 90
       {
90 91
         'instructions' => {
91
-          'message' =>  "You received a text <$.text> from <$.fields.from>",
92
-          'some_other_field' => "Looks like the weather is going to be <$.fields.weather>"
92
+          'message' =>  "You received a text {{text}} from {{fields.from}}",
93
+          'some_other_field' => "Looks like the weather is going to be {{fields.weather}}"
93 94
         },
94 95
         'matchers' => [],
95 96
         'mode' => "clean",
@@ -106,7 +107,7 @@ module Agents
106 107
       incoming_events.each do |event|
107 108
         formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {}
108 109
         payload = perform_matching(event.payload)
109
-        options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) }
110
+        formatted_event.merge! interpolate_options(options['instructions'], payload)
110 111
         formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true"
111 112
         formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true"
112 113
         create_event :payload => formatted_event
@@ -161,7 +162,7 @@ module Agents
161 162
             re = Regexp.new(regexp)
162 163
             proc { |hash|
163 164
               mhash = {}
164
-              value = Utils.value_at(hash, path)
165
+              value = interpolate_string(path, hash)
165 166
               if value.is_a?(String) && (m = re.match(value))
166 167
                 m.to_a.each_with_index { |s, i|
167 168
                   mhash[i.to_s] = s

+ 3 - 13
app/models/agents/hipchat_agent.rb

@@ -1,6 +1,6 @@
1 1
 module Agents
2 2
   class HipchatAgent < Agent
3
-    include JsonPathOptionsOverwritable
3
+    include LiquidInterpolatable
4 4
 
5 5
     cannot_be_scheduled!
6 6
     cannot_create_events!
@@ -18,22 +18,17 @@ module Agents
18 18
       If you want your message to notify the room members change `notify` to "true".
19 19
       Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random")
20 20
 
21
-      If you want to specify either of those attributes per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them (except the `auth_token`).
21
+      Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
22 22
     MD
23 23
 
24 24
     def default_options
25 25
       {
26 26
         'auth_token' => '',
27 27
         'room_name' => '',
28
-        'room_name_path' => '',
29 28
         'username' => "Huginn",
30
-        'username_path' => '',
31 29
         'message' => "Hello from Huginn!",
32
-        'message_path' => '',
33 30
         'notify' => false,
34
-        'notify_path' => '',
35 31
         'color' => 'yellow',
36
-        'color_path' => '',
37 32
       }
38 33
     end
39 34
 
@@ -49,14 +44,9 @@ module Agents
49 44
     def receive(incoming_events)
50 45
       client = HipChat::Client.new(options[:auth_token])
51 46
       incoming_events.each do |event|
52
-        mo = merge_json_path_options event
47
+        mo = interpolate_options options, event.payload
53 48
         client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color])
54 49
       end
55 50
     end
56
-
57
-    private
58
-    def options_with_path
59
-      [:room_name, :username, :message, :notify, :color]
60
-    end
61 51
   end
62 52
 end

+ 10 - 8
app/models/agents/human_task_agent.rb

@@ -2,6 +2,8 @@ require 'rturk'
2 2
 
3 3
 module Agents
4 4
   class HumanTaskAgent < Agent
5
+    include LiquidInterpolatable
6
+
5 7
     default_schedule "every_10m"
6 8
 
7 9
     description <<-MD
@@ -16,7 +18,7 @@ module Agents
16 18
 
17 19
       # Example
18 20
 
19
-      If created with an event, all HIT fields can contain interpolated values via [JSONPaths](http://goessner.net/articles/JsonPath/) placed between < and > characters.
21
+      If created with an event, all HIT fields can contain interpolated values via [liquid templating](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid).
20 22
       For example, if the incoming event was a Twitter event, you could make a HITT to rate its sentiment like this:
21 23
 
22 24
           {
@@ -25,7 +27,7 @@ module Agents
25 27
             "hit": {
26 28
               "assignments": 1,
27 29
               "title": "Sentiment evaluation",
28
-              "description": "Please rate the sentiment of this message: '<$.message>'",
30
+              "description": "Please rate the sentiment of this message: '{{message}}'",
29 31
               "reward": 0.05,
30 32
               "lifetime_in_seconds": "3600",
31 33
               "questions": [
@@ -83,7 +85,7 @@ module Agents
83 85
               "title": "Take a poll about some jokes",
84 86
               "instructions": "Please rank these jokes from most funny (5) to least funny (1)",
85 87
               "assignments": 3,
86
-              "row_template": "<$.joke>"
88
+              "row_template": "{{joke}}"
87 89
             },
88 90
             "hit": {
89 91
               "assignments": 5,
@@ -168,7 +170,7 @@ module Agents
168 170
           {
169 171
             'assignments' => 1,
170 172
             'title' => "Sentiment evaluation",
171
-            'description' => "Please rate the sentiment of this message: '<$.message>'",
173
+            'description' => "Please rate the sentiment of this message: '{{message}}'",
172 174
             'reward' => 0.05,
173 175
             'lifetime_in_seconds' => 24 * 60 * 60,
174 176
             'questions' =>
@@ -332,7 +334,7 @@ module Agents
332 334
                   'name' => "Item #{index + 1}",
333 335
                   'key' => index,
334 336
                   'required' => "true",
335
-                  'question' => Utils.interpolate_jsonpaths(options['poll_options']['row_template'], assignments[index].answers),
337
+                  'question' => interpolate_string(options['poll_options']['row_template'], assignments[index].answers),
336 338
                   'selections' => selections
337 339
                 }
338 340
               end
@@ -387,9 +389,9 @@ module Agents
387 389
 
388 390
     def create_hit(opts = {})
389 391
       payload = opts['payload'] || {}
390
-      title = Utils.interpolate_jsonpaths(opts['title'], payload).strip
391
-      description = Utils.interpolate_jsonpaths(opts['description'], payload).strip
392
-      questions = Utils.recursively_interpolate_jsonpaths(opts['questions'], payload)
392
+      title = interpolate_string(opts['title'], payload).strip
393
+      description = interpolate_string(opts['description'], payload).strip
394
+      questions = interpolate_options(opts['questions'], payload)
393 395
       hit = RTurk::Hit.create(:title => title) do |hit|
394 396
         hit.max_assignments = (opts['assignments'] || 1).to_i
395 397
         hit.description = description

+ 7 - 3
app/models/agents/jabber_agent.rb

@@ -1,5 +1,7 @@
1 1
 module Agents
2 2
   class JabberAgent < Agent
3
+    include LiquidInterpolatable
4
+
3 5
     cannot_be_scheduled!
4 6
     cannot_create_events!
5 7
 
@@ -10,7 +12,9 @@ module Agents
10 12
 
11 13
       The `message` is sent from `jabber_sender` to `jaber_receiver`. This message
12 14
       can contain any keys found in the source's payload, escaped using double curly braces.
13
-      ex: `"News Story: <$.title>: <$.url>"`
15
+      ex: `"News Story: {{title}}: {{url}}"`
16
+
17
+      Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
14 18
     MD
15 19
 
16 20
     def default_options
@@ -20,7 +24,7 @@ module Agents
20 24
         'jabber_sender'   => 'huginn@localhost',
21 25
         'jabber_receiver' => 'muninn@localhost',
22 26
         'jabber_password' => '',
23
-        'message'         => 'It will be <$.temp> out tomorrow',
27
+        'message'         => 'It will be {{temp}} out tomorrow',
24 28
         'expected_receive_period_in_days' => "2"
25 29
       }
26 30
     end
@@ -58,7 +62,7 @@ module Agents
58 62
     end
59 63
 
60 64
     def body(event)
61
-      Utils.interpolate_jsonpaths(options['message'], event.payload)
65
+      interpolate_string(options['message'], event.payload)
62 66
     end
63 67
   end
64 68
 end

+ 4 - 2
app/models/agents/peak_detector_agent.rb

@@ -2,10 +2,12 @@ require 'pp'
2 2
 
3 3
 module Agents
4 4
   class PeakDetectorAgent < Agent
5
+    include LiquidInterpolatable
6
+
5 7
     cannot_be_scheduled!
6 8
 
7 9
     description <<-MD
8
-      Use a PeakDetectorAgent to watch for peaks in an event stream.  When a peak is detected, the resulting Event will have a payload message of `message`.  You can include extractions in the message, for example: `I saw a bar of: <foo.bar>`
10
+      Use a PeakDetectorAgent to watch for peaks in an event stream.  When a peak is detected, the resulting Event will have a payload message of `message`.  You can include extractions in the message, for example: `I saw a bar of: {{foo.bar}}`, have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details.
9 11
 
10 12
       The `value_path` value is a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value of interest.  `group_by_path` is a hash path that will be used to group values, if present.
11 13
 
@@ -67,7 +69,7 @@ module Agents
67 69
         if newest_value > average_value + std_multiple * standard_deviation
68 70
           memory['peaks'][group] << newest_time
69 71
           memory['peaks'][group].reject! { |p| p <= newest_time - window_duration }
70
-          create_event :payload => { 'message' => options['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s }
72
+          create_event :payload => { 'message' => interpolate_string(options['message'], event.payload), 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s }
71 73
         end
72 74
       end
73 75
     end

+ 8 - 15
app/models/agents/pushbullet_agent.rb

@@ -1,6 +1,6 @@
1 1
 module Agents
2 2
   class PushbulletAgent < Agent
3
-    include JsonPathOptionsOverwritable
3
+    include LiquidInterpolatable
4 4
 
5 5
     cannot_be_scheduled!
6 6
     cannot_create_events!
@@ -20,7 +20,7 @@ module Agents
20 20
 
21 21
       You can provide a `title` and a `body`.
22 22
 
23
-      If you want to specify `title` or `body` per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them.
23
+      In every value of the options hash you can use the liquid templating, learn more about it at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid).
24 24
     MD
25 25
 
26 26
     def default_options
@@ -28,9 +28,7 @@ module Agents
28 28
         'api_key' => '',
29 29
         'device_id' => '',
30 30
         'title' => "Hello from Huginn!",
31
-        'title_path' => '',
32
-        'body' => '',
33
-        'body_path' => '',
31
+        'body' => '{{body}}',
34 32
       }
35 33
     end
36 34
 
@@ -52,16 +50,11 @@ module Agents
52 50
 
53 51
     private
54 52
     def query_options(event)
55
-      mo = merge_json_path_options event
56
-      basic_options.deep_merge(:body => {:title => mo[:title], :body => mo[:body]})
57
-    end
58
-
59
-    def basic_options
60
-      {:basic_auth => {:username =>options[:api_key], :password=>''}, :body => {:device_iden => options[:device_id], :type => 'note'}}
61
-    end
62
-
63
-    def options_with_path
64
-      [:title, :body]
53
+      mo = interpolate_options options, event.payload
54
+      {
55
+        :basic_auth => {:username =>mo[:api_key], :password=>''},
56
+        :body => {:device_iden => mo[:device_id], :title => mo[:title], :body => mo[:body], :type => 'note'}
57
+      }
65 58
     end
66 59
   end
67 60
 end

+ 6 - 5
app/models/agents/translation_agent.rb

@@ -1,5 +1,6 @@
1 1
 module Agents
2 2
   class TranslationAgent < Agent
3
+    include LiquidInterpolatable
3 4
 
4 5
     cannot_be_scheduled!
5 6
 
@@ -8,7 +9,7 @@ module Agents
8 9
       Services are provided using Microsoft Translator. You can [sign up](https://datamarket.azure.com/dataset/bing/microsofttranslator) and [register your application](https://datamarket.azure.com/developer/applications/register) to get `client_id` and `client_secret` which are required to use this agent.
9 10
       `to` must be filled with a [translator language code](http://msdn.microsoft.com/en-us/library/hh456380.aspx).
10 11
 
11
-      Specify what you would like to translate in `content` field, by specifying key and JSONPath of content to be translated.
12
+      Specify what you would like to translate in `content` field, you can use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) specify which part of the payload you want to translate.
12 13
 
13 14
       `expected_receive_period_in_days` is the maximum number of days you would allow to pass between events.
14 15
     MD
@@ -22,8 +23,8 @@ module Agents
22 23
         'to' => "fi",
23 24
         'expected_receive_period_in_days' => 1,
24 25
         'content' => {
25
-          'text' => "$.message.text",
26
-          'content' => "$.xyz"
26
+          'text' => "{{message.text}}",
27
+          'content' => "{{xyz}}"
27 28
         }
28 29
       }
29 30
     end
@@ -68,8 +69,8 @@ module Agents
68 69
       incoming_events.each do |event|
69 70
         translated_event = {}
70 71
         options['content'].each_pair do |key, value|
71
-          to_be_translated = Utils.values_at event.payload, value
72
-          translated_event[key] = translate to_be_translated.first, options['to'], access_token
72
+          to_be_translated = interpolate_string(value, event.payload)
73
+          translated_event[key] = translate(to_be_translated.first, options['to'], access_token)
73 74
         end
74 75
         create_event :payload => translated_event
75 76
       end

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

@@ -1,5 +1,7 @@
1 1
 module Agents
2 2
   class TriggerAgent < Agent
3
+    include LiquidInterpolatable
4
+
3 5
     cannot_be_scheduled!
4 6
 
5 7
     VALID_COMPARISON_TYPES = %w[regex !regex field<value field<=value field==value field!=value field>=value field>value]
@@ -13,7 +15,7 @@ module Agents
13 15
 
14 16
       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 17
 
16
-      All rules must match for the Agent to match.  The resulting Event will have a payload message of `message`.  You can include extractions in the message, for example: `I saw a bar of: <foo.bar>`
18
+      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.
17 19
 
18 20
       Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided.
19 21
 
@@ -46,7 +48,7 @@ module Agents
46 48
                       'value' => "foo\\d+bar",
47 49
                       'path' => "topkey.subkey.subkey.goal",
48 50
                     }],
49
-        'message' => "Looks like your pattern matched in '<value>'!"
51
+        'message' => "Looks like your pattern matched in '{{value}}'!"
50 52
       }
51 53
     end
52 54
 
@@ -88,9 +90,9 @@ module Agents
88 90
         if match
89 91
           if keep_event?
90 92
             payload = event.payload.dup
91
-            payload['message'] = make_message(event[:payload]) if options['message'].present?
93
+            payload['message'] = interpolate_string(options['message'], event.payload) if options['message'].present?
92 94
           else
93
-            payload = { 'message' => make_message(event[:payload]) }
95
+            payload = { 'message' => interpolate_string(options['message'], event.payload) }
94 96
           end
95 97
 
96 98
           create_event :payload => payload

+ 4 - 3
app/models/agents/twitter_publish_agent.rb

@@ -3,6 +3,7 @@ require "twitter"
3 3
 module Agents
4 4
   class TwitterPublishAgent < Agent
5 5
     include TwitterConcern
6
+    include LiquidInterpolatable
6 7
 
7 8
     cannot_be_scheduled!
8 9
 
@@ -15,7 +16,7 @@ module Agents
15 16
 
16 17
       To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
17 18
 
18
-      You must also specify a `message_path` parameter: a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value to tweet.
19
+      You must also specify a `message` parameter, you can use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the message.
19 20
 
20 21
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
21 22
     MD
@@ -31,7 +32,7 @@ module Agents
31 32
     def default_options
32 33
       {
33 34
         'expected_update_period_in_days' => "10",
34
-        'message_path' => "text"
35
+        'message' => "{{text}}"
35 36
       }
36 37
     end
37 38
 
@@ -41,7 +42,7 @@ module Agents
41 42
         incoming_events = incoming_events.first(20)
42 43
       end
43 44
       incoming_events.each do |event|
44
-        tweet_text = Utils.value_at(event.payload, options['message_path'])
45
+        tweet_text = interpolate_string(options['message'], event.payload)
45 46
         begin
46 47
           tweet = publish_tweet tweet_text
47 48
           create_event :payload => {

+ 45 - 0
db/migrate/20140505201716_migrate_agents_to_liquid_templating.rb

@@ -0,0 +1,45 @@
1
+class MigrateAgentsToLiquidTemplating < ActiveRecord::Migration
2
+  def up
3
+    Agent.where(:type => 'Agents::HipchatAgent').each do |agent|
4
+      LiquidMigrator.convert_all_agent_options(agent)
5
+    end
6
+    Agent.where(:type => 'Agents::EventFormattingAgent').each do |agent|
7
+      agent.options['instructions'] = LiquidMigrator.convert_hash(agent.options['instructions'], {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true})
8
+      agent.save
9
+    end
10
+    Agent.where(:type => 'Agents::PushbulletAgent').each do |agent|
11
+      LiquidMigrator.convert_all_agent_options(agent)
12
+    end
13
+    Agent.where(:type => 'Agents::JabberAgent').each do |agent|
14
+      LiquidMigrator.convert_all_agent_options(agent)
15
+    end
16
+    Agent.where(:type => 'Agents::DataOutputAgent').each do |agent|
17
+      LiquidMigrator.convert_all_agent_options(agent)
18
+    end
19
+    Agent.where(:type => 'Agents::TranslationAgent').each do |agent|
20
+      agent.options['content'] = LiquidMigrator.convert_hash(agent.options['content'], {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true})
21
+      agent.save
22
+    end
23
+    Agent.where(:type => 'Agents::TwitterPublishAgent').each do |agent|
24
+      if (message = agent.options.delete('message_path')).present?
25
+        agent.options['message'] = "{{#{message}}}"
26
+        agent.save
27
+      end
28
+    end
29
+    Agent.where(:type => 'Agents::TriggerAgent').each do |agent|
30
+      agent.options['message'] = LiquidMigrator.convert_make_message(agent.options['message'])
31
+      agent.save
32
+    end
33
+    Agent.where(:type => 'Agents::PeakDetectorAgent').each do |agent|
34
+      agent.options['message'] = LiquidMigrator.convert_make_message(agent.options['message'])
35
+      agent.save
36
+    end
37
+    Agent.where(:type => 'Agents::HumanTaskAgent').each do |agent|
38
+      LiquidMigrator.convert_all_agent_options(agent)
39
+    end
40
+  end
41
+
42
+  def down
43
+    raise ActiveRecord::IrreversibleMigration, "Cannot revert migration to Liquid templating"
44
+  end
45
+end

+ 78 - 0
lib/liquid_migrator.rb

@@ -0,0 +1,78 @@
1
+module LiquidMigrator
2
+  def self.convert_all_agent_options(agent)
3
+    agent.options = self.convert_hash(agent.options, {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true})
4
+    agent.save!
5
+  end
6
+
7
+  def self.convert_hash(hash, options={})
8
+    options = {:merge_path_attributes => false, :leading_dollarsign_is_jsonpath => false}.merge options
9
+    keys_to_remove = []
10
+    hash.tap do |hash|
11
+      hash.each_pair do |key, value|
12
+        case value.class.to_s
13
+        when 'String', 'FalseClass', 'TrueClass'
14
+          path_key = "#{key}_path"
15
+          if options[:merge_path_attributes] && !hash[path_key].nil?
16
+            # replace the value if the path is present
17
+            value = hash[path_key] if hash[path_key].present?
18
+            # in any case delete the path attibute
19
+            keys_to_remove << path_key
20
+          end
21
+          hash[key] = LiquidMigrator.convert_string value, options[:leading_dollarsign_is_jsonpath]
22
+        when 'ActiveSupport::HashWithIndifferentAccess'
23
+          hash[key] = convert_hash(hash[key], options)
24
+        when 'Array'
25
+          hash[key] = hash[key].collect { |k|
26
+            if k.class == String
27
+              convert_string(k, options[:leading_dollarsign_is_jsonpath])
28
+            else
29
+              convert_hash(k, options)
30
+            end
31
+          }
32
+        end
33
+      end
34
+        # remove the unneeded *_path attributes
35
+    end.select { |k, v| !keys_to_remove.include? k }
36
+  end
37
+
38
+  def self.convert_string(string, leading_dollarsign_is_jsonpath=false)
39
+    if string == true || string == false
40
+      # there might be empty *_path attributes for boolean defaults
41
+      string
42
+    elsif string[0] == '$' && leading_dollarsign_is_jsonpath
43
+      # in most cases a *_path attribute
44
+      convert_json_path string
45
+    else
46
+      # migrate the old interpolation syntax to the new liquid based
47
+      string.gsub(/<([^>]+)>/).each do
48
+        match = $1
49
+        if match =~ /\Aescape /
50
+          # convert the old escape syntax to a liquid filter
51
+          self.convert_json_path(match.gsub(/\Aescape /, '').strip, ' | uri_escape')
52
+        else
53
+          self.convert_json_path(match.strip)
54
+        end
55
+      end
56
+    end
57
+  end
58
+
59
+  def self.convert_make_message(string)
60
+    string.gsub(/<([^>]+)>/, "{{\\1}}")
61
+  end
62
+
63
+  def self.convert_json_path(string, filter = "")
64
+    check_path(string)
65
+    if string.start_with? '$.'
66
+      "{{#{string[2..-1]}#{filter}}}"
67
+    else
68
+      "{{#{string[1..-1]}#{filter}}}"
69
+    end
70
+  end
71
+
72
+  def self.check_path(string)
73
+    if string !~ /\A(\$\.?)?(\w+\.)*(\w+)\Z/
74
+      raise "JSONPath '#{string}' is too complex, please check your migration."
75
+    end
76
+  end
77
+end
78
+

+ 156 - 0
spec/lib/liquid_migrator_spec.rb

@@ -0,0 +1,156 @@
1
+require 'spec_helper'
2
+
3
+describe LiquidMigrator do
4
+  describe "converting JSONPath strings" do
5
+    it "should work" do
6
+      LiquidMigrator.convert_string("$.data", true).should == "{{data}}"
7
+      LiquidMigrator.convert_string("$.data.test", true).should == "{{data.test}}"
8
+      LiquidMigrator.convert_string("$first_title", true).should == "{{first_title}}"
9
+    end
10
+
11
+    it "should ignore strings which just contain a JSONPath" do
12
+      LiquidMigrator.convert_string("$.data").should == "$.data"
13
+      LiquidMigrator.convert_string("$first_title").should == "$first_title"
14
+      LiquidMigrator.convert_string(" $.data", true).should == " $.data"
15
+      LiquidMigrator.convert_string("lorem $.data", true).should == "lorem $.data"
16
+    end
17
+    it "should raise an exception when encountering complex JSONPaths" do
18
+      expect { LiquidMigrator.convert_string("$.data.test.*", true) }.
19
+        to raise_error("JSONPath '$.data.test.*' is too complex, please check your migration.")
20
+    end
21
+  end
22
+
23
+  describe "converting escaped JSONPath strings" do
24
+    it "should work" do
25
+      LiquidMigrator.convert_string("Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>").should ==
26
+                                    "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}"
27
+    end
28
+
29
+    it "should convert the 'escape' method correctly" do
30
+      LiquidMigrator.convert_string("Escaped: <escape $.content.name>\nNot escaped: <$.content.name>").should ==
31
+                                    "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}"
32
+    end
33
+
34
+    it "should raise an exception when encountering complex JSONPaths" do
35
+      expect { LiquidMigrator.convert_string("Received <$.content.text.*> from <$.content.name> .") }.
36
+        to raise_error("JSONPath '$.content.text.*' is too complex, please check your migration.")
37
+    end
38
+  end
39
+
40
+  describe "migrating a hash" do
41
+    it "should convert every attribute" do
42
+      LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}).should ==
43
+                                  {'a' => "$.data", 'b' => "This is a {{test}}"}
44
+    end
45
+    it "should work with leading_dollarsign_is_jsonpath" do
46
+      LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}, leading_dollarsign_is_jsonpath: true).should ==
47
+                                  {'a' => "{{data}}", 'b' => "This is a {{test}}"}
48
+    end
49
+    it "should use the corresponding *_path attributes when using merge_path_attributes"do
50
+      LiquidMigrator.convert_hash({'a' => "default", 'a_path' => "$.data"}, {leading_dollarsign_is_jsonpath: true, merge_path_attributes: true}).should ==
51
+                                  {'a' => "{{data}}"}
52
+    end
53
+    it "should raise an exception when encountering complex JSONPaths" do
54
+      expect { LiquidMigrator.convert_hash({'b' => "This is <$.complex[2]>"}) }.
55
+        to raise_error("JSONPath '$.complex[2]' is too complex, please check your migration.")
56
+    end
57
+  end
58
+
59
+  describe "migrating the 'make_message' format" do
60
+    it "should work" do
61
+      LiquidMigrator.convert_make_message('<message>').should == '{{message}}'
62
+      LiquidMigrator.convert_make_message('<new.message>').should == '{{new.message}}'
63
+      LiquidMigrator.convert_make_message('Hello <world>. How is <nested.life>').should == 'Hello {{world}}. How is {{nested.life}}'
64
+    end
65
+  end
66
+
67
+  describe "migrating an actual agent" do
68
+    before do
69
+      valid_params = {
70
+                        'auth_token' => 'token',
71
+                        'room_name' => 'test',
72
+                        'room_name_path' => '',
73
+                        'username' => "Huginn",
74
+                        'username_path' => '$.username',
75
+                        'message' => "Hello from Huginn!",
76
+                        'message_path' => '$.message',
77
+                        'notify' => false,
78
+                        'notify_path' => '',
79
+                        'color' => 'yellow',
80
+                        'color_path' => '',
81
+                      }
82
+
83
+      @agent = Agents::HipchatAgent.new(:name => "somename", :options => valid_params)
84
+      @agent.user = users(:jane)
85
+      @agent.save!
86
+    end
87
+
88
+    it "should work" do
89
+      LiquidMigrator.convert_all_agent_options(@agent)
90
+      @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'}
91
+    end
92
+
93
+    it "should work with nested hashes" do
94
+      @agent.options['very'] = {'nested' => '$.value'}
95
+      LiquidMigrator.convert_all_agent_options(@agent)
96
+      @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'very' => {'nested' => '{{value}}'}, 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'}
97
+    end
98
+
99
+    it "should work with nested arrays" do
100
+      @agent.options['array'] = ["one", "$.two"]
101
+      LiquidMigrator.convert_all_agent_options(@agent)
102
+      @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'array' => ['one', '{{two}}'], 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'}
103
+    end
104
+
105
+    it "should raise an exception when encountering complex JSONPaths" do
106
+      @agent.options['username_path'] = "$.very.complex[*]"
107
+      expect { LiquidMigrator.convert_all_agent_options(@agent) }.
108
+        to raise_error("JSONPath '$.very.complex[*]' is too complex, please check your migration.")
109
+    end
110
+
111
+    it "should work with the human task agent" do
112
+      valid_params = {
113
+        'expected_receive_period_in_days' => 2,
114
+        'trigger_on' => "event",
115
+        'hit' =>
116
+          {
117
+            'assignments' => 1,
118
+            'title' => "Sentiment evaluation",
119
+            'description' => "Please rate the sentiment of this message: '<$.message>'",
120
+            'reward' => 0.05,
121
+            'lifetime_in_seconds' => 24 * 60 * 60,
122
+            'questions' =>
123
+              [
124
+                {
125
+                  'type' => "selection",
126
+                  'key' => "sentiment",
127
+                  'name' => "Sentiment",
128
+                  'required' => "true",
129
+                  'question' => "Please select the best sentiment value:",
130
+                  'selections' =>
131
+                    [
132
+                      { 'key' => "happy", 'text' => "Happy" },
133
+                      { 'key' => "sad", 'text' => "Sad" },
134
+                      { 'key' => "neutral", 'text' => "Neutral" }
135
+                    ]
136
+                },
137
+                {
138
+                  'type' => "free_text",
139
+                  'key' => "feedback",
140
+                  'name' => "Have any feedback for us?",
141
+                  'required' => "false",
142
+                  'question' => "Feedback",
143
+                  'default' => "Type here...",
144
+                  'min_length' => "2",
145
+                  'max_length' => "2000"
146
+                }
147
+              ]
148
+          }
149
+      }
150
+      @agent = Agents::HumanTaskAgent.new(:name => "somename", :options => valid_params)
151
+      @agent.user = users(:jane)
152
+      LiquidMigrator.convert_all_agent_options(@agent)
153
+      @agent.reload.options['hit']['description'].should == "Please rate the sentiment of this message: '{{message}}'"
154
+    end
155
+  end
156
+end

+ 3 - 0
spec/models/agents/data_output_agent_spec.rb

@@ -1,8 +1,11 @@
1 1
 # encoding: utf-8
2 2
 
3 3
 require 'spec_helper'
4
+require 'models/concerns/liquid_interpolatable'
4 5
 
5 6
 describe Agents::DataOutputAgent do
7
+  it_behaves_like LiquidInterpolatable
8
+
6 9
   let(:agent) do
7 10
     _agent = Agents::DataOutputAgent.new(:name => 'My Data Output Agent')
8 11
     _agent.options = _agent.default_options.merge('secrets' => ['secret1', 'secret2'], 'events_to_show' => 2)

+ 4 - 4
spec/models/agents/event_formatting_agent_spec.rb

@@ -6,13 +6,13 @@ describe Agents::EventFormattingAgent do
6 6
         :name => "somename",
7 7
         :options => {
8 8
             :instructions => {
9
-                :message => "Received <$.content.text.*> from <$.content.name> .",
10
-                :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>"
9
+                :message => "Received {{content.text}} from {{content.name}} .",
10
+                :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}"
11 11
             },
12 12
             :mode => "clean",
13 13
             :matchers => [
14 14
                 {
15
-                    :path => "$.date.pretty",
15
+                    :path => "{{date.pretty}}",
16 16
                     :regexp => "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
17 17
                     :to => "pretty_date",
18 18
                 },
@@ -82,7 +82,7 @@ describe Agents::EventFormattingAgent do
82 82
     it "should allow escaping" do
83 83
       @event.payload[:content][:name] = "escape this!?"
84 84
       @event.save!
85
-      @checker.options[:instructions][:message] = "Escaped: <escape $.content.name>\nNot escaped: <$.content.name>"
85
+      @checker.options[:instructions][:message] = "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}"
86 86
       @checker.save!
87 87
       @checker.receive([@event])
88 88
       Event.last.payload[:message].should == "Escaped: escape+this%21%3F\nNot escaped: escape this!?"

+ 4 - 9
spec/models/agents/hipchat_agent_spec.rb

@@ -1,22 +1,17 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/json_path_options_overwritable'
2
+require 'models/concerns/liquid_interpolatable'
3 3
 
4 4
 describe Agents::HipchatAgent do
5
-  it_behaves_like JsonPathOptionsOverwritable
5
+  it_behaves_like LiquidInterpolatable
6 6
 
7 7
   before(:each) do
8 8
     @valid_params = {
9 9
                       'auth_token' => 'token',
10 10
                       'room_name' => 'test',
11
-                      'room_name_path' => '',
12
-                      'username' => "Huginn",
13
-                      'username_path' => '$.username',
14
-                      'message' => "Hello from Huginn!",
15
-                      'message_path' => '$.message',
11
+                      'username' => "{{username}}",
12
+                      'message' => "{{message}}",
16 13
                       'notify' => false,
17
-                      'notify_path' => '',
18 14
                       'color' => 'yellow',
19
-                      'color_path' => '',
20 15
                     }
21 16
 
22 17
     @checker = Agents::HipchatAgent.new(:name => "somename", :options => @valid_params)

+ 12 - 9
spec/models/agents/human_task_agent_spec.rb

@@ -1,6 +1,9 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/liquid_interpolatable'
2 3
 
3 4
 describe Agents::HumanTaskAgent do
5
+  it_behaves_like LiquidInterpolatable
6
+
4 7
   before do
5 8
     @checker = Agents::HumanTaskAgent.new(:name => "my human task agent")
6 9
     @checker.options = @checker.default_options
@@ -116,19 +119,19 @@ describe Agents::HumanTaskAgent do
116 119
       @checker.options['poll_options'] = { 'title' => "Take a poll about jokes",
117 120
                                            'instructions' => "Rank these by how funny they are",
118 121
                                            'assignments' => 3,
119
-                                           'row_template' => "<$.joke>" }
122
+                                           'row_template' => "{{joke}}" }
120 123
       @checker.should be_valid
121 124
       @checker.options['poll_options'] = { 'instructions' => "Rank these by how funny they are",
122 125
                                            'assignments' => 3,
123
-                                           'row_template' => "<$.joke>" }
126
+                                           'row_template' => "{{joke}}" }
124 127
       @checker.should_not be_valid
125 128
       @checker.options['poll_options'] = { 'title' => "Take a poll about jokes",
126 129
                                            'assignments' => 3,
127
-                                           'row_template' => "<$.joke>" }
130
+                                           'row_template' => "{{joke}}" }
128 131
       @checker.should_not be_valid
129 132
       @checker.options['poll_options'] = { 'title' => "Take a poll about jokes",
130 133
                                            'instructions' => "Rank these by how funny they are",
131
-                                           'row_template' => "<$.joke>" }
134
+                                           'row_template' => "{{joke}}" }
132 135
       @checker.should_not be_valid
133 136
       @checker.options['poll_options'] = { 'title' => "Take a poll about jokes",
134 137
                                            'instructions' => "Rank these by how funny they are",
@@ -207,9 +210,9 @@ describe Agents::HumanTaskAgent do
207 210
 
208 211
   describe "creating hits" do
209 212
     it "can create HITs based on events, interpolating their values" do
210
-      @checker.options['hit']['title'] = "Hi <.name>"
211
-      @checker.options['hit']['description'] = "Make something for <.name>"
212
-      @checker.options['hit']['questions'][0]['name'] = "<.name> Question 1"
213
+      @checker.options['hit']['title'] = "Hi {{name}}"
214
+      @checker.options['hit']['description'] = "Make something for {{name}}"
215
+      @checker.options['hit']['questions'][0]['name'] = "{{name}} Question 1"
213 216
 
214 217
       question_form = nil
215 218
       hitInterface = OpenStruct.new
@@ -232,7 +235,7 @@ describe Agents::HumanTaskAgent do
232 235
     end
233 236
 
234 237
     it "works without an event too" do
235
-      @checker.options['hit']['title'] = "Hi <.name>"
238
+      @checker.options['hit']['title'] = "Hi {{name}}"
236 239
       hitInterface = OpenStruct.new
237 240
       hitInterface.id = 123
238 241
       mock(hitInterface).question_form(instance_of Agents::HumanTaskAgent::AgentQuestionForm)
@@ -483,7 +486,7 @@ describe Agents::HumanTaskAgent do
483 486
           'title' => "Hi!",
484 487
           'instructions' => "hello!",
485 488
           'assignments' => 2,
486
-          'row_template' => "This is <.sentiment>"
489
+          'row_template' => "This is {{sentiment}}"
487 490
         }
488 491
         @event.save!
489 492
         mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } }

+ 4 - 1
spec/models/agents/jabber_agent_spec.rb

@@ -1,6 +1,9 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/liquid_interpolatable'
2 3
 
3 4
 describe Agents::JabberAgent do
5
+  it_behaves_like LiquidInterpolatable
6
+
4 7
   let(:sent) { [] }
5 8
   let(:config) {
6 9
     {
@@ -9,7 +12,7 @@ describe Agents::JabberAgent do
9 12
       jabber_sender: 'foo@localhost',
10 13
       jabber_receiver: 'bar@localhost',
11 14
       jabber_password: 'password',
12
-      message: 'Warning! <$.title> - <$.url>',
15
+      message: 'Warning! {{title}} - {{url}}',
13 16
       expected_receive_period_in_days: '2'
14 17
     }
15 18
   }

+ 3 - 0
spec/models/agents/peak_detector_agent_spec.rb

@@ -1,6 +1,9 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/liquid_interpolatable'
2 3
 
3 4
 describe Agents::PeakDetectorAgent do
5
+  it_behaves_like LiquidInterpolatable
6
+
4 7
   before do
5 8
     @valid_params = {
6 9
         'name' => "my peak detector agent",

+ 9 - 14
spec/models/agents/pushbullet_agent_spec.rb

@@ -1,14 +1,14 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/json_path_options_overwritable'
2
+require 'models/concerns/liquid_interpolatable'
3 3
 
4 4
 describe Agents::PushbulletAgent do
5
-  it_behaves_like JsonPathOptionsOverwritable
5
+  it_behaves_like LiquidInterpolatable
6 6
 
7 7
   before(:each) do
8 8
     @valid_params = {
9 9
                       'api_key' => 'token',
10 10
                       'device_id' => '124',
11
-                      'body_path' => '$.body',
11
+                      'body' => '{{body}}',
12 12
                       'title' => 'hello from huginn'
13 13
                     }
14 14
 
@@ -39,23 +39,18 @@ describe Agents::PushbulletAgent do
39 39
   end
40 40
 
41 41
   describe "helpers" do
42
-    it "it should return the correct basic_options" do
43
-      @checker.send(:basic_options).should == {:basic_auth => {:username =>@checker.options[:api_key], :password=>''},
44
-                                               :body => {:device_iden => @checker.options[:device_id], :type => 'note'}}
45
-    end
46
-
47
-
48 42
     it "should return the query_options" do
49
-      @checker.send(:query_options, @event).should == @checker.send(:basic_options).deep_merge({
50
-        :body => {:title => 'hello from huginn', :body => 'One two test'}
51
-      })
43
+      @checker.send(:query_options, @event).should == {
44
+        :body => {:title => 'hello from huginn', :body => 'One two test', :device_iden => @checker.options[:device_id], :type => 'note'},
45
+        :basic_auth => {:username =>@checker.options[:api_key], :password=>''}
46
+      }
52 47
     end
53 48
   end
54 49
 
55 50
   describe "#receive" do
56 51
     it "send a message to the hipchat" do
57 52
       stub_request(:post, "https://token:@api.pushbullet.com/api/pushes").
58
-        with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test").
53
+        with(:body => "device_iden=124&title=hello%20from%20huginn&body=One%20two%20test&type=note").
59 54
         to_return(:status => 200, :body => "ok", :headers => {})
60 55
       dont_allow(@checker).error
61 56
       @checker.receive([@event])
@@ -63,7 +58,7 @@ describe Agents::PushbulletAgent do
63 58
 
64 59
     it "should log resquests which return an error" do
65 60
       stub_request(:post, "https://token:@api.pushbullet.com/api/pushes").
66
-        with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test").
61
+        with(:body => "device_iden=124&title=hello%20from%20huginn&body=One%20two%20test&type=note").
67 62
         to_return(:status => 200, :body => "error", :headers => {})
68 63
       mock(@checker).error("error")
69 64
       @checker.receive([@event])

+ 6 - 2
spec/models/agents/translation_agent_spec.rb

@@ -1,6 +1,10 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/liquid_interpolatable'
3
+
2 4
 
3 5
 describe Agents::TranslationAgent do
6
+    it_behaves_like LiquidInterpolatable
7
+
4 8
     before do
5 9
         @valid_params = {
6 10
             :name    => "somename",
@@ -10,8 +14,8 @@ describe Agents::TranslationAgent do
10 14
                 :to            => "fi",
11 15
                 :expected_receive_period_in_days => 1,
12 16
                 :content       => {
13
-                    :text => "$.message",
14
-                    :content => "$.xyz"
17
+                    :text => "{{message}}",
18
+                    :content => "{{xyz}}"
15 19
                 }
16 20
             }
17 21
         }

+ 4 - 1
spec/models/agents/trigger_agent_spec.rb

@@ -1,6 +1,9 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/liquid_interpolatable'
2 3
 
3 4
 describe Agents::TriggerAgent do
5
+  it_behaves_like LiquidInterpolatable
6
+
4 7
   before do
5 8
     @valid_params = {
6 9
       'name' => "my trigger agent",
@@ -11,7 +14,7 @@ describe Agents::TriggerAgent do
11 14
                       'value' => "a\\db",
12 15
                       'path' => "foo.bar.baz",
13 16
                     }],
14
-        'message' => "I saw '<foo.bar.baz>' from <name>"
17
+        'message' => "I saw '{{foo.bar.baz}}' from {{name}}"
15 18
       }
16 19
     }
17 20
 

+ 1 - 1
spec/models/agents/twitter_publish_agent_spec.rb

@@ -9,7 +9,7 @@ describe Agents::TwitterPublishAgent do
9 9
       :consumer_secret => "---",
10 10
       :oauth_token => "---",
11 11
       :oauth_token_secret => "---",
12
-      :message_path => "text"
12
+      :message => "{{text}}"
13 13
     }
14 14
 
15 15
     @checker = Agents::TwitterPublishAgent.new(:name => "HuginnBot", :options => @opts)

+ 0 - 31
spec/models/concerns/json_path_options_overwritable.rb

@@ -1,31 +0,0 @@
1
-require 'spec_helper'
2
-
3
-shared_examples_for JsonPathOptionsOverwritable do
4
-  before(:each) do
5
-    @valid_params = described_class.new.default_options
6
-
7
-    @checker = described_class.new(:name => "somename", :options => @valid_params)
8
-    @checker.user = users(:jane)
9
-
10
-    @event = Event.new
11
-    @event.agent = agents(:bob_weather_agent)
12
-    @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"}
13
-    @event.save!
14
-  end
15
-
16
-  describe "select_option" do
17
-    it "should use the room_name_path if specified" do
18
-      @checker.options['room_name_path'] = "$.room_name"
19
-      @checker.send(:select_option, @event, :room_name).should == "test room"
20
-    end
21
-
22
-    it "should use the normal option when the path option is blank" do
23
-      @checker.options['room_name'] = 'test'
24
-      @checker.send(:select_option, @event, :room_name).should == "test"
25
-    end
26
-  end
27
-
28
-  it "should merge all options" do
29
-    @checker.send(:merge_json_path_options, @event).symbolize_keys.keys.should == @checker.send(:options_with_path)
30
-  end
31
-end

+ 56 - 0
spec/models/concerns/liquid_interpolatable.rb

@@ -0,0 +1,56 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for LiquidInterpolatable do
4
+  before(:each) do
5
+    @valid_params = {
6
+      "normal" => "just some normal text",
7
+      "variable" => "{{variable}}",
8
+      "text" => "Some test with an embedded {{variable}}",
9
+      "escape" => "This should be {{hello_world | uri_escape}}"
10
+    }
11
+
12
+    @checker = described_class.new(:name => "somename", :options => @valid_params)
13
+    @checker.user = users(:jane)
14
+
15
+    @event = Event.new
16
+    @event.agent = agents(:bob_weather_agent)
17
+    @event.payload = { :variable => 'hello', :hello_world => "Hello world"}
18
+    @event.save!
19
+  end
20
+
21
+  describe "interpolating liquid templates" do
22
+    it "should work" do
23
+      @checker.interpolate_options(@checker.options, @event.payload).should == {
24
+          "normal" => "just some normal text",
25
+          "variable" => "hello",
26
+          "text" => "Some test with an embedded hello",
27
+          "escape" => "This should be Hello+world"
28
+      }
29
+    end
30
+
31
+    it "hsould work with arrays", focus: true do
32
+      @checker.options = {"value" => ["{{variable}}", "Much array", "Hey, {{hello_world}}"]}
33
+      @checker.interpolate_options(@checker.options, @event.payload).should == {
34
+        "value" => ["hello", "Much array", "Hey, Hello world"]
35
+      }
36
+    end
37
+
38
+    it "should work recursively" do
39
+      @checker.options['hash'] = {'recursive' => "{{variable}}"}
40
+      @checker.options['indifferent_hash'] = ActiveSupport::HashWithIndifferentAccess.new({'recursive' => "{{variable}}"})
41
+      @checker.interpolate_options(@checker.options, @event.payload).should == {
42
+          "normal" => "just some normal text",
43
+          "variable" => "hello",
44
+          "text" => "Some test with an embedded hello",
45
+          "escape" => "This should be Hello+world",
46
+          "hash" => {'recursive' => 'hello'},
47
+          "indifferent_hash" => {'recursive' => 'hello'},
48
+      }
49
+    end
50
+
51
+    it "should work for strings" do
52
+      @checker.send(:interpolate_string, "{{variable}}", @event.payload).should == "hello"
53
+      @checker.send(:interpolate_string, "{{variable}} you", @event.payload).should == "hello you"
54
+    end
55
+  end
56
+end