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
@@ -1,3 +1,4 @@
1
1
  Hootenanny::Engine.routes.draw do
2
- resource :subscription, :only => :create
2
+ resource :subscription, :only => :create
3
+ resource :publish_notification, :only => :create
3
4
  end
@@ -0,0 +1,15 @@
1
+ class AddStartedAtAndLeaseDurationToSubscriptions < ActiveRecord::Migration
2
+ def change
3
+ remove_index :hootenanny_subscriptions, :subscriber
4
+ remove_index :hootenanny_subscriptions, [:subscriber, :topic]
5
+
6
+ add_column :hootenanny_subscriptions, :started_at, :datetime
7
+ add_column :hootenanny_subscriptions, :lease_duration, :integer
8
+
9
+ change_column :hootenanny_subscriptions, :started_at, :datetime, :null => false
10
+ change_column :hootenanny_subscriptions, :lease_duration, :integer, :null => false
11
+
12
+ add_index :hootenanny_subscriptions, :subscriber, :unique => false
13
+ add_index :hootenanny_subscriptions, [:subscriber, :topic], :unique => true
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class AddHmacSecretToSubscription < ActiveRecord::Migration
2
+ def change
3
+ add_column :hootenanny_subscriptions, :digest_secret, :string, :null => true, :limit => 200
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class AddPublishNotifications < ActiveRecord::Migration
2
+ def change
3
+ create_table :hootenanny_publish_notifications do |t|
4
+ t.string :topic, :limit => 250, :null => false
5
+ end
6
+
7
+ add_index 'hootenanny_publish_notifications', 'topic', :name => 'index_hootenanny_publish_notifications_on_topic', :unique => true
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddTimestampsToPublishNotification < ActiveRecord::Migration
2
+ def change
3
+ add_timestamps :hootenanny_publish_notifications
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddProcessedFlagToPublishNotifications < ActiveRecord::Migration
2
+ def change
3
+ add_column :hootenanny_publish_notifications, :processed, :boolean, :null => false, :default => false
4
+ add_index :hootenanny_publish_notifications, :processed
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class SwitchPublishNotificationProcessStateFromBooleanToString < ActiveRecord::Migration
2
+ def up
3
+ add_column :hootenanny_publish_notifications, :state, :string, :limit => 15, :null => false, :default => 'unprocessed'
4
+ remove_column :hootenanny_publish_notifications, :processed
5
+ end
6
+
7
+ def down
8
+ add_column :hootenanny_publish_notifications, :processed, :boolean, :default => false, :null => false
9
+ remove_column :hootenanny_publish_notifications, :state
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class AddIndexToNotificationsState < ActiveRecord::Migration
2
+ def change
3
+ add_index :hootenanny_publish_notifications, :state
4
+ end
5
+ end
@@ -31,11 +31,15 @@ Gem::Specification.new do |s|
31
31
 
32
32
  s.add_dependency 'rails', '~> 3.2'
33
33
  s.add_dependency 'strong_parameters', '~> 0.2.1'
34
+ s.add_dependency 'faraday', '~> 0.8.7'
35
+ s.add_dependency 'chronological', '~> 1.0.0beta10'
34
36
 
35
37
  s.add_development_dependency 'pg'
36
- s.add_development_dependency 'sqlite3'
37
- s.add_development_dependency 'rspec-rails', '~> 2.13'
38
+ s.add_development_dependency 'rspec-rails', '~> 2.14.0.rc1'
39
+ s.add_development_dependency 'rspec', '~> 2.14.0.rc1'
38
40
  s.add_development_dependency 'database_cleaner', '~> 0.9.1'
39
41
  s.add_development_dependency 'rspectacular', '~> 0.13'
40
42
  s.add_development_dependency 'factory_girl_rails', '~> 4.2'
43
+ s.add_development_dependency 'timecop', '~> 0.6.1'
44
+ s.add_development_dependency 'webmock', '~> 1.11'
41
45
  end
