hootenanny 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -7
- data/Gemfile.lock +46 -28
- data/app/controllers/hootenanny/notifications_controller.rb +31 -0
- data/app/controllers/hootenanny/parameters.rb +16 -0
- data/app/controllers/hootenanny/subscriptions_controller.rb +24 -3
- data/app/models/hootenanny/publish_notification.rb +90 -0
- data/app/models/hootenanny/subscription.rb +72 -23
- data/config/routes.rb +2 -1
- data/db/migrate/20130607182642_add_started_at_and_lease_duration_to_subscriptions.rb +15 -0
- data/db/migrate/20130608225621_add_hmac_secret_to_subscription.rb +5 -0
- data/db/migrate/20130611235218_add_publish_notifications.rb +9 -0
- data/db/migrate/20130612153138_add_timestamps_to_publish_notification.rb +5 -0
- data/db/migrate/20130705200729_add_processed_flag_to_publish_notifications.rb +6 -0
- data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb +11 -0
- data/db/migrate/20130711061558_add_index_to_notifications_state.rb +5 -0
- data/hootenanny.gemspec +6 -2
- data/lib/hootenanny/configuration.rb +43 -0
- data/lib/hootenanny/correspondent.rb +44 -0
- data/lib/hootenanny/errors.rb +42 -1
- data/lib/hootenanny/feed.rb +75 -0
- data/lib/hootenanny/feed/atom_feed.rb +18 -0
- data/lib/hootenanny/feed/atom_feed_item.rb +8 -0
- data/lib/hootenanny/feed/digest_feed.rb +14 -0
- data/lib/hootenanny/feed/digest_feed_item.rb +11 -0
- data/lib/hootenanny/feed/feed_item.rb +30 -0
- data/lib/hootenanny/feed/file.rb +66 -0
- data/lib/hootenanny/feed/json_feed.rb +48 -0
- data/lib/hootenanny/feed/json_feed_item.rb +8 -0
- data/lib/hootenanny/feed/null_feed.rb +27 -0
- data/lib/hootenanny/feed/rss_feed.rb +52 -0
- data/lib/hootenanny/feed/rss_feed_item.rb +8 -0
- data/lib/hootenanny/feed_store.rb +30 -0
- data/lib/hootenanny/feed_store/file_feed_store.rb +55 -0
- data/lib/hootenanny/feed_store/web_feed_store.rb +42 -0
- data/lib/hootenanny/hub.rb +116 -22
- data/lib/hootenanny/publish_notification_expiration_policy.rb +17 -0
- data/lib/hootenanny/request.rb +40 -0
- data/lib/hootenanny/request/publish_notification.rb +94 -0
- data/lib/hootenanny/request/subscription.rb +153 -0
- data/lib/hootenanny/subscription_delivery.rb +47 -0
- data/lib/hootenanny/topic.rb +38 -0
- data/lib/hootenanny/topic_synchronizer.rb +71 -0
- data/lib/hootenanny/uri.rb +53 -0
- data/lib/hootenanny/verification.rb +108 -0
- data/lib/hootenanny/version.rb +1 -1
- data/spec/dummy/config/database.yml +12 -6
- data/spec/dummy/db/migrate/20130607183149_add_started_at_and_lease_duration_to_subscriptions.hootenanny.rb +16 -0
- data/spec/dummy/db/migrate/20130608231253_add_hmac_secret_to_subscription.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130611235546_add_publish_notifications.hootenanny.rb +10 -0
- data/spec/dummy/db/migrate/20130612153353_add_timestamps_to_publish_notification.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130705200832_add_processed_flag_to_publish_notifications.hootenanny.rb +7 -0
- data/spec/dummy/db/migrate/20130711061518_switch_publish_notification_process_state_from_boolean_to_string.hootenanny.rb +12 -0
- data/spec/dummy/db/migrate/20130711061629_add_index_to_notifications_state.hootenanny.rb +6 -0
- data/spec/factories/publish_notification.rb +13 -0
- data/spec/factories/requests/publish_notification.rb +11 -0
- data/spec/factories/requests/subscription.rb +46 -0
- data/spec/factories/subscription.rb +14 -2
- data/spec/factories/verification.rb +15 -0
- data/spec/features/publishers/can_notify_the_hub_of_content_updates_spec.rb +58 -0
- data/spec/features/subscribers/are_protected_from_unwarranted_subscriptions_spec.rb +59 -0
- data/spec/features/subscribers/can_receive_distributions_of_topic_content_spec.rb +175 -0
- data/spec/features/subscribers/can_subscribe_to_a_topic_spec.rb +76 -7
- data/spec/fixtures/feeds/atom/97d79220f68b4bf27.atom +0 -0
- data/spec/fixtures/feeds/atom/sample_feed.atom +51 -0
- data/spec/fixtures/feeds/digest/97d79220f68b4bf27.digest +1 -0
- data/spec/fixtures/feeds/digest/complete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +6 -0
- data/spec/fixtures/feeds/digest/incomplete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +5 -0
- data/spec/fixtures/feeds/digest/sample_feed.digest +6 -0
- data/spec/fixtures/feeds/json/97d79220f68b4bf27.json +1 -0
- data/spec/fixtures/feeds/json/feed_with_one_item.json +21 -0
- data/spec/fixtures/feeds/json/sample_feed.json +30 -0
- data/spec/fixtures/feeds/rss/5b187098da59f077f/97d79220f68b4bf27.rss +21 -0
- data/spec/fixtures/feeds/rss/97d79220f68b4bf27.rss +0 -0
- data/spec/fixtures/feeds/rss/feed_with_one_item.rss +15 -0
- data/spec/fixtures/feeds/rss/minimal_feed.rss +12 -0
- data/spec/fixtures/feeds/rss/sample_feed.rss +22 -0
- data/spec/fixtures/feeds/rss/sample_feed_2.rss +22 -0
- data/spec/lib/hootenanny/configuration_spec.rb +7 -0
- data/spec/lib/hootenanny/correspondent_spec.rb +94 -0
- data/spec/lib/hootenanny/errors_spec.rb +21 -0
- data/spec/lib/hootenanny/feed/atom_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/atom_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/digest_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/digest_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/feed_item_spec.rb +49 -0
- data/spec/lib/hootenanny/feed/file_spec.rb +66 -0
- data/spec/lib/hootenanny/feed/json_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/json_feed_spec.rb +128 -0
- data/spec/lib/hootenanny/feed/null_feed_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_spec.rb +143 -0
- data/spec/lib/hootenanny/feed_spec.rb +159 -0
- data/spec/lib/hootenanny/feed_store/file_feed_store_spec.rb +58 -0
- data/spec/lib/hootenanny/feed_store/web_feed_store_spec.rb +47 -0
- data/spec/lib/hootenanny/feed_store_spec.rb +27 -0
- data/spec/lib/hootenanny/hub_spec.rb +73 -0
- data/spec/lib/hootenanny/publish_notification_expiration_policy_spec.rb +35 -0
- data/spec/lib/hootenanny/request/publish_notification_spec.rb +43 -0
- data/spec/lib/hootenanny/request/subscription_spec.rb +89 -0
- data/spec/lib/hootenanny/request_spec.rb +21 -0
- data/spec/lib/hootenanny/subscription_delivery_spec.rb +54 -0
- data/spec/lib/hootenanny/topic_spec.rb +15 -0
- data/spec/lib/hootenanny/topic_synchronizer_spec.rb +98 -0
- data/spec/lib/hootenanny/uri_spec.rb +32 -0
- data/spec/lib/hootenanny/verification_spec.rb +92 -0
- data/spec/models/hootenanny/publish_notification_spec.rb +55 -0
- data/spec/models/hootenanny/subscription_spec.rb +58 -19
- data/spec/support/verification.rb +63 -0
- metadata +231 -14
- 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: '
|
16
|
-
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('
|
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
|
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: '
|
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' => '
|
111
|
+
'message' => 'Only HTTP and HTTPS URIs are allowed.' }
|
43
112
|
}.to_json)
|
44
113
|
end
|
45
114
|
end
|
File without changes
|
@@ -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 @@
|
|
1
|
+
{}
|
@@ -0,0 +1 @@
|
|
1
|
+
{}
|
@@ -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>
|