hootenanny 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +5 -7
  4. data/Gemfile.lock +46 -28
  5. data/app/controllers/hootenanny/notifications_controller.rb +31 -0
  6. data/app/controllers/hootenanny/parameters.rb +16 -0
  7. data/app/controllers/hootenanny/subscriptions_controller.rb +24 -3
  8. data/app/models/hootenanny/publish_notification.rb +90 -0
  9. data/app/models/hootenanny/subscription.rb +72 -23
  10. data/config/routes.rb +2 -1
  11. data/db/migrate/20130607182642_add_started_at_and_lease_duration_to_subscriptions.rb +15 -0
  12. data/db/migrate/20130608225621_add_hmac_secret_to_subscription.rb +5 -0
  13. data/db/migrate/20130611235218_add_publish_notifications.rb +9 -0
  14. data/db/migrate/20130612153138_add_timestamps_to_publish_notification.rb +5 -0
  15. data/db/migrate/20130705200729_add_processed_flag_to_publish_notifications.rb +6 -0
  16. data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb +11 -0
  17. data/db/migrate/20130711061558_add_index_to_notifications_state.rb +5 -0
  18. data/hootenanny.gemspec +6 -2
  19. data/lib/hootenanny/configuration.rb +43 -0
  20. data/lib/hootenanny/correspondent.rb +44 -0
  21. data/lib/hootenanny/errors.rb +42 -1
  22. data/lib/hootenanny/feed.rb +75 -0
  23. data/lib/hootenanny/feed/atom_feed.rb +18 -0
  24. data/lib/hootenanny/feed/atom_feed_item.rb +8 -0
  25. data/lib/hootenanny/feed/digest_feed.rb +14 -0
  26. data/lib/hootenanny/feed/digest_feed_item.rb +11 -0
  27. data/lib/hootenanny/feed/feed_item.rb +30 -0
  28. data/lib/hootenanny/feed/file.rb +66 -0
  29. data/lib/hootenanny/feed/json_feed.rb +48 -0
  30. data/lib/hootenanny/feed/json_feed_item.rb +8 -0
  31. data/lib/hootenanny/feed/null_feed.rb +27 -0
  32. data/lib/hootenanny/feed/rss_feed.rb +52 -0
  33. data/lib/hootenanny/feed/rss_feed_item.rb +8 -0
  34. data/lib/hootenanny/feed_store.rb +30 -0
  35. data/lib/hootenanny/feed_store/file_feed_store.rb +55 -0
  36. data/lib/hootenanny/feed_store/web_feed_store.rb +42 -0
  37. data/lib/hootenanny/hub.rb +116 -22
  38. data/lib/hootenanny/publish_notification_expiration_policy.rb +17 -0
  39. data/lib/hootenanny/request.rb +40 -0
  40. data/lib/hootenanny/request/publish_notification.rb +94 -0
  41. data/lib/hootenanny/request/subscription.rb +153 -0
  42. data/lib/hootenanny/subscription_delivery.rb +47 -0
  43. data/lib/hootenanny/topic.rb +38 -0
  44. data/lib/hootenanny/topic_synchronizer.rb +71 -0
  45. data/lib/hootenanny/uri.rb +53 -0
  46. data/lib/hootenanny/verification.rb +108 -0
  47. data/lib/hootenanny/version.rb +1 -1
  48. data/spec/dummy/config/database.yml +12 -6
  49. data/spec/dummy/db/migrate/20130607183149_add_started_at_and_lease_duration_to_subscriptions.hootenanny.rb +16 -0
  50. data/spec/dummy/db/migrate/20130608231253_add_hmac_secret_to_subscription.hootenanny.rb +6 -0
  51. data/spec/dummy/db/migrate/20130611235546_add_publish_notifications.hootenanny.rb +10 -0
  52. data/spec/dummy/db/migrate/20130612153353_add_timestamps_to_publish_notification.hootenanny.rb +6 -0
  53. data/spec/dummy/db/migrate/20130705200832_add_processed_flag_to_publish_notifications.hootenanny.rb +7 -0
  54. data/spec/dummy/db/migrate/20130711061518_switch_publish_notification_process_state_from_boolean_to_string.hootenanny.rb +12 -0
  55. data/spec/dummy/db/migrate/20130711061629_add_index_to_notifications_state.hootenanny.rb +6 -0
  56. data/spec/factories/publish_notification.rb +13 -0
  57. data/spec/factories/requests/publish_notification.rb +11 -0
  58. data/spec/factories/requests/subscription.rb +46 -0
  59. data/spec/factories/subscription.rb +14 -2
  60. data/spec/factories/verification.rb +15 -0
  61. data/spec/features/publishers/can_notify_the_hub_of_content_updates_spec.rb +58 -0
  62. data/spec/features/subscribers/are_protected_from_unwarranted_subscriptions_spec.rb +59 -0
  63. data/spec/features/subscribers/can_receive_distributions_of_topic_content_spec.rb +175 -0
  64. data/spec/features/subscribers/can_subscribe_to_a_topic_spec.rb +76 -7
  65. data/spec/fixtures/feeds/atom/97d79220f68b4bf27.atom +0 -0
  66. data/spec/fixtures/feeds/atom/sample_feed.atom +51 -0
  67. data/spec/fixtures/feeds/digest/97d79220f68b4bf27.digest +1 -0
  68. data/spec/fixtures/feeds/digest/complete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +6 -0
  69. data/spec/fixtures/feeds/digest/incomplete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +5 -0
  70. data/spec/fixtures/feeds/digest/sample_feed.digest +6 -0
  71. data/spec/fixtures/feeds/json/97d79220f68b4bf27.json +1 -0
  72. data/spec/fixtures/feeds/json/feed_with_one_item.json +21 -0
  73. data/spec/fixtures/feeds/json/sample_feed.json +30 -0
  74. data/spec/fixtures/feeds/rss/5b187098da59f077f/97d79220f68b4bf27.rss +21 -0
  75. data/spec/fixtures/feeds/rss/97d79220f68b4bf27.rss +0 -0
  76. data/spec/fixtures/feeds/rss/feed_with_one_item.rss +15 -0
  77. data/spec/fixtures/feeds/rss/minimal_feed.rss +12 -0
  78. data/spec/fixtures/feeds/rss/sample_feed.rss +22 -0
  79. data/spec/fixtures/feeds/rss/sample_feed_2.rss +22 -0
  80. data/spec/lib/hootenanny/configuration_spec.rb +7 -0
  81. data/spec/lib/hootenanny/correspondent_spec.rb +94 -0
  82. data/spec/lib/hootenanny/errors_spec.rb +21 -0
  83. data/spec/lib/hootenanny/feed/atom_feed_item_spec.rb +9 -0
  84. data/spec/lib/hootenanny/feed/atom_feed_spec.rb +40 -0
  85. data/spec/lib/hootenanny/feed/digest_feed_item_spec.rb +9 -0
  86. data/spec/lib/hootenanny/feed/digest_feed_spec.rb +40 -0
  87. data/spec/lib/hootenanny/feed/feed_item_spec.rb +49 -0
  88. data/spec/lib/hootenanny/feed/file_spec.rb +66 -0
  89. data/spec/lib/hootenanny/feed/json_feed_item_spec.rb +9 -0
  90. data/spec/lib/hootenanny/feed/json_feed_spec.rb +128 -0
  91. data/spec/lib/hootenanny/feed/null_feed_spec.rb +9 -0
  92. data/spec/lib/hootenanny/feed/rss_feed_item_spec.rb +9 -0
  93. data/spec/lib/hootenanny/feed/rss_feed_spec.rb +143 -0
  94. data/spec/lib/hootenanny/feed_spec.rb +159 -0
  95. data/spec/lib/hootenanny/feed_store/file_feed_store_spec.rb +58 -0
  96. data/spec/lib/hootenanny/feed_store/web_feed_store_spec.rb +47 -0
  97. data/spec/lib/hootenanny/feed_store_spec.rb +27 -0
  98. data/spec/lib/hootenanny/hub_spec.rb +73 -0
  99. data/spec/lib/hootenanny/publish_notification_expiration_policy_spec.rb +35 -0
  100. data/spec/lib/hootenanny/request/publish_notification_spec.rb +43 -0
  101. data/spec/lib/hootenanny/request/subscription_spec.rb +89 -0
  102. data/spec/lib/hootenanny/request_spec.rb +21 -0
  103. data/spec/lib/hootenanny/subscription_delivery_spec.rb +54 -0
  104. data/spec/lib/hootenanny/topic_spec.rb +15 -0
  105. data/spec/lib/hootenanny/topic_synchronizer_spec.rb +98 -0
  106. data/spec/lib/hootenanny/uri_spec.rb +32 -0
  107. data/spec/lib/hootenanny/verification_spec.rb +92 -0
  108. data/spec/models/hootenanny/publish_notification_spec.rb +55 -0
  109. data/spec/models/hootenanny/subscription_spec.rb +58 -19
  110. data/spec/support/verification.rb +63 -0
  111. metadata +231 -14
  112. data/spec/controllers/hootenanny/hub_spec.rb +0 -15
