rabbit_feed 1.0.2 → 2.0.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/DEVELOPING.md +14 -2
  4. data/Gemfile.lock +27 -26
  5. data/README.md +17 -11
  6. data/example/non_rails_app/Gemfile.lock +18 -17
  7. data/example/non_rails_app/spec/lib/non_rails_app/event_handler_spec.rb +1 -1
  8. data/example/non_rails_app/spec/lib/non_rails_app/event_routing_spec.rb +4 -2
  9. data/example/rails_app/Gemfile.lock +34 -33
  10. data/example/rails_app/spec/event_routing_spec.rb +3 -3
  11. data/lib/rabbit_feed/configuration.rb +25 -11
  12. data/lib/rabbit_feed/connection_concern.rb +4 -13
  13. data/lib/rabbit_feed/consumer_connection.rb +0 -6
  14. data/lib/rabbit_feed/event.rb +58 -18
  15. data/lib/rabbit_feed/event_definitions.rb +18 -6
  16. data/lib/rabbit_feed/producer.rb +12 -11
  17. data/lib/rabbit_feed/producer_connection.rb +1 -1
  18. data/lib/rabbit_feed/testing_support/rspec_matchers/publish_event.rb +2 -8
  19. data/lib/rabbit_feed/testing_support/test_rabbit_feed_consumer.rb +3 -3
  20. data/lib/rabbit_feed/version.rb +1 -1
  21. data/lib/rabbit_feed.rb +1 -0
  22. data/rabbit_feed.gemspec +2 -2
  23. data/spec/features/step_definitions/connectivity_steps.rb +1 -1
  24. data/spec/fixtures/configuration.yml +0 -1
  25. data/spec/lib/rabbit_feed/configuration_spec.rb +59 -12
  26. data/spec/lib/rabbit_feed/connection_concern_spec.rb +11 -0
  27. data/spec/lib/rabbit_feed/event_definitions_spec.rb +25 -11
  28. data/spec/lib/rabbit_feed/event_routing_spec.rb +7 -7
  29. data/spec/lib/rabbit_feed/event_spec.rb +119 -11
  30. data/spec/lib/rabbit_feed/producer_spec.rb +14 -0
  31. data/spec/lib/rabbit_feed/testing_support/testing_helper_spec.rb +6 -3
  32. metadata +6 -6
@@ -2,42 +2,82 @@ module RabbitFeed
2
2
  class Event
3
3
  include ActiveModel::Validations
4
4
 
5
- attr_reader :schema, :payload
6
- validates_presence_of :schema, :payload
5
+ SCHEMA_VERSION = '2.0.0'
7
6
 
8
- def initialize schema, payload
9
- @schema = schema
10
- @payload = payload
7
+ attr_reader :schema, :payload, :metadata
8
+ validates :metadata, presence: true
9
+ validates :payload, length: { minimum: 0, allow_nil: false, message: 'can\'t be nil' }
10
+ validate :required_metadata
11
+
12
+ def initialize metadata, payload={}, schema=nil
13
+ @schema = schema
14
+ @payload = payload.with_indifferent_access if payload
15
+ @metadata = metadata.with_indifferent_access if metadata
11
16
  validate!
12
17
  end
13
18
 
14
19
  def serialize
15
20
  buffer = StringIO.new
16
21
  writer = Avro::DataFile::Writer.new buffer, (Avro::IO::DatumWriter.new schema), schema
17
- writer << payload
22
+ writer << { 'metadata' => metadata, 'payload' => payload }
18
23
  writer.close
19
24
  buffer.string
20
25
  end
21
26
 
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
27
+ def application
28
+ metadata[:application]
29
+ end
30
+
31
+ def name
32
+ metadata[:name]
31
33
  end
32
34
 
