Merge pull request #843 from cantino/data_output_agent_can_build_complex_xml

allow DataOutputAgent to build complex XML

Andrew Cantino 9 年之前
父节点
当前提交
6f8b2ba221

+ 86 - 6
app/models/agents/data_output_agent.rb

@@ -22,6 +22,23 @@ module Agents
22 22
           * `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. The `pubDate` key for each item will have the creation time of the Event unless given.
23 23
           * `events_to_show` - The number of events to output in RSS or JSON. (default: `40`)
24 24
           * `ttl` - A value for the <ttl> element in RSS output. (default: `60`)
25
+
26
+        If you'd like to output RSS tags with attributes, such as `enclosure`, use something like the following in your `template`:
27
+
28
+            "enclosure": {
29
+              "_attributes": {
30
+                "url": "{{media_url}}",
31
+                "length": "1234456789",
32
+                "type": "audio/mpeg"
33
+              }
34
+            },
35
+            "another_tag": {
36
+              "_attributes": {
37
+                "key": "value",
38
+                "another_key": "another_value"
39
+              },
40
+              "_contents": "tag contents (can be an object for nesting)"
41
+            }
25 42
       MD
26 43
     end
27 44
 
@@ -35,15 +52,12 @@ module Agents
35 52
           "item" => {
36 53
             "title" => "{{title}}",
37 54
             "description" => "Secret hovertext: {{hovertext}}",
38
-            "link" => "{{url}}",
55
+            "link" => "{{url}}"
39 56
           }
40 57
         }
41 58
       }
42 59
     end
43 60
 
44
-    #"guid" => "",
45
-    #  "pubDate" => ""
46
-
47 61
     def working?
48 62
       last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
49 63
     end
@@ -104,7 +118,7 @@ module Agents
104 118
             'title' => feed_title,
105 119
             'description' => feed_description,
106 120
             'pubDate' => Time.now,
107
-            'items' => items
121
+            'items' => simplify_item_for_json(items)
108 122
           }
109 123
 
110 124
           return [content, 200]
@@ -122,7 +136,7 @@ module Agents
122 136
 
123 137
           XML
124 138
 
125
-          content += items.to_xml(:skip_types => true, :root => "items", :skip_instruct => true, :indent => 1).gsub(/^<\/?items>/, '').strip
139
+          content += simplify_item_for_xml(items).to_xml(skip_types: true, root: "items", skip_instruct: true, indent: 1).gsub(/^<\/?items>/, '').strip
126 140
 
127 141
           content += Utils.unindent(<<-XML)
128 142
             </channel>
@@ -139,5 +153,71 @@ module Agents
139 153
         end
140 154
       end
141 155
     end
156
+
157
+    private
158
+
159
+    class XMLNode
160
+      def initialize(tag_name, attributes, contents)
161
+        @tag_name, @attributes, @contents = tag_name, attributes, contents
162
+      end
163
+
164
+      def to_xml(options)
165
+        if @contents.is_a?(Hash)
166
+          options[:builder].tag! @tag_name, @attributes do
167
+            @contents.each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options.merge(skip_instruct: true)) }
168
+          end
169
+        else
170
+          options[:builder].tag! @tag_name, @attributes, @contents
171
+        end
172
+      end
173
+    end
174
+
175
+    def simplify_item_for_xml(item)
176
+      if item.is_a?(Hash)
177
+        item.each.with_object({}) do |(key, value), memo|
178
+          if value.is_a?(Hash)
179
+            if value.key?('_attributes') || value.key?('_contents')
180
+              memo[key] = XMLNode.new(key, value['_attributes'], simplify_item_for_xml(value['_contents']))
181
+            else
182
+              memo[key] = simplify_item_for_xml(value)
183
+            end
184
+          else
185
+            memo[key] = value
186
+          end
187
+        end
188
+      elsif item.is_a?(Array)
189
+        item.map { |value| simplify_item_for_xml(value) }
190
+      else
191
+        item
192
+      end
193
+    end
194
+
195
+    def simplify_item_for_json(item)
196
+      if item.is_a?(Hash)
197
+        item.each.with_object({}) do |(key, value), memo|
198
+          if value.is_a?(Hash)
199
+            if value.key?('_attributes') || value.key?('_contents')
200
+              contents = if value['_contents'] && value['_contents'].is_a?(Hash)
201
+                           simplify_item_for_json(value['_contents'])
202
+                         elsif value['_contents']
203
+                           { "contents" => value['_contents'] }
204
+                         else
205
+                           {}
206
+                         end
207
+
208
+              memo[key] = contents.merge(value['_attributes'] || {})
209
+            else
210
+              memo[key] = simplify_item_for_json(value)
211
+            end
212
+          else
213
+            memo[key] = value
214
+          end
215
+        end
216
+      elsif item.is_a?(Array)
217
+        item.map { |value| simplify_item_for_json(value) }
218
+      else
219
+        item
220
+      end
221
+    end
142 222
   end
143 223
 end

+ 3 - 1
app/models/agents/wunderlist_agent.rb

@@ -53,7 +53,9 @@ module Agents
53 53
         end
54 54
       end
55 55
     end
56
-  private
56
+
57
+    private
58
+
57 59
     def request_guard(&blk)
58 60
       response = yield
59 61
       error("Error during http request: #{response.body}") if response.code > 400

+ 131 - 1
spec/models/agents/data_output_agent_spec.rb

@@ -83,7 +83,7 @@ describe Agents::DataOutputAgent do
83 83
       expect(status).to eq(200)
84 84
     end
85 85
 
86
-    describe "returning events as RSS and JSON" do
86
+    describe "outputtng events as RSS and JSON" do
87 87
       let!(:event1) do
88 88
         agents(:bob_website_agent).create_event :payload => {
89 89
           "url" => "http://imgs.xkcd.com/comics/evolving.png",
@@ -194,5 +194,135 @@ describe Agents::DataOutputAgent do
194 194
         })
195 195
       end
196 196
     end
197
+
198
+    describe "outputting nesting" do
199
+      before do
200
+        agent.options['template']['item']['enclosure'] = {
201
+          "_attributes" => {
202
+            "type" => "audio/mpeg",
203
+            "url" => "{{media_url}}"
204
+          }
205
+        }
206
+        agent.options['template']['item']['foo'] = {
207
+          "_attributes" => {
208
+            "attr" => "attr-value-{{foo}}"
209
+          },
210
+          "_contents" => "Foo: {{foo}}"
211
+        }
212
+        agent.options['template']['item']['nested'] = {
213
+          "_attributes" => {
214
+            "key" => "value"
215
+          },
216
+          "_contents" => {
217
+            "title" => "some title"
218
+          }
219
+        }
220
+        agent.options['template']['item']['simpleNested'] = {
221
+          "title" => "some title",
222
+          "complex" => {
223
+            "_attributes" => {
224
+              "key" => "value"
225
+            },
226
+            "_contents" => {
227
+              "first" => {
228
+                "_attributes" => {
229
+                  "a" => "b"
230
+                },
231
+                "_contents" => {
232
+                  "second" => "value"
233
+                }
234
+              }
235
+            }
236
+          }
237
+        }
238
+        agent.save!
239
+      end
240
+
241
+      let!(:event) do
242
+        agents(:bob_website_agent).create_event :payload => {
243
+          "url" => "http://imgs.xkcd.com/comics/evolving.png",
244
+          "title" => "Evolving",
245
+          "hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.",
246
+          "media_url" => "http://google.com/audio.mpeg",
247
+          "foo" => 1
248
+        }
249
+      end
250
+
251
+      it "can output JSON" do
252
+        content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
253
+        expect(status).to eq(200)
254
+        expect(content['items'].first).to eq(
255
+          {
256
+            'title' => 'Evolving',
257
+            'description' => 'Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.',
258
+            'link' => 'http://imgs.xkcd.com/comics/evolving.png',
259
+            'guid' => event.id,
260
+            'pubDate' => event.created_at.rfc2822,
261
+            'enclosure' => {
262
+              "type" => "audio/mpeg",
263
+              "url" => "http://google.com/audio.mpeg"
264
+            },
265
+            'foo' => {
266
+              'attr' => 'attr-value-1',
267
+              'contents' => 'Foo: 1'
268
+            },
269
+            'nested' => {
270
+              "key" => "value",
271
+              "title" => "some title"
272
+            },
273
+            'simpleNested' => {
274
+              "title" => "some title",
275
+              "complex" => {
276
+                "key"=>"value",
277
+                "first" => {
278
+                  "a" => "b",
279
+                  "second"=>"value"
280
+                }
281
+              }
282
+            }
283
+          }
284
+        )
285
+      end
286
+
287
+      it "can output RSS" do
288
+        stub(agent).feed_link { "https://yoursite.com" }
289
+        content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
290
+        expect(status).to eq(200)
291
+        expect(content_type).to eq('text/xml')
292
+        expect(content.gsub(/\s+/, '')).to eq Utils.unindent(<<-XML).gsub(/\s+/, '')
293
+          <?xml version="1.0" encoding="UTF-8" ?>
294
+          <rss version="2.0">
295
+          <channel>
296
+           <title>XKCD comics as a feed</title>
297
+           <description>This is a feed of recent XKCD comics, generated by Huginn</description>
298
+           <link>https://yoursite.com</link>
299
+           <lastBuildDate>#{Time.now.rfc2822}</lastBuildDate>
300
+           <pubDate>#{Time.now.rfc2822}</pubDate>
301
+           <ttl>60</ttl>
302
+
303
+           <item>
304
+             <title>Evolving</title>
305
+             <description>Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.</description>
306
+             <link>http://imgs.xkcd.com/comics/evolving.png</link>
307
+             <pubDate>#{event.created_at.rfc2822}</pubDate>
308
+             <enclosure type="audio/mpeg" url="http://google.com/audio.mpeg" />
309
+             <foo attr="attr-value-1">Foo: 1</foo>
310
+             <nested key="value"><title>some title</title></nested>
311
+             <simpleNested>
312
+               <title>some title</title>
313
+               <complex key="value">
314
+                 <first a="b">
315
+                   <second>value</second>
316
+                 </first>
317
+               </complex>
318
+             </simpleNested>
319
+             <guid>#{event.id}</guid>
320
+           </item>
321
+
322
+          </channel>
323
+          </rss>
324
+        XML
325
+      end
326
+    end
197 327
   end
198 328
 end

+ 2 - 1
spec/models/agents/wunderlist_agent_spec.rb

@@ -54,8 +54,9 @@ describe Agents::WunderlistAgent do
54 54
 
55 55
   describe "#receive" do
56 56
     it "send a message to the hipchat" do
57
-      stub_request(:post, 'https://a.wunderlist.com/api/v1/tasks').with { |request| request.body == 'abc'}
57
+      stub_request(:post, 'https://a.wunderlist.com/api/v1/tasks')
58 58
       @checker.receive([@event])
59
+      expect(WebMock).to have_requested(:post, "https://a.wunderlist.com/api/v1/tasks")
59 60
     end
60 61
   end
61 62