@@ -0,0 +1,175 @@
1
+ require 'rspectacular/spec_helpers/rails_engine'
2
+ require 'pathname'
3
+ require 'hootenanny'
4
+
5
+ ###
6
+ # In order me to be albe to process information I find relevant within a timely fashion
7
+ # As a Subscriber
8
+ # I want to be able to receive topic content updates from the hub
9
+ #
10
+ feature 'Subscribers receive content updates' do
11
+ let(:topic_content) { JSON.dump(JSON.load(Pathname.new('./spec/fixtures/feeds/json/sample_feed.json').read)) }
12
+ let(:partial_topic_content) { JSON.dump(JSON.load(Pathname.new('./spec/fixtures/feeds/json/feed_with_one_item.json').read)) }
13
+ let(:topic_digest_content) { '{"items":["1a6ed1752c74c2b7fcc0f43ce39e53f8f220cd85f85e339b06eb75a0f16bbea8","6d557b1225129abebcb56d4c986dc63bf644ee865c3833f52ca69e75b4275052"]}' }
14
+ let(:partial_topic_digest_content) { '{"items":["6d557b1225129abebcb56d4c986dc63bf644ee865c3833f52ca69e75b4275052"]}' }
15
+ let(:broadcasted_topic_dir) { Pathname.new('./tmp/broadcasted_topics') }
16
+ let(:broadcasted_topic_file) { Pathname.new('./tmp/broadcasted_topics/5b187098da59f077f/97d79220f68b4bf27.digest') }
17
+
18
+ after(:each) { broadcasted_topic_dir.rmtree if broadcasted_topic_dir.exist? }
19
+
20
+ scenario 'Subscribers receive full content updates for their feed if they have never before had a feed broadcast' do
21
+ # Given that a topic has content
22
+ stub_request(:get, 'http://topic.com').
23
+ to_return(body: topic_content,
24
+ headers: {'Content-Type' => 'application/json'})
25
+
26
+ # And the subscriber is set up to receive said content
27
+ content_delivery_request = \
28
+ stub_request(:post, 'http://subscriber.com').
29
+ with( :body => topic_content,
30
+ :headers => {'Content-Type' => 'application/json'})
31
+
32
+ # And there is a notification that the topic has new content
33
+ create(:publish_notification, :topic => 'http://topic.com')
34
+
35
+ # And a subscriber is subscribed to that topic
36
+ create(:subscription, :active,
37
+ subscriber: 'http://subscriber.com',
38
+ topic: 'http://topic.com')
39
+
40
+ # And the subscriber has previously never received a content update
41
+ expect(broadcasted_topic_dir).not_to be_exist
42
+
43
+ # When the broadcasting process is kicked off
44
+ Hootenanny::Hub.broadcast
45
+
46
+ # Then the subscriber should have been sent all of the content for the feed
47
+ expect(content_delivery_request).to have_been_requested
48
+
49
+ # And the feed they were sent should be saved to the default broadcast store
50
+ expect(broadcasted_topic_file).to be_exist
51
+ expect(broadcasted_topic_file.read).to eql topic_digest_content
52
+
53
+ # And the notification should be marked as processed
54
+ expect(Hootenanny::PublishNotification.processed).to have(1).items
55
+ expect(Hootenanny::PublishNotification.unprocessed).to have(0).items
56
+ end
57
+
58
+ scenario 'Subscribers receive partial content updates if they have previously been sent a content update' do
59
+ # Given that a topic has content
60
+ stub_request(:get, 'http://topic.com').
61
+ to_return(body: topic_content,
62
+ headers: {'Content-Type' => 'application/json'})
63
+
64
+ # And the subscriber is set up to receive said content
65
+ content_delivery_request = \
66
+ stub_request(:post, 'http://subscriber.com').
67
+ with( :body => partial_topic_content,
68
+ :headers => {'Content-Type' => 'application/json'})
69
+
70
+ # And there is a notification that the topic has new content
71
+ create(:publish_notification, :topic => 'http://topic.com')
72
+
73
+ # And a subscriber is subscribed to that topic
74
+ create(:subscription, :active,
75
+ subscriber: 'http://subscriber.com',
76
+ topic: 'http://topic.com')
77
+
78
+ # And the subscriber has previously received a content update
79
+ broadcasted_topic_file.dirname.mkpath
80
+ ::File.write(broadcasted_topic_file, partial_topic_digest_content)
81
+
82
+ # When the broadcasting process is kicked off
83
+ Hootenanny::Hub.broadcast
84
+
85
+ # Then the subscriber should have been sent only the new or updated items for that feed
86
+ expect(content_delivery_request).to have_been_requested
87
+
88
+ # And the feed they were sent should be saved to the default broadcast store
89
+ expect(broadcasted_topic_file).to be_exist
90
+ expect(broadcasted_topic_file.read).to eql topic_digest_content
91
+
92
+ # And the notification should be marked as processed
93
+ expect(Hootenanny::PublishNotification.processed).to have(1).items
94
+ expect(Hootenanny::PublishNotification.unprocessed).to have(0).items
95
+ end
96
+
97
+ scenario 'Subscribers who reply unsuccessfully will have their content update reposted' do
98
+ # Given that a topic has content
99
+ stub_request(:get, 'http://topic.com').
100
+ to_return(body: topic_content,
101
+ headers: {'Content-Type' => 'application/json'})
102
+
103
+ # And the subscriber is not ready to receive said content
104
+ content_delivery_request = \
105
+ stub_request(:post, 'http://subscriber.com').
106
+ with( :body => topic_content,
107
+ :headers => {'Content-Type' => 'application/json'}).
108
+ to_return(:status => 500)
109
+
110
+ # And there is a notification that the topic has new content
111
+ create(:publish_notification, :topic => 'http://topic.com')
112
+
113
+ # And a subscriber is subscribed to that topic
114
+ create(:subscription, :active,
115
+ subscriber: 'http://subscriber.com',
116
+ topic: 'http://topic.com')
117
+
118
+ # And the subscriber has previously never received a content update
119
+ expect(broadcasted_topic_dir).not_to be_exist
120
+
121
+ # When the broadcasting process is kicked off
122
+ Hootenanny::Hub.broadcast
123
+
124
+ # Then the subscriber should have been sent the items for the feed
125
+ expect(content_delivery_request).to have_been_requested
126
+
127
+ # And the feed they were sent should not be saved to the default broadcast store
128
+ expect(broadcasted_topic_file).not_to be_exist
129
+
130
+ # And the notification should not be marked as processed
131
+ expect(Hootenanny::PublishNotification.processed).to have(0).items
132
+ expect(Hootenanny::PublishNotification.unprocessed).to have(1).items
133
+ end
134
+
135
+ scenario 'Subscribers who repeatedly reply unsuccessfully will have their notification request revoked after a reasonable period of time' do
136
+ # Given that a topic has content
137
+ stub_request(:get, 'http://topic.com').
138
+ to_return(body: topic_content,
139
+ headers: {'Content-Type' => 'application/json'})
140
+
141
+ # And the subscriber is not ready to receive said content
142
+ content_delivery_request = \
143
+ stub_request(:post, 'http://subscriber.com').
144
+ with( :body => topic_content,
145
+ :headers => {'Content-Type' => 'application/json'}).
146
+ to_return(:status => 500)
147
+
148
+ # And there is a notification that the topic has new content
149
+ create(:publish_notification, :topic => 'http://topic.com')
150
+
151
+ # And a subscriber is subscribed to that topic
152
+ create(:subscription, :active,
153
+ subscriber: 'http://subscriber.com',
154
+ topic: 'http://topic.com')
155
+
156
+ # And the subscriber has previously never received a content update
157
+ expect(broadcasted_topic_dir).not_to be_exist
158
+
159
+ # And it has been retried unsuccessfully for a day
160
+ Timecop.travel(1.day.from_now + 1)
161
+
162
+ # When the broadcasting process is kicked off
163
+ Hootenanny::Hub.broadcast
164
+
165
+ # Then the subscriber should have been sent the items for the feed
166
+ expect(content_delivery_request).to have_been_requested
167
+
168
+ # And the feed they were sent should not be saved to the default broadcast store
169
+ expect(broadcasted_topic_file).not_to be_exist
170
+
171
+ # And the notification should not be marked as errored
172
+ expect(Hootenanny::PublishNotification.expired).to have(1).items
173
+ expect(Hootenanny::PublishNotification.unprocessed).to have(0).items
174
+ end
175
+ end
@@ -6,30 +6,99 @@ require 'hootenanny'
6
6
  # As a potential Subscriber