33
- def method_missing(method_name, *args, &block)
34
- payload[method_name.to_s]
35
+ def created_at_utc
36
+ (Time.iso8601 metadata[:created_at_utc]) if metadata[:created_at_utc].present?
37
+ end
38
+
39
+ class << self
40
+
41
+ def deserialize serialized_event
42
+ datum_reader = Avro::IO::DatumReader.new
43
+ reader = Avro::DataFile::Reader.new (StringIO.new serialized_event), datum_reader
44
+ event_hash = nil
45
+ reader.each do |datum|
46
+ event_hash = datum
47
+ end
48
+ reader.close
49
+ if (version_1? event_hash)
50
+ new_from_version_1 event_hash, datum_reader.readers_schema
51
+ else
52
+ new event_hash['metadata'], event_hash['payload'], datum_reader.readers_schema
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def version_1? event_hash
59
+ %w(metadata payload).none?{|key| event_hash.has_key? key}
60
+ end
61
+
62
+ def new_from_version_1 metadata_and_payload, schema
63
+ metadata = {}
64
+ %w(application name host version environment created_at_utc).each do |field|
65
+ metadata[field] = metadata_and_payload.delete field
66
+ end
67
+ new metadata, metadata_and_payload, schema
68
+ end
35
69
  end
36
70
 
37
71
  private
38
72
 
39
73
  def validate!
40
- raise Error.new errors.messages if invalid?
74
+ raise Error.new "Invalid event: #{errors.messages}" if invalid?
75
+ end
76
+
77
+ def required_metadata
78
+ if metadata
79
+ errors.add(:metadata, 'name field is required') if metadata[:name].blank?
80
+ end
41
81
  end
42
82
  end
43
83
  end
@@ -51,19 +51,31 @@ module RabbitFeed
51
51
  @definition = block.call if block.present?
52
52
  end
53
53
 
