rabbit_feed 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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