7
7
  # I want to be able to get notifications about updates to a topic I am interressted in
8
8
  #
9
- feature 'Subscribers can subscribe to a topic' do
9
+ feature 'Subscribers can subscribe to a topic', :web_mock do
10
+ background do
11
+ confirmed_subscription_verification('http://mycallback',
12
+ 'http://mytopic')
13
+ end
14
+
10
15
  scenario 'Potential subscribers can subscribe to a new topic' do
11
16
  # Given there are no subscriptions
12
17
  expect( Hootenanny::Subscription ).to have(0).items
13
18
 
14
19
  # When a subscription is requested for a topic
15
- post( '/hootenanny/subscription', topic: 'my_topic',
16
- callback: 'my_callback')
20
+ post( '/hootenanny/subscription', topic: 'http://mytopic',
21
+ callback: 'http://mycallback')
17
22
 
18
23
  # Then the subscriber should be subscribed
19
- expect( Hootenanny::Subscription.to('my_topic') ).to have(1).item
24
+ expect( Hootenanny::Subscription.to('http://mytopic') ).to have(1).item
20
25
 
21
26
  # And the response should be 204 'No Content'
22
27
  expect( last_response.status ).to eql 204
23
28
  expect( last_response ).to be_empty
24
29
  end
25
30
 