54
- def payload
55
- ([
54
+ def payload_schema
55
+ { name: "#{name}_payload", type: 'record', fields: fields.map(&:schema) }
56
+ end
57
+
58
+ def metadata_schema
59
+ { name: 'event_metadata', type: 'record', fields: [
56
60
  (Field.new 'application', 'string', 'The name of the application that created the event'),
57
61
  (Field.new 'host', 'string', 'The hostname of the server on which the event was created'),
58
62
  (Field.new 'environment', 'string', 'The environment in which the event was created'),
59
- (Field.new 'version', 'string', 'The version of the event'),
63
+ (Field.new 'version', 'string', 'The version of the event payload'),
64
+ (Field.new 'schema_version', 'string', 'The version of the event schema'),
60
65
  (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)
66
+ (Field.new 'created_at_utc', 'string', 'The UTC time that the event was created'),
67
+ ].map(&:schema) }
68
+ end
69
+
70
+ def event_schema
71
+ [
72
+ { name: 'payload', type: payload_schema, doc: 'The event payload (defined by the source system)' },
73
+ { name: 'metadata', type: metadata_schema, doc: 'The event metadata (defined by rabbit feed)' },
74
+ ]
63
75
  end
64
76
 
65
77
  def schema
66
- @schema ||= (Avro::Schema.parse ({ name: name, type: 'record', doc: definition, fields: payload }.to_json))
78
+ @schema ||= (Avro::Schema.parse ({ name: name, type: 'record', doc: definition, fields: event_schema }.to_json))
67
79
  end
68
80
 
69
81
  def validate!
@@ -8,23 +8,24 @@ module RabbitFeed
8
8
  raise (Error.new 'Unable to publish event. No event definitions set.') unless event_definitions.present?
9
9
  event_definition = event_definitions[name] or raise (Error.new "definition for event: #{name} not found")
10
10
  timestamp = Time.now.utc
11
- payload = (enriched_payload payload, event_definition.version, name, timestamp)
12
- event = Event.new event_definition.schema, payload
11
+ metadata = (metadata event_definition.version, name, timestamp)
12
+ event = Event.new metadata, payload, event_definition.schema
13
13
  ProducerConnection.publish event.serialize, (options name, timestamp)
14
14
  event
15
15
  end
16
16
 
17
17
  private
18
18
 
19
- def enriched_payload payload, version, name, timestamp
20
- payload.merge ({
21
- 'application' => RabbitFeed.configuration.application,
22
- 'host' => Socket.gethostname,
23
- 'environment' => RabbitFeed.environment,
24
- 'created_at_utc' => timestamp.iso8601(6),
25
- 'version' => version,
26
- 'name' => name,
27
- })
19
+ def metadata version, name, timestamp
20
+ {
21
+ 'application' => RabbitFeed.configuration.application,
22
+ 'host' => Socket.gethostname,
23
+ 'environment' => RabbitFeed.environment,
24
+ 'created_at_utc' => timestamp.iso8601(6),
25
+ 'version' => version,
26
+ 'name' => name,
27
+ 'schema_version' => Event::SCHEMA_VERSION,
28
+ }
28
29
  end
29
30
 
30
31
  def routing_key event_name
@@ -57,7 +57,7 @@ module RabbitFeed
57
57
  end
58
58
 
59
59
  def self.connection_options
60
- default_connection_options.merge({
60
+ super.merge({
61
61
  threaded: false, # With threading enabled, there is a chance of losing an event during connection recovery
62
62
  })
63
63
  end
@@ -28,7 +28,7 @@ module RabbitFeed
28
28
 
29
29
  with_expected_payload = negative_expectation
30
30
  if received_expected_event && !with_expected_payload
31
- actual_payload = (strip_defaults_from actual_event.payload)
31
+ actual_payload = actual_event.payload
32
32
  with_expected_payload = expected_payload.nil? || actual_payload == expected_payload
33
33
  end
34
34
 
@@ -61,16 +61,10 @@ module RabbitFeed
61
61
 
62
62
  private
63
63
 
64
- def strip_defaults_from payload
65
- payload.reject do |key, value|
66
- ['application', 'host', 'environment', 'created_at_utc', 'version', 'name'].include? key
67
- end
68
- end
69
-
70
64
  def received_events_message
71
65
  if TestingSupport.published_events.any?
72
66
  TestingSupport.published_events.map do |received_event|
73
- "#{received_event.name} with #{strip_defaults_from received_event.payload}"
67
+ "#{received_event.name} with #{received_event.payload}"
74
68
  end
75
69
  else
76
70
  'no events'
@@ -1,9 +1,9 @@
1
1
  module RabbitFeed
2
2
  module TestingSupport
3
3
  class TestRabbitFeedConsumer
4
- def consume_event(event)
5
- event = Event.new('no schema',event)
6
- RabbitFeed::Consumer.event_routing.handle_event(event)
4
+
5
+ def consume_event event
6
+ RabbitFeed::Consumer.event_routing.handle_event event
7
7
  end
8
8
  end
9
9
  end
@@ -1,3 +1,3 @@
1
1
  module RabbitFeed
2
- VERSION = '1.0.2'
2
+ VERSION = '2.0.0'
3
3
  end
data/lib/rabbit_feed.rb CHANGED
@@ -17,6 +17,7 @@ require 'rabbit_feed/producer_connection'
17
17
  require 'rabbit_feed/producer'
18
18
  require 'rabbit_feed/event_definitions'
19
19
  require 'rabbit_feed/testing_support'
20
+ require 'rabbit_feed/version'
20
21
 
21
22
  module RabbitFeed
22
23
  extend self
data/rabbit_feed.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ['lib']
20
20
 
21
21
  # Gem for interfacing with RabbitMq
22
- spec.add_dependency 'bunny', '>= 1.1.9', '< 1.7.0'
22
+ spec.add_dependency 'bunny', '>= 1.1.9', '< 1.8.0'
23
23
  # We use some helpers from ActiveSupport
24
24
  spec.add_dependency 'activesupport', '>= 3.2.0', '< 5.0.0'
25
25
  # We use validations from ActiveModel
@@ -31,5 +31,5 @@ Gem::Specification.new do |spec|
31
31
  # Schema definitions and serialization for events
32
32
  spec.add_dependency 'avro', '>= 1.5.4', '< 1.8.0'
33
33
  # For stubbing and custom matchers
34
- spec.add_development_dependency 'rspec', '>=2.14.0', '< 3.2.0'
34
+ spec.add_development_dependency 'rspec', '>=2.14.0', '< 3.3.0'
35
35
  end
@@ -41,7 +41,7 @@ module Turnip::Steps
41
41
 
42
42
  def assert_event_presence event
43
43
  expect(event).to_not be_nil
44
- expect(event.field).to eq @event_text
44
+ expect(event.payload[:field]).to eq @event_text
45
45
  end
46
46
 
47
47
  def wait_for_event
@@ -5,7 +5,6 @@ test:
5
5
  password: guest
6
6
  application: rabbit_feed
7
7
  exchange: rabbit_feed_exchange
8
- pool_size: 1
9
8
  pool_timeout: 1
10
9
  heartbeat: 60
11
10
  connect_timeout: 1
@@ -15,6 +15,57 @@ module RabbitFeed
15
15
  it { should eq 'test.rabbit_feed' }
16
16
  end
17
17
 
18
+ describe '#connection_options' do
19
+ subject { (described_class.new options).connection_options }
20
+
21
+ context 'when there are no defaults' do
22
+ let(:options) do
23
+ {
24
+ application: 'rabbit_feed',
25
+ environment: 'test',
26
+ }
27
+ end
28
+
29
+ it { should include(logger: RabbitFeed.log) }
30
+ it { should include(recover_from_connection_close: true) }
31
+ it { should include(threaded: true) }
32
+ it { should_not include(:heartbeat) }
33
+ it { should_not include(:network_recovery_interval) }
34
+ it { should_not include(:connect_timeout) }
35
+ it { should_not include(:host) }
36
+ it { should_not include(:user) }
37
+ it { should_not include(:password) }
38
+ it { should_not include(:port) }
39
+ end
40
+
41
+ context 'when there are defaults' do
42
+ let(:options) do
43
+ {
44
+ application: 'rabbit_feed',
45
+ environment: 'test',
46
+ heartbeat: 5,
47
+ connect_timeout: 1,
48
+ host: 'localhost',
49
+ user: 'guest',
50
+ password: 'guest',
51
+ port: 1234,
52
+ network_recovery_interval: 1,
53
+ }
54
+ end
55
+
56
+ it { should include(heartbeat: 5) }
57
+ it { should include(connect_timeout: 1) }
58
+ it { should include(host: 'localhost') }
59
+ it { should include(user: 'guest') }
60
+ it { should include(password: 'guest') }
61
+ it { should include(port: 1234) }
62
+ it { should include(network_recovery_interval: 1) }
63
+ it { should include(logger: RabbitFeed.log) }
64
+ it { should include(recover_from_connection_close: true) }
65
+ it { should include(threaded: true) }
66
+ end
67
+ end
68
+
18
69
  describe '.load' do
19
70
  let(:file_path) { 'spec/fixtures/configuration.yml' }
20
71
  let(:environment) { 'test' }
@@ -45,7 +96,6 @@ module RabbitFeed
45
96
  its(:application) { should eq 'rabbit_feed' }
46
97
  its(:environment) { should eq 'test' }
47
98
  its(:exchange) { should eq 'rabbit_feed_exchange' }
48
- its(:pool_size) { should eq 1 }
49
99
  its(:pool_timeout) { should eq 1 }
50
100
  its(:heartbeat) { should eq 60 }
51
101
  its(:connect_timeout) { should eq 1 }
@@ -68,16 +118,15 @@ module RabbitFeed
68
118
  }
69
119
  end
70
120
 
71
- its(:host) { should eq 'localhost' }
72
- its(:port) { should eq 5672 }
73
- its(:user) { should eq 'guest' }
74
- its(:password) { should eq 'guest' }
121
+ its(:host) { should be_nil }
122
+ its(:port) { should be_nil }
123
+ its(:user) { should be_nil }
124
+ its(:password) { should be_nil }
125
+ its(:heartbeat) { should be_nil }
126
+ its(:network_recovery_interval) { should be_nil }
75
127
  its(:exchange) { should eq 'amq.topic' }
76
- its(:pool_size) { should eq 1 }
77
- its(:pool_timeout) { should eq 5 }
78
- its(:heartbeat) { should eq 5 }
79
- its(:connect_timeout) { should eq 10 }
80
- its(:network_recovery_interval) { should eq 1 }
128
+ its(:pool_timeout) { should eq 120 }
129
+ its(:connect_timeout) { should be_nil }
81
130
  its(:auto_delete_queue) { should be_falsey }
82
131
  its(:auto_delete_exchange) { should be_falsey }
83
132
  end
@@ -92,7 +141,6 @@ module RabbitFeed
92
141
  application: 'rabbit_feed',
93
142
  environment: 'test',
94
143
  exchange: 'exchange_name',
95
- pool_size: 2,
96
144
  pool_timeout: 6,
97
145
  heartbeat: 3,
98
146
  connect_timeout: 4,
@@ -109,7 +157,6 @@ module RabbitFeed
109
157
  its(:application) { should eq 'rabbit_feed' }
110
158
  its(:environment) { should eq 'test' }
111
159
  its(:exchange) { should eq 'exchange_name' }
112
- its(:pool_size) { should eq 2 }
113
160
  its(:pool_timeout) { should eq 6 }
114
161
  its(:heartbeat) { should eq 3 }
115
162
  its(:connect_timeout) { should eq 4 }
@@ -30,6 +30,11 @@ module RabbitFeed
30
30
  expect(subject.instance_variable_get(:@connection_pool)).to be_a ConnectionPool
31
31
  end
32
32
 
33
+ it 'creates a connection pool of one connection' do
34
+ expect(ConnectionPool).to receive(:new).with(hash_including({size: 1})).and_call_original
35
+ subject.with_connection{|connection| connection }
36
+ end
37
+
33
38
  it 'provides an instance of the class' do
34
39
  actual = subject.with_connection{|connection| connection }
35
40
  expect(actual).to be_a subject
@@ -97,6 +102,7 @@ module RabbitFeed
97
102
  describe '.retry_on_closed_connection' do
98
103
  before do
99
104
  subject.with_connection{|connection| connection }
105
+ allow(subject).to receive(:sleep).at_least(:once)
100
106
  end
101
107
 
102
108
  it_behaves_like 'an operation that retries on exception', :retry_on_closed_connection, Bunny::ConnectionClosedError
@@ -111,6 +117,11 @@ module RabbitFeed
111
117
  expect { subject.retry_on_closed_connection { raise Bunny::ConnectionClosedError.new 'blah' } }.to raise_error
112
118
  expect(subject.instance_variable_get(:@connection_pool)).to be_nil
113
119
  end
120
+
121
+ it 'waits between retries' do
122
+ expect(subject).to receive(:sleep).with(1).twice
123
+ begin; subject.retry_on_closed_connection { raise Bunny::ConnectionClosedError.new 'blah' }; rescue; end
124
+ end
114
125
  end
115
126
  end
116
127
  end
@@ -38,17 +38,31 @@ module RabbitFeed
38
38
  end
39
39
 
40
40
  it { should be_valid }
41
- its(:fields) { should_not be_empty }
42
- its(:schema) { should be_a Avro::Schema }
43
- its(:payload){ should =~ [
44
- {name: 'application', type: 'string', doc: 'The name of the application that created the event'},
45
- {name: 'host', type: 'string', doc: 'The hostname of the server on which the event was created'},
46
- {name: 'environment', type: 'string', doc: 'The environment in which the event was created'},
47
- {name: 'version', type: 'string', doc: 'The version of the event'},
48
- {name: 'created_at_utc', type: 'string', doc: 'The UTC time that the event was created'},
49
- {name: 'field', type: 'string', doc: 'field definition'},
50
- {name: 'name', type: 'string', doc: 'The name of the event'}
51
- ]
41
+ its(:fields) { should_not be_empty }
42
+ its(:schema) { should be_a Avro::Schema }
43
+ its(:metadata_schema) { should eq({
44
+ name: 'event_metadata',
45
+ type: 'record',
46
+ fields: [
47
+ {name: 'application', type: 'string', doc: 'The name of the application that created the event'},
48
+ {name: 'host', type: 'string', doc: 'The hostname of the server on which the event was created'},
49
+ {name: 'environment', type: 'string', doc: 'The environment in which the event was created'},
50
+ {name: 'version', type: 'string', doc: 'The version of the event payload'},
51
+ {name: 'schema_version', type: 'string', doc: 'The version of the event schema'},
52
+ {name: 'name', type: 'string', doc: 'The name of the event'},
53
+ {name: 'created_at_utc', type: 'string', doc: 'The UTC time that the event was created'},
54
+ ]})
55
+ }
56
+ its(:payload_schema) { should eq({
57
+ name: 'event_name_payload',
58
+ type: 'record',
59
+ fields: [{name: 'field', type: 'string', doc: 'field definition'}],
60
+ })
61
+ }
62
+ its(:event_schema) { should match([
63
+ { name: 'payload', type: an_instance_of(Hash), doc: 'The event payload (defined by the source system)' },
64
+ { name: 'metadata', type: an_instance_of(Hash), doc: 'The event metadata (defined by rabbit feed)' },
65
+ ])
52
66
  }
53
67
 
54
68
  context 'when the name is nil' do
@@ -41,11 +41,11 @@ module RabbitFeed
41
41
 
42
42
  it 'routes the event to the correct action, preferring named applications' do
43
43
  events = [
44
- double(:event, application: 'dummy_1', name: 'event_1', payload: 1),
45
- double(:event, application: 'dummy_1', name: 'event_2', payload: 2),
46
- double(:event, application: 'dummy_1', name: 'event_4', payload: 4),
47
- double(:event, application: 'dummy_2', name: 'event_3', payload: 3),
48
- double(:event, application: 'none', name: 'event_4', payload: 4),
44
+ Event.new({application: 'dummy_1', name: 'event_1'}, {payload: 1}),
45
+ Event.new({application: 'dummy_1', name: 'event_2'}, {payload: 2}),
46
+ Event.new({application: 'dummy_1', name: 'event_4'}, {payload: 4}),
47
+ Event.new({application: 'dummy_2', name: 'event_3'}, {payload: 3}),
48
+ Event.new({application: 'none', name: 'event_4'}, {payload: 4}),
49
49
  ]
50
50
  events.each do |event|
51
51
  (RabbitFeed::Consumer.event_routing.handle_event event).should eq event.payload
@@ -54,8 +54,8 @@ module RabbitFeed
54
54
 
55
55
  it 'raises a routing error when the event cannot be routed' do
56
56
  events = [
57
- double(:event, application: 'dummy_9', name: 'event_1', payload: 1),
58
- double(:event, application: 'dummy_1', name: 'event_9', payload: 3),
57
+ Event.new({application: 'dummy_9', name: 'event_1'}, {payload: 1}),
58
+ Event.new({application: 'dummy_1', name: 'event_9'}, {payload: 3}),
59
59
  ]
60
60
  events.each do |event|
61
61
  expect{ RabbitFeed::Consumer.event_routing.handle_event event }.to raise_error RoutingError
@@ -2,32 +2,140 @@ require 'spec_helper'
2
2
 
3
3
  module RabbitFeed
4
4
  describe Event do
5
- let(:schema) { double(:schema) }
6
- let(:payload) { { 'customer_id' => '123' } }
5
+ let(:schema) { double(:schema) }
6
+ let(:payload) { { 'customer_id' => '123' } }
7
+ let(:metadata) { { 'name' => 'test_event' } }
7
8
 
8
- subject { described_class.new schema, payload }
9
+ subject { described_class.new metadata, payload, schema }
9
10
 
10
11
  describe '.new' do
11
12
 
12
13
  it { should be_valid }
13
- its(:schema) { should eq schema }
14
- its(:payload) { should eq({ 'customer_id' => '123' }) }
14
+ its(:schema) { should eq schema }
15
+ its(:payload) { should eq({ 'customer_id' => '123' }) }
16
+ its(:metadata) { should eq({ 'name' => 'test_event' }) }
15
17
 
16
- context 'when schema is nil' do
17
- let(:schema) {}
18
+ context 'when payload is nil' do
19
+ let(:payload) {}
18
20
 
19
21
  it 'should raise an error' do
20
- expect{ subject }.to raise_error Error
22
+ expect{ subject }.to raise_error 'Invalid event: {:payload=>["can\'t be nil"]}'
21
23
  end
22
24
  end
23
25
 
24
- context 'when payload is nil' do
25
- let(:payload) {}
26
+ context 'when metadata is blank' do
27
+ let(:metadata) {}
28
+
29
+ it 'should raise an error' do
30
+ expect{ subject }.to raise_error 'Invalid event: {:metadata=>["can\'t be blank"]}'
31
+ end
32
+ end
33
+
34
+ context 'when name is blank' do
35
+ let(:metadata) {{ 'name' => '' }}
26
36
 
27
37
  it 'should raise an error' do
28
- expect{ subject }.to raise_error Error
38
+ expect{ subject }.to raise_error 'Invalid event: {:metadata=>["name field is required"]}'
29
39
  end
30
40
  end
31
41
  end
42
+
43
+ describe '#name' do
44
+ let(:metadata) { { 'name' => 'test_event' } }
45
+
46
+ its(:name) { should eq('test_event') }
47
+ end
48
+
49
+ describe '#application' do
50
+ let(:metadata) { { 'name' => 'test_event', 'application' => 'test_application' } }
51
+
52
+ its(:application) { should eq('test_application') }
53
+ end
54
+
55
+ describe '#created_at_utc' do
56
+
57
+ context 'when the created_at_utc is in the metadata' do
58
+ let(:metadata) { { 'name' => 'test_event', 'created_at_utc' => '2015-03-02T15:55:19.411299Z' } }
59
+
60
+ its(:created_at_utc) { should eq(Time.iso8601('2015-03-02T15:55:19.411299Z')) }
61
+ end
62
+
63
+ context 'when the created_at_utc is not in the metadata' do
64
+ let(:metadata) { { 'name' => 'test_event', 'created_at_utc' => '' } }
65
+
66
+ its(:created_at_utc) { should be_nil }
67
+ end
68
+ end
69
+
70
+ describe '.deserialize' do
71
+ subject { described_class.deserialize serialized_event }
72
+
73
+ context 'from a post-1.0 event' do
74
+ let(:schema) do
75
+ Avro::Schema.parse ({ name: 'post-1.0', type: 'record', fields: [
76
+ { name: 'payload', type: {
77
+ name: 'event_payload', type: 'record', fields: [
78
+ { name: 'customer_id', type: 'string' },
79
+ ]
80
+ },
81
+ },
82
+ { name: 'metadata', type: {
83
+ name: 'event_metadata', type: 'record', fields: [
84
+ { name: 'name', type: 'string' },
85
+ ]
86
+ },
87
+ },
88
+ ]}.to_json)
89
+ end
90
+ let(:serialized_event) { (described_class.new metadata, payload, schema).serialize }
91
+
92
+ its(:schema) { should eq schema }
93
+ its(:payload) { should eq({ 'customer_id' => '123' }) }
94
+ its(:metadata) { should eq({ 'name' => 'test_event' }) }
95
+ end
96
+
97
+ context 'from a 1.0 event' do
98
+ let(:metadata_and_payload) do
99
+ {
100
+ 'application' => 'rabbit_feed',
101
+ 'name' => 'test_event',
102
+ 'host' => 'localhost',
103
+ 'version' => '1.0.0',
104
+ 'environment' => 'test',
105
+ 'created_at_utc' => '2015-02-22',
106
+ 'customer_id' => '123',
107
+ }
108
+ end
109
+ let(:schema) do
110
+ Avro::Schema.parse ({ name: '1.0', type: 'record', fields: [
111
+ { name: 'customer_id', type: 'string' },
112
+ { name: 'application', type: 'string' },
113
+ { name: 'name', type: 'string' },
114
+ { name: 'host', type: 'string' },
115
+ { name: 'version', type: 'string' },
116
+ { name: 'environment', type: 'string' },
117
+ { name: 'created_at_utc', type: 'string' },
118
+ ]}.to_json)
119
+ end
120
+ let(:serialized_event) do
121
+ buffer = StringIO.new
122
+ writer = Avro::DataFile::Writer.new buffer, (Avro::IO::DatumWriter.new schema), schema
123
+ writer << metadata_and_payload
124
+ writer.close
125
+ buffer.string
126
+ end
127
+
128
+ its(:schema) { should eq schema }
129
+ its(:payload) { should eq({ 'customer_id' => '123' }) }
130
+ its(:metadata) { should eq({
131
+ 'application' => 'rabbit_feed',
132
+ 'name' => 'test_event',
133
+ 'host' => 'localhost',
134
+ 'version' => '1.0.0',
135
+ 'environment' => 'test',
136
+ 'created_at_utc' => '2015-02-22',
137
+ })}
138
+ end
139
+ end
32
140
  end
33
141
  end