@@ -0,0 +1,43 @@
1
+ require 'singleton'
2
+ require 'hootenanny/topic_synchronizer'
3
+ require 'hootenanny/subscription_delivery'
4
+ require 'hootenanny/feed_store/web_feed_store'
5
+ require 'hootenanny/feed_store/file_feed_store'
6
+
7
+ module Hootenanny
8
+ def self.config
9
+ Hootenanny::Configuration.instance
10
+ end
11
+
12
+ class Configuration
13
+ include Singleton
14
+
15
+ attr_accessor :default_local_feed_store,
16
+ :default_remote_feed_store,
17
+ :default_synchronizer,
18
+ :default_subscription_delivery
19
+
20
+ def default_local_feed_store
21
+ FeedStore::FileFeedStore.new(
22
+ :location => File.join('.',
23
+ 'tmp',
24
+ 'broadcasted_topics'))
25
+ end
26
+
27
+ def default_remote_feed_store
28
+ FeedStore::WebFeedStore.new
29
+ end
30
+
31
+ def default_synchronizer
32
+ TopicSynchronizer
33
+ end
34
+
35
+ def default_subscription_delivery
36
+ SubscriptionDelivery
37
+ end
38
+
39
+ def default_publish_notification_expiration_threshold
40
+ Time.now - (24 * 60 * 60)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ require 'hootenanny/configuration'
2
+ require 'hootenanny/subscription'
3
+ require 'hootenanny/topic'
4
+
5
+ module Hootenanny
6
+ class Correspondent
7
+ def initialize(topic, options = {})
8
+ self.topic = topic
9
+ self.synchronizer_class = options[:synchronizer] ||
10
+ Hootenanny.config.default_synchronizer
11
+ end
12
+
13
+ def self.broadcast(topic, options = {})
14
+ correspondent = new(topic, options = {})
15
+
16
+ correspondent.broadcast
17
+ end
18
+
19
+ def broadcast
20
+ subscriptions.all? do |subscription|
21
+ synchronizer_class.sync(subscriber: subscription.subscriber,
22
+ digest_secret: subscription.digest_secret,
23
+ topic: topic.url)
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def subscriptions
30
+ Hootenanny::Subscription.to(topic.url)
31
+ end
32
+
33
+ attr_accessor :topic,
34
+ :synchronizer_class
35
+
36
+ def topic=(other)
37
+ @topic = if other.respond_to?(:url)
38
+ other
39
+ else
40
+ Hootenanny::Topic.from_url(other)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,7 +1,48 @@
1
- class Hootenanny::SubscriptionAssignmentError < ArgumentError
1
+ require 'delegate'
2
+
3
+ class Hootenanny::Error < StandardError
2
4
  def to_h
3
5
  {
4
6
  message: self.message
5
7
  }
6
8
  end
9
+
10
+ def as_json(options = {})
11
+ {
12
+ error: {
13
+ message: self.message
14
+ }
15
+ }
16
+ end
17
+
18
+ def self.wrap(exception)
19
+ hootenanny_error = self.new(exception.message)
20
+
21
+ hootenanny_error.set_backtrace exception.backtrace
22
+
23
+ hootenanny_error
24
+ end
25
+ end
26
+
27
+ module Hootenanny
28
+ class Request; end
29
+ class URI < SimpleDelegator; end
30
+ class Feed; end
31
+ class FeedStore; end
32
+ end
33
+
34
+ class Hootenanny::Request::BuildError < Hootenanny::Error; end
35
+
36
+ class Hootenanny::URI::InvalidError < Hootenanny::Error; end
37
+ class Hootenanny::URI::InvalidSchemeError < Hootenanny::Error
38
+ def initialize(message = 'Only HTTP and HTTPS URIs are allowed.')
39
+ super
40
+ end
7
41
  end
