hootenanny 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>