31
+ scenario 'Potential subscribers can request a duration during which the subscription will be active', :time_mock do
32
+ # Given there are no subscriptions
33
+ expect( Hootenanny::Subscription ).to have(0).items
34
+
35
+ # When a subscription is requested which should be active for a given period
36
+ # of time
37
+ post( '/hootenanny/subscription', topic: 'http://mytopic',
38
+ callback: 'http://mycallback',
39
+ lease_seconds: 2)
40
+
41
+ # And that period has not yet elapsed
42
+
43
+ # Then the subscriber should be subscribed
44
+ expect( Hootenanny::Subscription.to('http://mytopic') ).to have(1).item
45
+
46
+ # When the subscription period has elapsed
47
+ Timecop.freeze(2)
48
+
49
+ # Then the subscriber should not be subscribed
50
+ expect( Hootenanny::Subscription.to('http://mytopic') ).to have(0).items
51
+ end
52
+
26
53
  scenario 'Potential subscribers receive error information if there is a problem' do
27
54
  # Given there are no subscriptions
28
55
  expect( Hootenanny::Subscription ).to have(0).items
29
56
 
30
- # When a invalid subscription is requested for a topic
57
+ # When a subscription request is posted which is confirmable
58
+ confirmed_subscription_verification('http://mycallback', 'http://!nv4l!d')
59
+
60
+ # But is otherwise invalid
31
61
  post( '/hootenanny/subscription', topic: 'http://!nv4l!d',
32
- callback: 'my_callback')
62
+ callback: 'http://mycallback')
63
+
64
+ # Then the subscriber should not be subscribed
65
+ expect( Hootenanny::Subscription ).to have(0).items
66
+
67
+ # And the response should be 400 'Bad Request'
68
+ expect( last_response.status ).to eql 400
69
+
70
+ # And the response should include a message indicating the problem
71
+ expect( last_response.body ).to eql({'error' => {
72
+ 'message' => 'the scheme http does not accept registry part: !nv4l!d (or bad hostname?)' }
73
+ }.to_json)
74
+ end
75
+
76
+ scenario 'Potential subscribers receive error information if they do not supply all required parameters' do
77
+ # Given there are no subscriptions
78
+ expect( Hootenanny::Subscription ).to have(0).items
79
+
80
+ # When a invalid subscription is requested for a topic
81
+ post( '/hootenanny/subscription', callback: 'http://mycallback')
82
+
83
+ # Then the subscriber should not be subscribed
84
+ expect( Hootenanny::Subscription ).to have(0).items
85
+
86
+ # And the response should be 400 'Bad Request'
87
+ expect( last_response.status ).to eql 400
88
+
89
+ # And the response should include a message indicating the problem
90
+ expect( last_response.body ).to eql({'error' => {
91
+ 'message' => 'Required parameter missing: topic' }
92
+ }.to_json)
93
+ end
94
+
95
+ scenario 'Potential subscribers receive error information if they use a non-HTTP/HTTPS URI' do
96
+ # Given there are no subscriptions
97
+ expect( Hootenanny::Subscription ).to have(0).items
98
+
99
+ # When a invalid subscription is requested for a topic
100
+ post( '/hootenanny/subscription', topic: 'ftp://mytopic',
101
+ callback: 'ftp://mycallback')
33
102
 
