rabbit_feed 0.3.1

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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/Brewfile +4 -0
  5. data/DEVELOPING.md +140 -0
  6. data/Gemfile +15 -0
  7. data/Gemfile.lock +121 -0
  8. data/LICENSE.txt +9 -0
  9. data/README.md +304 -0
  10. data/Rakefile +30 -0
  11. data/bin/bundle +3 -0
  12. data/bin/rabbit_feed +11 -0
  13. data/example/non_rails_app/.rspec +1 -0
  14. data/example/non_rails_app/Gemfile +7 -0
  15. data/example/non_rails_app/Gemfile.lock +56 -0
  16. data/example/non_rails_app/Rakefile +5 -0
  17. data/example/non_rails_app/bin/benchmark +63 -0
  18. data/example/non_rails_app/bin/bundle +3 -0
  19. data/example/non_rails_app/config/rabbit_feed.yml +8 -0
  20. data/example/non_rails_app/lib/non_rails_app.rb +32 -0
  21. data/example/non_rails_app/lib/non_rails_app/event_handler.rb +10 -0
  22. data/example/non_rails_app/log/.keep +0 -0
  23. data/example/non_rails_app/spec/lib/non_rails_app/event_handler_spec.rb +14 -0
  24. data/example/non_rails_app/spec/lib/non_rails_app/event_routing_spec.rb +14 -0
  25. data/example/non_rails_app/spec/spec_helper.rb +31 -0
  26. data/example/non_rails_app/tmp/pids/.keep +0 -0
  27. data/example/rails_app/.gitignore +17 -0
  28. data/example/rails_app/.node-version +1 -0
  29. data/example/rails_app/.rspec +1 -0
  30. data/example/rails_app/Gemfile +36 -0
  31. data/example/rails_app/Gemfile.lock +173 -0
  32. data/example/rails_app/README.rdoc +28 -0
  33. data/example/rails_app/Rakefile +6 -0
  34. data/example/rails_app/app/assets/images/.keep +0 -0
  35. data/example/rails_app/app/assets/javascripts/application.js +16 -0
  36. data/example/rails_app/app/assets/javascripts/beavers.js.coffee +3 -0
  37. data/example/rails_app/app/assets/stylesheets/application.css +15 -0
  38. data/example/rails_app/app/assets/stylesheets/beavers.css.scss +3 -0
  39. data/example/rails_app/app/assets/stylesheets/scaffolds.css.scss +69 -0
  40. data/example/rails_app/app/controllers/application_controller.rb +5 -0
  41. data/example/rails_app/app/controllers/beavers_controller.rb +81 -0
  42. data/example/rails_app/app/controllers/concerns/.keep +0 -0
  43. data/example/rails_app/app/helpers/application_helper.rb +2 -0
  44. data/example/rails_app/app/helpers/beavers_helper.rb +2 -0
  45. data/example/rails_app/app/mailers/.keep +0 -0
  46. data/example/rails_app/app/models/.keep +0 -0
  47. data/example/rails_app/app/models/beaver.rb +2 -0
  48. data/example/rails_app/app/models/concerns/.keep +0 -0
  49. data/example/rails_app/app/views/beavers/_form.html.erb +21 -0
  50. data/example/rails_app/app/views/beavers/edit.html.erb +6 -0
  51. data/example/rails_app/app/views/beavers/index.html.erb +25 -0
  52. data/example/rails_app/app/views/beavers/index.json.jbuilder +4 -0
  53. data/example/rails_app/app/views/beavers/new.html.erb +5 -0
  54. data/example/rails_app/app/views/beavers/show.html.erb +9 -0
  55. data/example/rails_app/app/views/beavers/show.json.jbuilder +1 -0
  56. data/example/rails_app/app/views/layouts/application.html.erb +14 -0
  57. data/example/rails_app/bin/bundle +3 -0
  58. data/example/rails_app/bin/rails +4 -0
  59. data/example/rails_app/bin/rake +4 -0
  60. data/example/rails_app/config.ru +4 -0
  61. data/example/rails_app/config/application.rb +25 -0
  62. data/example/rails_app/config/boot.rb +4 -0
  63. data/example/rails_app/config/database.yml +22 -0
  64. data/example/rails_app/config/environment.rb +5 -0
  65. data/example/rails_app/config/environments/development.rb +83 -0
  66. data/example/rails_app/config/environments/test.rb +39 -0
  67. data/example/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  68. data/example/rails_app/config/initializers/cookies_serializer.rb +3 -0
  69. data/example/rails_app/config/initializers/filter_parameter_logging.rb +4 -0
  70. data/example/rails_app/config/initializers/inflections.rb +16 -0
  71. data/example/rails_app/config/initializers/mime_types.rb +4 -0
  72. data/example/rails_app/config/initializers/rabbit_feed.rb +43 -0
  73. data/example/rails_app/config/initializers/session_store.rb +3 -0
  74. data/example/rails_app/config/initializers/wrap_parameters.rb +14 -0
  75. data/example/rails_app/config/locales/en.yml +23 -0
  76. data/example/rails_app/config/rabbit_feed.yml +8 -0
  77. data/example/rails_app/config/routes.rb +58 -0
  78. data/example/rails_app/config/secrets.yml +18 -0
  79. data/example/rails_app/config/unicorn.rb +4 -0
  80. data/example/rails_app/db/migrate/20140424102400_create_beavers.rb +9 -0
  81. data/example/rails_app/db/schema.rb +22 -0
  82. data/example/rails_app/db/seeds.rb +7 -0
  83. data/example/rails_app/lib/assets/.keep +0 -0
  84. data/example/rails_app/lib/event_handler.rb +7 -0
  85. data/example/rails_app/lib/tasks/.keep +0 -0
  86. data/example/rails_app/log/.keep +0 -0
  87. data/example/rails_app/public/404.html +67 -0
  88. data/example/rails_app/public/422.html +67 -0
  89. data/example/rails_app/public/500.html +66 -0
  90. data/example/rails_app/public/favicon.ico +0 -0
  91. data/example/rails_app/public/robots.txt +5 -0
  92. data/example/rails_app/spec/controllers/beavers_controller_spec.rb +32 -0
  93. data/example/rails_app/spec/event_routing_spec.rb +15 -0
  94. data/example/rails_app/spec/spec_helper.rb +51 -0
  95. data/example/rails_app/test/controllers/.keep +0 -0
  96. data/example/rails_app/test/controllers/beavers_controller_test.rb +49 -0
  97. data/example/rails_app/test/fixtures/.keep +0 -0
  98. data/example/rails_app/test/fixtures/beavers.yml +7 -0
  99. data/example/rails_app/test/helpers/.keep +0 -0
  100. data/example/rails_app/test/helpers/beavers_helper_test.rb +4 -0
  101. data/example/rails_app/test/integration/.keep +0 -0
  102. data/example/rails_app/test/mailers/.keep +0 -0
  103. data/example/rails_app/test/models/.keep +0 -0
  104. data/example/rails_app/test/models/beaver_test.rb +7 -0
  105. data/example/rails_app/test/test_helper.rb +13 -0
  106. data/example/rails_app/tmp/pids/.keep +0 -0
  107. data/example/rails_app/vendor/assets/javascripts/.keep +0 -0
  108. data/example/rails_app/vendor/assets/stylesheets/.keep +0 -0
  109. data/lib/dsl.rb +9 -0
  110. data/lib/rabbit_feed.rb +41 -0
  111. data/lib/rabbit_feed/client.rb +181 -0
  112. data/lib/rabbit_feed/configuration.rb +50 -0
  113. data/lib/rabbit_feed/connection_concern.rb +95 -0
  114. data/lib/rabbit_feed/consumer.rb +14 -0
  115. data/lib/rabbit_feed/consumer_connection.rb +108 -0
  116. data/lib/rabbit_feed/event.rb +43 -0
  117. data/lib/rabbit_feed/event_definitions.rb +98 -0
  118. data/lib/rabbit_feed/event_routing.rb +90 -0
  119. data/lib/rabbit_feed/producer.rb +47 -0
  120. data/lib/rabbit_feed/producer_connection.rb +65 -0
  121. data/lib/rabbit_feed/testing_support/rspec_matchers/publish_event.rb +90 -0
  122. data/lib/rabbit_feed/testing_support/testing_helpers.rb +16 -0
  123. data/lib/rabbit_feed/version.rb +3 -0
  124. data/logo.png +0 -0
  125. data/rabbit_feed.gemspec +35 -0
  126. data/run_benchmark +35 -0
  127. data/run_example +62 -0
  128. data/run_recovery_test +26 -0
  129. data/spec/features/connectivity.feature +13 -0
  130. data/spec/features/step_definitions/connectivity_steps.rb +96 -0
  131. data/spec/fixtures/configuration.yml +14 -0
  132. data/spec/lib/rabbit_feed/client_spec.rb +116 -0
  133. data/spec/lib/rabbit_feed/configuration_spec.rb +121 -0
  134. data/spec/lib/rabbit_feed/connection_concern_spec.rb +116 -0
  135. data/spec/lib/rabbit_feed/consumer_connection_spec.rb +85 -0
  136. data/spec/lib/rabbit_feed/event_definitions_spec.rb +139 -0
  137. data/spec/lib/rabbit_feed/event_routing_spec.rb +121 -0
  138. data/spec/lib/rabbit_feed/event_spec.rb +33 -0
  139. data/spec/lib/rabbit_feed/producer_connection_spec.rb +72 -0
  140. data/spec/lib/rabbit_feed/producer_spec.rb +57 -0
  141. data/spec/lib/rabbit_feed/testing_support/rspec_matchers/publish_event_spec.rb +60 -0
  142. data/spec/lib/rabbit_feed/testing_support/testing_helper_spec.rb +34 -0
  143. data/spec/spec_helper.rb +58 -0
  144. data/spec/support/shared_examples_for_connections.rb +40 -0
  145. metadata +305 -0