42
+
43
+ class Hootenanny::SubscriptionError < Hootenanny::Error; end
44
+ class Hootenanny::Feed::UnknownContentTypeError < Hootenanny::Error; end
45
+ class Hootenanny::Feed::ParseError < Hootenanny::Error; end
46
+ class Hootenanny::Feed::CombinationError < Hootenanny::Error; end
47
+ class Hootenanny::FeedStore::InferenceError < Hootenanny::Error; end
48
+ class Hootenanny::FeedStore::UnknownStorageTypeError < Hootenanny::Error; end
@@ -0,0 +1,75 @@
1
+ require 'active_support/inflector'
2
+ require 'hootenanny/errors'
3
+ require 'hootenanny/feed/rss_feed'
4
+ require 'hootenanny/feed/json_feed'
5
+ require 'hootenanny/feed/atom_feed'
6
+ require 'hootenanny/feed/digest_feed'
7
+ require 'hootenanny/feed/null_feed'
8
+
9
+ module Hootenanny
10
+ class Feed
11
+ FEED_TYPE_MAPPINGS = {
12
+ :rss => 'RSS',
13
+ :json => 'JSON',
14
+ :atom => 'Atom',
15
+ :digest => 'Digest',
16
+ :'' => 'Null',
17
+ }
18
+
19
+ def self.infer(content, options = {})
20
+ type = options.fetch(:type).to_s.downcase.to_sym
21
+
22
+ feed_class(type).from_content(content)
23
+ rescue KeyError => e
24
+ raise ::Hootenanny::Feed::UnknownContentTypeError.wrap(e)
25
+ end
26
+
27
+ def self.from_content(content)
28
+ adapter = allocate
29
+
30
+ adapter.send :content=, content
31
+
32
+ adapter
33
+ end
34
+
35
+ def -(other)
36
+ feed = self.class.from_content(self.content)
37
+ feed.items = self.items - other.items
38
+ feed
39
+ end
40
+
41
+ def +(other)
42
+ raise Hootenanny::Feed::CombinationError unless self.class == other.class
43
+
44
+ feed = self.class.from_content(self.content)
45
+ feed.items = (self.items + other.items).uniq
46
+ feed
47
+ end
48
+
49
+ def empty?
50
+ items.empty?
51
+ end
52
+
53
+ def to_digest_feed
54
+ feed = DigestFeed.from_content('{}')
55
+ feed.items = self.items.map { |item| DigestFeedItem.new(item.to_digest) }
56
+ feed
57
+ end
58
+
59
+ def type
60
+ self.class.name.match(/Hootenanny::Feed::(\w+)Feed/)[1].downcase.to_sym
61
+ end
62
+
63
+ protected
64
+
65
+ attr_writer :items
66
+
67
+ private
68
+
69
+ def self.feed_class(type)
70
+ type_class_partial = FEED_TYPE_MAPPINGS.fetch(type)
71
+
72
+ "Hootenanny::Feed::#{type_class_partial}Feed".constantize
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,18 @@
1
+ require 'rss'
2
+ require 'hootenanny/feed'
3
+ require 'hootenanny/feed/atom_feed_item'
4
+
5
+ module Hootenanny
6
+ class Feed
7
+ class AtomFeed < ::Hootenanny::Feed::RSSFeed
8
+
9
+ def items
10
+ @items ||= content.entries.map { |i| Hootenanny::Feed::AtomFeedItem.new i }
11
+ end
12
+
13
+ def content_type
14
+ 'application/atom+xml'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ require 'hootenanny/feed/feed_item'
2
+
3
+ module Hootenanny
4
+ class Feed
5
+ class AtomFeedItem < ::Hootenanny::Feed::FeedItem
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ require 'json'
2
+ require 'hootenanny/feed'
3
+ require 'hootenanny/feed/digest_feed_item'
4
+
5
+ module Hootenanny
6
+ class Feed
7
+ class DigestFeed < ::Hootenanny::Feed::JSONFeed
8
+
9
+ def items
10
+ @items ||= content.fetch('items', []).map { |i| Hootenanny::Feed::DigestFeedItem.new i }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ require 'hootenanny/feed/feed_item'
2
+
3
+ module Hootenanny
4
+ class Feed
5
+ class DigestFeedItem < ::Hootenanny::Feed::FeedItem
6
+ def to_digest
7
+ content
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ require 'digest'
2
+
3
+ module Hootenanny
4
+ class Feed
5
+ class FeedItem
6
+
7
+ attr_accessor :content
8
+
9
+ def initialize(content)
10
+ self.content = content
11
+ end
12
+
13
+ def hash
14
+ self.to_digest.hash
15
+ end
16
+
17
+ def ==(comparee)
18
+ self.to_digest == comparee.to_digest
19
+ end
20
+
21
+ def eql?(comparee)
22
+ self == comparee
23
+ end
24
+
25
+ def to_digest
26
+ Digest::SHA256.hexdigest content.to_s
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,66 @@
1
+ require 'pathname'
2
+
3
+ module Hootenanny
4
+ class Feed
5
+ class File
6
+ FILE_EXTENSION_MAPPINGS = {
7
+ '.rss' => 'RSS',
8
+ '.atom' => 'Atom',
9
+ '.json' => 'JSON',
10
+ '.digest' => 'Digest',
11
+ }
12
+
13
+ def initialize(base_path)
14
+ self.path = base_path
15
+ end
16
+
17
+ def <<(other_path)
18
+ return self if other_path.nil? || other_path == ''
19
+
20
+ self.class.new(path + other_path)
21
+ end
22
+
23
+ def read
24
+ file.exist? ? file.read : ''
25
+ end
26
+
27
+ def write(content)
28
+ path.dirname.mkpath
29
+
30
+ path.open('w+') do |f|
31
+ f.write(content)
32
+ end
33
+ end
34
+
35
+ def type
36
+ FILE_EXTENSION_MAPPINGS[extension]
37
+ end
38
+
39
+ def to_s
40
+ file.to_s
41
+ end
42
+
43
+ protected
44
+
45
+ attr_accessor :path
46
+
47
+ def path=(other)
48
+ @path = Pathname.new(other)
49
+ end
50
+
51
+ def file
52
+ @file ||= if path.file?
53
+ path
54
+ else
55
+ Pathname.glob(path).fetch(0, Pathname.new(''))
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def extension
62
+ file.extname
63
+ end
64
+ end
65
+ end
66
+ end