34
103
  # Then the subscriber should not be subscribed
35
104
  expect( Hootenanny::Subscription ).to have(0).items
@@ -39,7 +108,7 @@ feature 'Subscribers can subscribe to a topic' do
39
108
 
40
109
  # And the response should include a message indicating the problem
41
110
  expect( last_response.body ).to eql({'error' => {
42
- 'message' => 'All options passed need to be valid URIs' }
111
+ 'message' => 'Only HTTP and HTTPS URIs are allowed.' }
43
112
  }.to_json)
44
113
  end
45
114
  end
@@ -0,0 +1,51 @@
1
+ <feed xmlns='http://www.w3.org/2005/Atom'
2
+ xmlns:thr='http://purl.org/syndication/thread/1.0'
3
+ xml:base='https://www.tbray.org/ongoing/ongoing.atom'
4
+ xml:lang='en-us'>
5
+
6
+ <title>ongoing by Tim Bray</title>
7
+ <link rel='hub' href='http://pubsubhubbub.appspot.com/' />
8
+ <id>https://www.tbray.org/ongoing/</id>
9
+ <link href='./' />
10
+ <link rel='self' href='' />
11
+ <link rel='replies' thr:count='101' href='/home/tbray.org/www/html/ongoing/comments.atom' />
12
+ <logo>rsslogo.jpg</logo>
13
+ <icon>/favicon.ico</icon>
14
+ <updated>2013-06-17T17:33:02-07:00</updated>
15
+ <author><name>Tim Bray</name></author>
16
+ <subtitle>ongoing fragmented essay by Tim Bray</subtitle>
17
+ <rights>All content written by Tim Bray and photos by Tim Bray Copyright Tim Bray, some rights reserved, see /ongoing/misc/Copyright</rights>
18
+ <generator uri='/misc/Colophon'>Generated from XML source code using Perl, Expat, Emacs, Mysql, Ruby, Java, and ImageMagick. Industrial-strength technology, baby.</generator>
19
+
20
+ <entry xml:base='When/201x/2013/06/16/'>
21
+ <title>Unmapped Lands</title>
22
+ <link href='The-Unmapped-Lands' />
23
+ <link rel='replies' thr:count='0' type='application/xhtml+xml' href='The-Unmapped-Lands#comments' />
24
+ <id>https://www.tbray.org/ongoing/When/201x/2013/06/16/The-Unmapped-Lands</id>
25
+ <published>2013-06-16T12:00:00-07:00</published>
26
+ <updated>2013-06-16T09:47:15-07:00</updated>
27
+ <category scheme='https://www.tbray.org/ongoing/What/' term='Arts/Books' />
28
+ <category scheme='https://www.tbray.org/ongoing/What/' term='Arts' />
29
+ <category scheme='https://www.tbray.org/ongoing/What/' term='Books' />
30
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>summary</div></summary>
31
+ <content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
32
+ description
33
+ </div></content>
34
+ </entry>
35
+
36
+ <entry xml:base='When/201x/2013/06/16/'>
37
+ <title>Golang Diaries I</title>
38
+ <link href='Go-Love-Hate' />
39
+ <link rel='replies' thr:count='27' type='application/xhtml+xml' href='Go-Love-Hate#comments' />
40
+ <id>https://www.tbray.org/ongoing/When/201x/2013/06/16/Go-Love-Hate</id>
41
+ <published>2013-06-16T12:00:00-07:00</published>
42
+ <updated>2013-06-15T13:07:09-07:00</updated>
43
+ <category scheme='https://www.tbray.org/ongoing/What/' term='Technology/Software' />
44
+ <category scheme='https://www.tbray.org/ongoing/What/' term='Technology' />
45
+ <category scheme='https://www.tbray.org/ongoing/What/' term='Software' />
46
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>summary2</div></summary>
47
+ <content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
48
+ description2
49
+ </div></content>
50
+ </entry>
51
+ </feed>
@@ -0,0 +1,6 @@
1
+ {
2
+ "items": [
3
+ "a97acd993db7ef4cc74022f357954d3c4b52ab659218b89a59cdd355880d4ef6",
4
+ "01bfe038abb650189f78891be1581a52a7754a9be5fee7f8feeb0f7d617d3dd6"
5
+ ]
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "items": [
3
+ "01bfe038abb650189f78891be1581a52a7754a9be5fee7f8feeb0f7d617d3dd6"
4
+ ]
5
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "items": [
3
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
4
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
5
+ ]
6
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "title": "Ruby Rogues",
3
+ "feed_url": "http://rubyrogues.com/feed/",
4
+ "link": "http://rubyrogues.com",
5
+ "description": "Rubyist.where(:rogue => true)",
6
+ "language": "en-US",
7
+ "author": "Charles Max Wood, James Edward Gray II, David Brady, Avdi Grimm, Josh Susser, Katrina Owen",
8
+ "explicit": "no",
9
+ "image": "http://rubyrogues.com/wp-content/uploads/2013/05/RubyRogues_iTunes.jpg",
10
+ "items": [
11
+ {
12
+ "guid": "http://rubyrogues.com/?p=1361",
13
+ "title": "109 RR Extreme Programming with Will Read",
14
+ "link": "http://rubyrogues.com/109-rr-extreme-programming-with-will-read/",
15
+ "description": "description",
16
+ "pubDate": "Wed, 12 Jun 2013 13:00:31 +0000",
17
+ "enclosure": "http://traffic.libsyn.com/rubyrogues/RR109ExtremeProgramming.mp3",
18
+ "duration": "57:13"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "title": "Ruby Rogues",
3
+ "feed_url": "http://rubyrogues.com/feed/",
4
+ "link": "http://rubyrogues.com",
5
+ "description": "Rubyist.where(:rogue => true)",
6
+ "language": "en-US",
7
+ "author": "Charles Max Wood, James Edward Gray II, David Brady, Avdi Grimm, Josh Susser, Katrina Owen",
8
+ "explicit": "no",
9
+ "image": "http://rubyrogues.com/wp-content/uploads/2013/05/RubyRogues_iTunes.jpg",
10
+ "items": [
11
+ {
12
+ "guid": "http://rubyrogues.com/?p=1361",
13
+ "title": "109 RR Extreme Programming with Will Read",
14
+ "link": "http://rubyrogues.com/109-rr-extreme-programming-with-will-read/",
15
+ "description": "description",
16
+ "pubDate": "Wed, 12 Jun 2013 13:00:31 +0000",
17
+ "enclosure": "http://traffic.libsyn.com/rubyrogues/RR109ExtremeProgramming.mp3",
18
+ "duration": "57:13"
19
+ },
20
+ {
21
+ "guid": "http://rubyrogues.com/?p=1354",
22
+ "title": "108 RR Ruby Trends",
23
+ "link": "http://rubyrogues.com/108-rr-ruby-trends/",
24
+ "description": "description 2",
25
+ "pubDate": "Wed, 05 Jun 2013 13:00:46 +0000",
26
+ "enclosure": "http://traffic.libsyn.com/rubyrogues/RR108CommunityTrends.mp3",
27
+ "duration": "57:13"
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,21 @@
1
+ <rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:rawvoice="http://www.rawvoice.com/rawvoiceRssModule/" version="2.0">
2
+ <channel>
3
+ <title>Ruby Rogues</title>
4
+ <link>http://rubyrogues.com</link>
5
+ <description>Rubyist.where(:rogue => true)</description>
6
+ <language>en-US</language>
7
+ <item>
8
+ <title>109 RR Extreme Programming with Will Read</title>
9
+ <link>http://rubyrogues.com/109-rr-extreme-programming-with-will-read/</link>
10
+ <guid isPermaLink="false">http://rubyrogues.com/?p=1361</guid>
11
+ <description>description text</description>
12
+ <enclosure url="http://traffic.libsyn.com/rubyrogues/RR109ExtremeProgramming.mp3" length="73410493" type="audio/mpeg"/>
13
+ </item>
14
+ <item>
15
+ <title>108 RR Ruby Trends</title>
16
+ <link>http://rubyrogues.com/108-rr-ruby-trends/</link>
17
+ <guid isPermaLink="false">http://rubyrogues.com/?p=1354</guid>
18
+ <description>description text 2</description>
19
+ <enclosure url="http://traffic.libsyn.com/rubyrogues/RR108CommunityTrends.mp3" length="63807463" type="audio/mpeg"/>
20
+ </item>
21
+ </channel>