@@ -0,0 +1,50 @@
1
+ module RabbitFeed
2
+ class Configuration
3
+ include ActiveModel::Validations
4
+
5
+ attr_reader :host, :port, :user, :password, :application, :environment, :exchange, :pool_size, :pool_timeout, :heartbeat, :connect_timeout, :network_recovery_interval, :auto_delete_queue, :auto_delete_exchange
6
+ validates_presence_of :host, :port, :user, :password, :application, :environment, :exchange, :pool_size, :pool_timeout, :heartbeat, :connect_timeout, :network_recovery_interval
7
+
8
+ def initialize options
9
+ RabbitFeed.log.debug "RabbitFeed initialising with options: #{options}..."
10
+
11
+ @host = options[:host] || 'localhost'
12
+ @port = options[:port] || 5672
13
+ @user = options[:user] || 'guest'
14
+ @password = options[:password] || 'guest'
15
+ @exchange = options[:exchange] || 'amq.topic'
16
+ @pool_size = options[:pool_size] || 1
17
+ @pool_timeout = options[:pool_timeout] || 5
18
+ @heartbeat = options[:heartbeat] || 5
19
+ @connect_timeout = options[:connect_timeout] || 10
20
+ @network_recovery_interval = options[:network_recovery_interval] || 1
21
+ @auto_delete_queue = !!(options[:auto_delete_queue] || false)
22
+ @auto_delete_exchange = !!(options[:auto_delete_exchange] || false)
23
+ @application = options[:application]
24
+ @environment = options[:environment]
25
+ validate!
26
+ end
27
+
28
+ def self.load file_path, environment
29
+ RabbitFeed.log.debug "Reading configurations from #{file_path} in #{environment}..."
30
+
31
+ options = read_configuration_file file_path, environment
32
+ new options.merge(environment: environment)
33
+ end
34
+
35
+ def queue
36
+ "#{environment}.#{application}"
37
+ end
38
+
39
+ private
40
+
41
+ def self.read_configuration_file file_path, environment
42
+ raw_configuration = YAML.load(ERB.new(File.read(file_path)).result)
43
+ HashWithIndifferentAccess.new (raw_configuration[environment] || {})
44
+ end
45
+
46
+ def validate!
47
+ raise ConfigurationError.new errors.messages if invalid?
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,95 @@
1
+ module RabbitFeed
2
+ module ConnectionConcern
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+
7
+ def default_connection_options
8
+ {
9
+ heartbeat: RabbitFeed.configuration.heartbeat,
10
+ connect_timeout: RabbitFeed.configuration.connect_timeout,
11
+ host: RabbitFeed.configuration.host,
12
+ user: RabbitFeed.configuration.user,
13
+ password: RabbitFeed.configuration.password,
14
+ port: RabbitFeed.configuration.port,
15
+ network_recovery_interval: RabbitFeed.configuration.network_recovery_interval,
16
+ logger: RabbitFeed.log,
17
+ }
18
+ end
19
+
20
+ def with_connection &block
21
+ connection_pool.with do |connection|
22
+ yield connection
23
+ end
24
+ end
25
+
26
+ def retry_on_exception tries=3, &block
27
+ yield
28
+ rescue Bunny::ConnectionClosedError
29
+ raise # There is no point in retrying if the connection is closed
30
+ rescue => e
31
+ RabbitFeed.log.warn "Exception encountered; #{tries - 1} tries remaining. #{self.to_s}: #{e.message} #{e.backtrace}"
32
+ unless (tries -= 1).zero?
33
+ retry
34
+ end
35
+ raise
36
+ end
37
+
38
+ def retry_on_closed_connection tries=3, &block
39
+ yield
40
+ rescue Bunny::ConnectionClosedError => e
41
+ RabbitFeed.log.warn "Closed connection exception encountered; #{tries - 1} tries remaining. #{self.to_s}: #{e.message} #{e.backtrace}"
42
+ unless (tries -= 1).zero?
43
+ unset_connection
44
+ sleep RabbitFeed.configuration.network_recovery_interval
45
+ retry
46
+ end
47
+ raise
48
+ end
49
+
50
+ def close
51
+ RabbitFeed.log.debug "Closing connection: #{self.to_s}..."
52
+ @bunny_connection.close if @bunny_connection.present? && !closed?
53
+ unset_connection
54
+ rescue => e
55
+ RabbitFeed.log.warn "Exception encountered whilst closing #{self.to_s}: #{e.message} #{e.backtrace}"
56
+ end
57
+
58
+ def bunny_connection
59
+ if @bunny_connection.nil?
60
+ retry_on_exception do
61
+ RabbitFeed.log.debug "Opening connection: #{self.to_s}..."
62
+ @bunny_connection = Bunny.new connection_options
63
+ @bunny_connection.start
64
+ end
65
+ end
66
+
67
+ @bunny_connection
68
+ end
69
+ private :bunny_connection
70
+
71
+ def connection_pool
72
+ @connection_pool ||= ConnectionPool.new(
73
+ size: RabbitFeed.configuration.pool_size,
74
+ timeout: RabbitFeed.configuration.pool_timeout
75
+ ) do
76
+ new bunny_connection.create_channel
77
+ end
78
+ end
79
+ private :connection_pool
80
+
81
+ def closed?
82
+ @bunny_connection.present? && @bunny_connection.closed?
83
+ end
84
+ private :closed?
85
+
86
+ def unset_connection
87
+ RabbitFeed.log.debug "Unsetting connection: #{self.to_s}..."
88
+ @connection_pool = nil
89
+ @bunny_connection = nil
90
+ end
91
+ private :unset_connection
92
+
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,14 @@
1
+ module RabbitFeed
2
+ module Consumer
3
+ extend self
4
+
5
+ attr_accessor :event_routing
6
+
7
+ def run
8
+ ConsumerConnection.consume do |raw_event|
9
+ event = Event.deserialize raw_event
10
+ event_routing.handle_event event
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,108 @@
1
+ module RabbitFeed
2
+ class ConsumerConnection
3
+ include ConnectionConcern
4
+
5
+ SUBSCRIPTION_OPTIONS = {
6
+ consumer_tag: Socket.gethostname, # Use the host name of the server
7
+ manual_ack: true, # Manually acknowledge messages once they've been processed
8
+ block: false, # Don't block the thread whilst consuming from the queue, as this breaks during connection recovery
9
+ }.freeze
10
+
11
+ SEVEN_DAYS_IN_MS = 7.days * 1000
12
+
13
+ QUEUE_OPTIONS = {
14
+ durable: true, # Persist across server restart
15
+ no_declare: false, # Create the queue if it does not exist
16
+ arguments: {
17
+ 'x-ha-policy' => 'all', # Apply the queue on all mirrors
18
+ 'x-expires' => SEVEN_DAYS_IN_MS, # Auto-delete the queue after a period of inactivity (in ms)
19
+ },
20
+ }.freeze
21
+
22
+ attr_reader :queue
23
+
24
+ def initialize channel
25
+ channel.prefetch(1) # Fetch one message at a time to preserve order
26
+ RabbitFeed.log.debug "Declaring queue on #{self.to_s} (channel #{channel.id}) named: #{RabbitFeed.configuration.queue} with options: #{queue_options}..."
27
+ @queue = channel.queue RabbitFeed.configuration.queue, queue_options
28
+ bind_on_accepted_routes
29
+ end
30
+
31
+ def self.consume &block
32
+ with_connection do |consumer_connection|
33
+ consumer_connection.consume(&block)
34
+ end
35
+ end
36
+
37
+ def consume &block
38
+ RabbitFeed.log.info "Consuming messages on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
39
+
40
+ consumer = queue.subscribe(SUBSCRIPTION_OPTIONS) do |delivery_info, properties, payload|
41
+ handle_message delivery_info, payload, &block
42
+ end
43
+
44
+ sleep # Sleep indefinitely, as the consumer runs in its own thread
45
+ rescue
46
+ (cancel_consumer consumer) if consumer.present?
47
+ raise
48
+ end
49
+
50
+ private
51
+
52
+ def queue_options
53
+ {
54
+ auto_delete: RabbitFeed.configuration.auto_delete_queue,
55
+ }.merge QUEUE_OPTIONS
56
+ end
57
+
58
+ def self.connection_options
59
+ default_connection_options.merge({
60
+ threaded: true,
61
+ })
62
+ end
63
+
64
+ def bind_on_accepted_routes
65
+ if RabbitFeed::Consumer.event_routing.present?
66
+ RabbitFeed::Consumer.event_routing.accepted_routes.each do |accepted_route|
67
+ queue.bind(RabbitFeed.configuration.exchange, { routing_key: accepted_route })
68
+ end
69
+ else
70
+ queue.bind(RabbitFeed.configuration.exchange)
71
+ end
72
+ end
73
+
74
+ def acknowledge delivery_info
75
+ queue.channel.ack(delivery_info.delivery_tag)
76
+ RabbitFeed.log.debug "Message acknowledged on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
77
+ end
78
+
79
+ def handle_message delivery_info, payload, &block
80
+ RabbitFeed.log.debug "Message received on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
81
+
82
+ begin
83
+ yield payload
84
+ acknowledge delivery_info
85
+ rescue => e
86
+ handle_processing_exception delivery_info, e
87
+ end
88
+ end
89
+
90
+ def cancel_consumer consumer
91
+ cancel_ok = consumer.cancel
92
+ RabbitFeed.log.debug "Consumer: #{cancel_ok.consumer_tag} cancelled on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
93
+ end
94
+
95
+ def negative_acknowledge delivery_info
96
+ # Tell rabbit that we were unable to process the message
97
+ # This will re-queue the message
98
+ queue.channel.nack(delivery_info.delivery_tag, false, true)
99
+ RabbitFeed.log.debug "Message negatively acknowledged on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
100
+ end
101
+
102
+ def handle_processing_exception delivery_info, exception
103
+ negative_acknowledge delivery_info
104
+ RabbitFeed.log.error "Exception encountered while consuming message on #{self.to_s} from queue #{RabbitFeed.configuration.queue}: #{exception.message} #{exception.backtrace}"
105
+ RabbitFeed.exception_notify exception
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,43 @@
1
+ module RabbitFeed
2
+ class Event
3
+ include ActiveModel::Validations
4
+
5
+ attr_reader :schema, :payload
6
+ validates_presence_of :schema, :payload
7
+
8
+ def initialize schema, payload
9
+ @schema = schema
10
+ @payload = payload
11
+ validate!
12
+ end
13
+
14
+ def serialize
15
+ buffer = StringIO.new
16
+ writer = Avro::DataFile::Writer.new buffer, (Avro::IO::DatumWriter.new schema), schema
17
+ writer << payload
18
+ writer.close
19
+ buffer.string
20
+ end
21
+
22
+ def self.deserialize event
23
+ datum_reader = Avro::IO::DatumReader.new
24
+ reader = Avro::DataFile::Reader.new (StringIO.new event), datum_reader
25
+ payload = nil
26
+ reader.each do |datum|
27
+ payload = datum
28
+ end
29
+ reader.close
30
+ Event.new datum_reader.readers_schema, payload
31
+ end
32
+
33
+ def method_missing(method_name, *args, &block)
34
+ payload[method_name.to_s]
35
+ end
36
+
37
+ private
38
+
39
+ def validate!
40
+ raise Error.new errors.messages if invalid?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,98 @@
1
+ module RabbitFeed
2
+ class EventDefinitions
3
+
4
+ class Field
5
+ include ActiveModel::Validations
6
+
7
+ attr_reader :name, :type, :definition
8
+ validates_presence_of :name, :type, :definition
9
+
10
+ def initialize name, type, definition
11
+ @name = name
12
+ @type = type
13
+ @definition = definition
14
+ validate!
15
+ end
16
+
17
+ def schema
18
+ { name: name, type: type, doc: definition }
19
+ end
20
+
21
+ private
22
+
23
+ def validate!
24
+ raise ConfigurationError.new "Bad field specification for #{name}: #{errors.messages}" if invalid?
25
+ end
26
+ end
27
+
28
+ class Event
29
+ include ActiveModel::Validations
30
+
31
+ attr_reader :name, :definition, :version, :fields
32
+ validates_presence_of :name, :definition, :version
33
+ validate :schema_parseable
34
+ validates :version, format: { with: /\A\d+\.\d+\.\d+\z/, message: 'must be in *.*.* format' }
35
+
36
+ def initialize name, version
37
+ @name = name
38
+ @version = version
39
+ @fields = []
40
+ end
41
+
42
+ def payload_contains &block
43
+ self.instance_eval(&block)
44
+ end
45
+
46
+ def field name, options
47
+ fields << (Field.new name, options[:type], options[:definition])
48
+ end
49
+
50
+ def defined_as &block
51
+ @definition = block.call if block.present?
52
+ end
53
+
54
+ def payload
55
+ ([
56
+ (Field.new 'application', 'string', 'The name of the application that created the event'),
57
+ (Field.new 'host', 'string', 'The hostname of the server on which the event was created'),
58
+ (Field.new 'environment', 'string', 'The environment in which the event was created'),
59
+ (Field.new 'version', 'string', 'The version of the event'),
60
+ (Field.new 'name', 'string', 'The name of the event'),
61
+ (Field.new 'created_at_utc', 'string', 'The UTC time that the event was created')
62
+ ] + fields).map(&:schema)
63
+ end
64
+
65
+ def schema
66
+ @schema ||= (Avro::Schema.parse ({ name: name, type: 'record', doc: definition, fields: payload }.to_json))
67
+ end
68
+
69
+ def validate!
70
+ raise ConfigurationError.new "Bad event specification for #{name}: #{errors.messages}" if invalid?
71
+ end
72
+
73
+ private
74
+
75
+ def schema_parseable
76
+ schema
77
+ rescue => e
78
+ errors.add(:fields, "could not be parsed into a schema, reason: #{e.message}")
79
+ end
80
+ end
81
+
82
+ attr_reader :events
83
+
84
+ def initialize
85
+ @events = {}
86
+ end
87
+
88
+ def define_event name, options, &block
89
+ events[name] = Event.new name, options[:version]
90
+ events[name].instance_eval(&block)
91
+ events[name].validate!
92
+ end
93
+
94
+ def [] name
95
+ events[name]
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,90 @@
1
+ module RabbitFeed
2
+ class EventRouting
3
+
4
+ class Event
5
+ include ActiveModel::Validations
6
+
7
+ attr_reader :name, :action
8
+ validates_presence_of :name, :action
9
+ validate :action_arity
10
+
11
+ def initialize name, block
12
+ @name = name
13
+ @action = block
14
+
15
+ validate!
16
+ end
17
+
18
+ def handle_event event
19
+ action.call event
20
+ end
21
+
22
+ private
23
+
24
+ def action_arity
25
+ errors.add(:action, 'arity should be 1') if action.present? && action.arity != 1
26
+ end
27
+
28
+ def validate!
29
+ raise ConfigurationError.new "Bad event specification for #{name}: #{errors.messages}" if invalid?
30
+ end
31
+ end
32
+
33
+ class Application
34
+ include ActiveModel::Validations
35
+
36
+ attr_reader :events, :name
37
+ validates_presence_of :name
38
+
39
+ def initialize name
40
+ @name = name
41
+ @events = {}
42
+
43
+ validate!
44
+ end
45
+
46
+ def event name, &block
47
+ event = (Event.new name, block)
48
+ events[event.name] = event
49
+ end
50
+
51
+ def accepted_routes
52
+ events.values.map do |event|
53
+ "#{RabbitFeed.environment}.#{name}.#{event.name}"
54
+ end
55
+ end
56
+
57
+ def handle_event event
58
+ event_rule = events[event.name]
59
+ raise RoutingError.new "No routing defined for application with name: #{event.application} for events named: #{event.name}" unless event_rule.present?
60
+ event_rule.handle_event event
61
+ end
62
+
63
+ def validate!
64
+ raise ConfigurationError.new "Bad application specification for #{name}: #{errors.messages}" if invalid?
65
+ end
66
+ end
67
+
68
+ attr_reader :applications
69
+
70
+ def initialize
71
+ @applications = {}
72
+ end
73
+
74
+ def accept_from name, &block
75
+ application = Application.new name
76
+ application.instance_eval(&block)
77
+ applications[application.name] = application
78
+ end
79
+
80
+ def accepted_routes
81
+ applications.values.map{|application| application.accepted_routes }.flatten
82
+ end
83
+
84
+ def handle_event event
85
+ application = applications[event.application]
86
+ raise RoutingError.new "No routing defined for application with name: #{event.application}" unless application.present?
87
+ application.handle_event event
88
+ end
89
+ end
90
+ end