deimos-ruby 1.11.2 → 1.12.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -2
  3. data/Gemfile.lock +8 -8
  4. data/README.md +103 -0
  5. data/deimos-ruby.gemspec +1 -1
  6. data/docs/CONFIGURATION.md +4 -0
  7. data/lib/deimos/active_record_consume/batch_consumption.rb +7 -9
  8. data/lib/deimos/active_record_consume/message_consumption.rb +4 -3
  9. data/lib/deimos/active_record_consumer.rb +2 -2
  10. data/lib/deimos/active_record_producer.rb +3 -0
  11. data/lib/deimos/config/configuration.rb +29 -0
  12. data/lib/deimos/consume/batch_consumption.rb +2 -2
  13. data/lib/deimos/consume/message_consumption.rb +2 -2
  14. data/lib/deimos/consumer.rb +10 -0
  15. data/lib/deimos/producer.rb +4 -3
  16. data/lib/deimos/schema_backends/avro_base.rb +64 -1
  17. data/lib/deimos/schema_backends/avro_schema_registry.rb +1 -1
  18. data/lib/deimos/schema_backends/base.rb +18 -2
  19. data/lib/deimos/schema_class/base.rb +62 -0
  20. data/lib/deimos/schema_class/enum.rb +24 -0
  21. data/lib/deimos/schema_class/record.rb +66 -0
  22. data/lib/deimos/shared_config.rb +5 -0
  23. data/lib/deimos/test_helpers.rb +43 -7
  24. data/lib/deimos/utils/schema_class.rb +29 -0
  25. data/lib/deimos/version.rb +1 -1
  26. data/lib/deimos.rb +21 -0
  27. data/lib/generators/deimos/schema_class/templates/schema_class.rb.tt +15 -0
  28. data/lib/generators/deimos/schema_class/templates/schema_enum.rb.tt +21 -0
  29. data/lib/generators/deimos/schema_class/templates/schema_record.rb.tt +65 -0
  30. data/lib/generators/deimos/schema_class_generator.rb +247 -0
  31. data/lib/tasks/deimos.rake +8 -0
  32. data/spec/active_record_batch_consumer_spec.rb +120 -110
  33. data/spec/active_record_consumer_spec.rb +97 -88
  34. data/spec/active_record_producer_spec.rb +38 -27
  35. data/spec/batch_consumer_spec.rb +37 -28
  36. data/spec/config/configuration_spec.rb +10 -3
  37. data/spec/consumer_spec.rb +95 -84
  38. data/spec/generators/active_record_generator_spec.rb +1 -0
  39. data/spec/generators/schema_class/my_schema_with_complex_types_spec.rb +206 -0
  40. data/spec/generators/schema_class_generator_spec.rb +186 -0
  41. data/spec/producer_spec.rb +110 -0
  42. data/spec/schema_classes/generated.rb +156 -0
  43. data/spec/schema_classes/my_nested_schema.rb +114 -0
  44. data/spec/schema_classes/my_schema.rb +53 -0
  45. data/spec/schema_classes/my_schema_key.rb +35 -0
  46. data/spec/schema_classes/my_schema_with_complex_types.rb +172 -0
  47. data/spec/schemas/com/my-namespace/Generated.avsc +6 -0
  48. data/spec/schemas/com/my-namespace/MySchemaWithComplexTypes.avsc +95 -0
  49. data/spec/spec_helper.rb +6 -1
  50. metadata +28 -4
@@ -21,6 +21,7 @@ describe Deimos, 'configuration' do
21
21
  kafka_logger logger
22
22
  reraise_consumer_errors true
23
23
  schema_registry_url 'http://schema.registry'
24
+ schema.use_schema_classes false
24
25
  seed_broker 'whatever'
25
26
  schema_path 'some_path'
26
27
  producer_schema_namespace 'namespace'
@@ -37,6 +38,7 @@ describe Deimos, 'configuration' do
37
38
  expect(described_class.config.kafka.logger).to eq(logger)
38
39
  expect(described_class.config.consumers.reraise_errors).to eq(true)
39
40
  expect(described_class.config.schema.registry_url).to eq('http://schema.registry')
41
+ expect(described_class.config.schema.use_schema_classes).to eq(false)
40
42
  expect(described_class.config.kafka.seed_brokers).to eq('whatever')
41
43
  expect(described_class.config.producers.schema_namespace).to eq('namespace')
42
44
  expect(described_class.config.producers.topic_prefix).to eq('prefix')
@@ -86,7 +88,8 @@ describe Deimos, 'configuration' do
86
88
  offset_commit_threshold: 0,
87
89
  offset_retention_time: nil,
88
90
  heartbeat_interval: 10,
89
- handler: 'ConsumerTest::MyConsumer'
91
+ handler: 'ConsumerTest::MyConsumer',
92
+ use_schema_classes: nil
90
93
  }, {
91
94
  topic: 'my_batch_consume_topic',
92
95
  group_id: 'my_batch_group_id',
@@ -102,7 +105,8 @@ describe Deimos, 'configuration' do
102
105
  offset_commit_threshold: 0,
103
106
  offset_retention_time: nil,
104
107
  heartbeat_interval: 10,
105
- handler: 'ConsumerTest::MyBatchConsumer'
108
+ handler: 'ConsumerTest::MyBatchConsumer',
109
+ use_schema_classes: nil
106
110
  }
107
111
  ],
108
112
  producer: {
@@ -178,6 +182,7 @@ describe Deimos, 'configuration' do
178
182
  offset_commit_threshold 13
179
183
  offset_retention_time 13
180
184
  heartbeat_interval 13
185
+ use_schema_classes false
181
186
  end
182
187
  consumer do
183
188
  disabled true
@@ -185,6 +190,7 @@ describe Deimos, 'configuration' do
185
190
  schema 'blah2'
186
191
  topic 'blah2'
187
192
  group_id 'myconsumerid2'
193
+ use_schema_classes false
188
194
  end
189
195
  end
190
196
 
@@ -227,7 +233,8 @@ describe Deimos, 'configuration' do
227
233
  offset_commit_threshold: 13,
228
234
  offset_retention_time: 13,
229
235
  heartbeat_interval: 13,
230
- handler: 'MyConfigConsumer'
236
+ handler: 'MyConfigConsumer',
237
+ use_schema_classes: false
231
238
  }
232
239
  ],
233
240
  producer: {
@@ -3,7 +3,6 @@
3
3
  # :nodoc:
4
4
  module ConsumerTest
5
5
  describe Deimos::Consumer, 'Message Consumer' do
6
-
7
6
  prepend_before(:each) do
8
7
  # :nodoc:
9
8
  consumer_class = Class.new(described_class) do
@@ -13,7 +12,7 @@ module ConsumerTest
13
12
 
14
13
  # :nodoc:
15
14
  def fatal_error?(_exception, payload, _metadata)
16
- payload == 'fatal'
15
+ payload.to_s == 'fatal'
17
16
  end
18
17
 
19
18
  # :nodoc:
@@ -24,98 +23,110 @@ module ConsumerTest
24
23
  stub_const('ConsumerTest::MyConsumer', consumer_class)
25
24
  end
26
25
 
27
- it 'should consume a message' do
28
- test_consume_message(MyConsumer,
29
- 'test_id' => 'foo',
30
- 'some_int' => 123) do |payload, _metadata|
31
- expect(payload['test_id']).to eq('foo')
32
- end
33
- end
26
+ describe 'consume' do
27
+ SCHEMA_CLASS_SETTINGS.each do |setting, use_schema_classes|
28
+ context "with Schema Class consumption #{setting}" do
29
+ before(:each) do
30
+ Deimos.configure { |config| config.schema.use_schema_classes = use_schema_classes }
31
+ end
34
32
 
35
- it 'should consume a nil message' do
36
- test_consume_message(MyConsumer, nil) do |payload, _metadata|
37
- expect(payload).to be_nil
38
- end
39
- end
33
+ it 'should consume a message' do
34
+ test_consume_message(MyConsumer,
35
+ 'test_id' => 'foo',
36
+ 'some_int' => 123) do |payload, _metadata|
37
+ expect(payload['test_id']).to eq('foo')
38
+ expect(payload['some_int']).to eq(123)
39
+ end
40
+ end
40
41
 
41
- it 'should consume a message idempotently' do
42
- # testing for a crash and re-consuming the same message/metadata
43
- key = { 'test_id' => 'foo' }
44
- test_metadata = { key: key }
45
- allow_any_instance_of(MyConsumer).to(receive(:decode_key)) do |_instance, k|
46
- k['test_id']
47
- end
48
- MyConsumer.new.around_consume({ 'test_id' => 'foo',
49
- 'some_int' => 123 }, test_metadata) do |_payload, metadata|
50
- expect(metadata[:key]).to eq('foo')
51
- end
52
- MyConsumer.new.around_consume({ 'test_id' => 'foo',
53
- 'some_int' => 123 }, test_metadata) do |_payload, metadata|
54
- expect(metadata[:key]).to eq('foo')
55
- end
56
- end
42
+ it 'should consume a nil message' do
43
+ test_consume_message(MyConsumer, nil) do |payload, _metadata|
44
+ expect(payload).to be_nil
45
+ end
46
+ end
57
47
 
58
- it 'should consume a message on a topic' do
59
- test_consume_message('my_consume_topic',
60
- 'test_id' => 'foo',
61
- 'some_int' => 123) do |payload, _metadata|
62
- expect(payload['test_id']).to eq('foo')
63
- end
64
- end
48
+ it 'should consume a message idempotently' do
49
+ # testing for a crash and re-consuming the same message/metadata
50
+ key = { 'test_id' => 'foo' }
51
+ test_metadata = { key: key }
52
+ allow_any_instance_of(MyConsumer).to(receive(:decode_key)) do |_instance, k|
53
+ k['test_id']
54
+ end
55
+ MyConsumer.new.around_consume({ 'test_id' => 'foo',
56
+ 'some_int' => 123 }, test_metadata) do |_payload, metadata|
57
+ expect(metadata[:key]).to eq('foo')
58
+ end
59
+ MyConsumer.new.around_consume({ 'test_id' => 'foo',
60
+ 'some_int' => 123 }, test_metadata) do |_payload, metadata|
61
+ expect(metadata[:key]).to eq('foo')
62
+ end
63
+ end
65
64
 
66
- it 'should fail on invalid message' do
67
- test_consume_invalid_message(MyConsumer, 'invalid' => 'key')
68
- end
65
+ it 'should consume a message on a topic' do
66
+ test_consume_message('my_consume_topic',
67
+ 'test_id' => 'foo',
68
+ 'some_int' => 123) do |payload, _metadata|
69
+ expect(payload['test_id']).to eq('foo')
70
+ expect(payload['some_int']).to eq(123)
71
+ end
72
+ end
69
73
 
70
- it 'should fail if reraise is false but fatal_error is true' do
71
- Deimos.configure { |config| config.consumers.reraise_errors = false }
72
- test_consume_invalid_message(MyConsumer, 'fatal')
73
- end
74
+ it 'should fail on invalid message' do
75
+ test_consume_invalid_message(MyConsumer, 'invalid' => 'key')
76
+ end
74
77
 
75
- it 'should fail if fatal_error is true globally' do
76
- Deimos.configure do |config|
77
- config.consumers.fatal_error = proc { true }
78
- config.consumers.reraise_errors = false
79
- end
80
- test_consume_invalid_message(MyConsumer, 'invalid' => 'key')
81
- end
78
+ it 'should fail if reraise is false but fatal_error is true' do
79
+ Deimos.configure { |config| config.consumers.reraise_errors = false }
80
+ test_consume_invalid_message(MyConsumer, 'fatal')
81
+ end
82
82
 
83
- it 'should fail on message with extra fields' do
84
- test_consume_invalid_message(MyConsumer,
85
- 'test_id' => 'foo',
86
- 'some_int' => 123,
87
- 'extra_field' => 'field name')
88
- end
83
+ it 'should fail if fatal_error is true globally' do
84
+ Deimos.configure do |config|
85
+ config.consumers.fatal_error = proc { true }
86
+ config.consumers.reraise_errors = false
87
+ end
88
+ test_consume_invalid_message(MyConsumer, 'invalid' => 'key')
89
+ end
89
90
 
90
- it 'should not fail when before_consume fails without reraising errors' do
91
- Deimos.configure { |config| config.consumers.reraise_errors = false }
92
- expect {
93
- test_consume_message(
94
- MyConsumer,
95
- { 'test_id' => 'foo',
96
- 'some_int' => 123 },
97
- { skip_expectation: true }
98
- ) { raise 'OH NOES' }
99
- }.not_to raise_error
100
- end
91
+ it 'should fail on message with extra fields' do
92
+ test_consume_invalid_message(MyConsumer,
93
+ 'test_id' => 'foo',
94
+ 'some_int' => 123,
95
+ 'extra_field' => 'field name')
96
+ end
101
97
 
102
- it 'should not fail when consume fails without reraising errors' do
103
- Deimos.configure { |config| config.consumers.reraise_errors = false }
104
- expect {
105
- test_consume_message(
106
- MyConsumer,
107
- { 'invalid' => 'key' },
108
- { skip_expectation: true }
109
- )
110
- }.not_to raise_error
111
- end
98
+ it 'should not fail when before_consume fails without reraising errors' do
99
+ Deimos.configure { |config| config.consumers.reraise_errors = false }
100
+ expect {
101
+ test_consume_message(
102
+ MyConsumer,
103
+ { 'test_id' => 'foo',
104
+ 'some_int' => 123 },
105
+ { skip_expectation: true }
106
+ ) { raise 'OH NOES' }
107
+ }.not_to raise_error
108
+ end
109
+
110
+ it 'should not fail when consume fails without reraising errors' do
111
+ Deimos.configure { |config| config.consumers.reraise_errors = false }
112
+ expect {
113
+ test_consume_message(
114
+ MyConsumer,
115
+ { 'invalid' => 'key' },
116
+ { skip_expectation: true }
117
+ )
118
+ }.not_to raise_error
119
+ end
112
120
 
113
- it 'should call original' do
114
- expect {
115
- test_consume_message(MyConsumer,
116
- { 'test_id' => 'foo', 'some_int' => 123 },
117
- { call_original: true })
118
- }.to raise_error('This should not be called unless call_original is set')
121
+ it 'should call original' do
122
+ expect {
123
+ test_consume_message(MyConsumer,
124
+ { 'test_id' => 'foo', 'some_int' => 123 },
125
+ { call_original: true })
126
+ }.to raise_error('This should not be called unless call_original is set')
127
+ end
128
+ end
129
+ end
119
130
  end
120
131
 
121
132
  describe 'decode_key' do
@@ -28,6 +28,7 @@ RSpec.describe Deimos::Generators::ActiveRecordGenerator do
28
28
  t.bigint :a_long
29
29
  t.float :a_float
30
30
  t.float :a_double
31
+ t.string :an_optional_int
31
32
  t.string :an_enum
32
33
  t.json :an_array
33
34
  t.json :a_map
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ # For testing the generated class.
4
+ RSpec.describe Schemas::MySchemaWithComplexTypes do
5
+ let(:payload_hash) do
6
+ {
7
+ test_id: 'test id',
8
+ test_float: 1.2,
9
+ test_string_array: %w(abc def),
10
+ test_int_array: [123, 456],
11
+ some_integer_map: { 'int_1' => 1, 'int_2' => 2 },
12
+ some_record: Schemas::ARecord.new(a_record_field: 'field 1'),
13
+ some_optional_record: Schemas::ARecord.new(a_record_field: 'field 2'),
14
+ some_record_array: [Schemas::ARecord.new(a_record_field: 'field 3'),
15
+ Schemas::ARecord.new(a_record_field: 'field 4')],
16
+ some_record_map: {
17
+ 'record_1' => Schemas::ARecord.new(a_record_field: 'field 5'),
18
+ 'record_2' => Schemas::ARecord.new(a_record_field: 'field 6')
19
+ },
20
+ some_enum_array: [Schemas::AnEnum.new('sym1'),
21
+ Schemas::AnEnum.new('sym2')]
22
+ }
23
+ end
24
+
25
+ describe 'class initialization' do
26
+
27
+ it 'should initialize the class from keyword arguments' do
28
+ klass = described_class.new(
29
+ test_id: payload_hash[:test_id],
30
+ test_float: payload_hash[:test_float],
31
+ test_string_array: payload_hash[:test_string_array],
32
+ some_record: payload_hash[:some_record],
33
+ some_optional_record: payload_hash[:some_optional_record],
34
+ some_record_array: payload_hash[:some_record_array],
35
+ some_record_map: payload_hash[:some_record_map],
36
+ some_enum_array: payload_hash[:some_enum_array]
37
+ )
38
+ expect(klass).to be_instance_of(described_class)
39
+ end
40
+
41
+ it 'should initialize the class from a hash with symbols as keys' do
42
+ klass = described_class.new(**payload_hash)
43
+ expect(klass).to be_instance_of(described_class)
44
+ end
45
+
46
+ it 'should initialize the class when missing attributes' do
47
+ payload_hash.delete(:test_id)
48
+ klass = described_class.new(**payload_hash)
49
+ expect(klass).to be_instance_of(described_class)
50
+ end
51
+
52
+ end
53
+
54
+ describe 'base class methods' do
55
+ let(:klass) do
56
+ described_class.new(**payload_hash)
57
+ end
58
+
59
+ let(:schema_fields) do
60
+ %w(test_id test_float test_int_array test_optional_int test_string_array some_integer_map some_record some_optional_record some_record_array some_record_map some_enum_array)
61
+ end
62
+
63
+ it 'should return the name of the schema and namespace' do
64
+ expect(klass.schema).to eq('MySchemaWithComplexTypes')
65
+ expect(klass.namespace).to eq('com.my-namespace')
66
+ expect(klass.full_schema).to eq('com.my-namespace.MySchemaWithComplexTypes')
67
+ end
68
+
69
+ it 'should return an array of all fields in the schema' do
70
+ expect(klass.schema_fields).to match_array(schema_fields)
71
+ end
72
+
73
+ it 'should return a json version of the payload' do
74
+ described_class.new(**payload_hash)
75
+ payload_h = {
76
+ 'test_id' => 'test id',
77
+ 'test_float' => 1.2,
78
+ 'test_string_array' => %w(abc def),
79
+ 'test_int_array' => [123, 456],
80
+ 'test_optional_int' => 123,
81
+ 'some_integer_map' => { 'int_1' => 1, 'int_2' => 2 },
82
+ 'some_record' => { 'a_record_field' => 'field 1' },
83
+ 'some_optional_record' => { 'a_record_field' => 'field 2' },
84
+ 'some_record_array' => [
85
+ { 'a_record_field' => 'field 3' },
86
+ { 'a_record_field' => 'field 4' }
87
+ ],
88
+ 'some_record_map' => {
89
+ 'record_1' => { 'a_record_field' => 'field 5' },
90
+ 'record_2' => { 'a_record_field' => 'field 6' }
91
+ },
92
+ 'some_enum_array' => %w(sym1 sym2)
93
+ }
94
+
95
+ expect(klass.as_json).to eq(payload_h)
96
+ end
97
+
98
+ it 'should return a JSON string of the payload' do
99
+ s = '{"test_id":"test id","test_float":1.2,"test_string_array":["abc","def"],"test_int_array":[123,456],"test_optional_int":123,"some_integer_map":{"int_1":1,"int_2":2},"some_record":{"a_record_field":"field 1"},"some_optional_record":{"a_record_field":"field 2"},"some_record_array":[{"a_record_field":"field 3"},{"a_record_field":"field 4"}],"some_record_map":{"record_1":{"a_record_field":"field 5"},"record_2":{"a_record_field":"field 6"}},"some_enum_array":["sym1","sym2"]}'
100
+ expect(klass.to_json).to eq(s)
101
+ end
102
+ end
103
+
104
+ describe 'defaults' do
105
+ it 'should set an_optional_int if it is not provided' do
106
+ payload_hash.delete(:an_optional_int)
107
+ klass = described_class.new(**payload_hash)
108
+ expect(klass.test_optional_int).to eq(123)
109
+ end
110
+
111
+ it 'should set some_record if it is not provided' do
112
+ payload_hash.delete(:some_record)
113
+ klass = described_class.new(**payload_hash)
114
+ expect(klass.some_record).to eq(Schemas::ARecord.new(a_record_field: 'Test String'))
115
+ end
116
+
117
+ it 'should set some_record to nil' do
118
+ klass = described_class.new(**payload_hash.merge(some_record: nil))
119
+ expect(klass.some_record).to be_nil
120
+ end
121
+ end
122
+
123
+ describe 'getters and setters' do
124
+ let(:klass) do
125
+ described_class.new(**payload_hash)
126
+ end
127
+
128
+ context 'when getting attributes' do
129
+ it 'should get of values of primitive types' do
130
+ expect(klass.test_id).to eq('test id')
131
+ expect(klass.test_float).to eq(1.2)
132
+ expect(klass.test_string_array).to eq(%w(abc def))
133
+ end
134
+
135
+ it 'should get the value of some_record_array' do
136
+ some_record_array = klass.some_record_array
137
+ expect(some_record_array.first).to be_instance_of(Schemas::ARecord)
138
+ expect(some_record_array.first.a_record_field).to eq('field 3')
139
+ end
140
+
141
+ it 'should get the value of some_record_map' do
142
+ some_record_map = klass.some_record_map
143
+ expect(some_record_map['record_1']).to be_instance_of(Schemas::ARecord)
144
+ expect(some_record_map['record_1'].a_record_field).to eq('field 5')
145
+ end
146
+
147
+ it 'should get the value of some_enum_array' do
148
+ some_enum_array = klass.some_enum_array
149
+ expect(some_enum_array.first).to be_instance_of(Schemas::AnEnum)
150
+ expect(some_enum_array.first.an_enum).to eq('sym1')
151
+ end
152
+
153
+ it 'should get the value of some_record' do
154
+ record = klass.some_record
155
+ expect(record).to be_instance_of(Schemas::ARecord)
156
+ expect(record.a_record_field).to eq('field 1')
157
+ expect(record.to_h).to eq({ 'a_record_field' => 'field 1' })
158
+ end
159
+
160
+ it 'should support Hash-style element access of values' do
161
+ expect(klass['test_id']).to eq('test id')
162
+ expect(klass['test_float']).to eq(1.2)
163
+ expect(klass['test_string_array']).to eq(%w(abc def))
164
+ end
165
+ end
166
+
167
+ context 'when setting attributes' do
168
+ it 'should modify the value of test_id' do
169
+ expect(klass.test_id).to eq('test id')
170
+
171
+ klass.test_id = 'something different'
172
+ expect(klass.test_id).to eq('something different')
173
+ end
174
+
175
+ it 'should modify the value of some_optional_record' do
176
+ expect(klass.some_optional_record).to eq(Schemas::ARecord.new(a_record_field: 'field 2'))
177
+ klass.some_optional_record = Schemas::ARecord.new(a_record_field: 'new field')
178
+
179
+ expect(klass.some_optional_record).to eq(Schemas::ARecord.new(a_record_field: 'new field'))
180
+ expect(klass.some_optional_record.as_json).to eq({ 'a_record_field' => 'new field' })
181
+ end
182
+
183
+ it 'should accept a hash object inner records' do
184
+ klass.some_optional_record = { a_record_field: 'new field' }
185
+ expect(klass.some_optional_record).to eq(Schemas::ARecord.new(a_record_field: 'new field'))
186
+ expect(klass.some_optional_record.as_json).to eq({ 'a_record_field' => 'new field' })
187
+ end
188
+
189
+ it 'should modify the value of some_enum_array' do
190
+ klass.some_enum_array.first.an_enum = 'new_sym'
191
+ expect(klass.some_enum_array.first).to eq(Schemas::AnEnum.new('new_sym'))
192
+
193
+ klass.some_enum_array.second.an_enum = Schemas::AnEnum.new('other_sym')
194
+ expect(klass.some_enum_array.second.an_enum).to eq('other_sym')
195
+ end
196
+
197
+ it 'should modify the value of some_record_map' do
198
+ klass.some_record_map['record_1'].a_record_field = 'new field'
199
+ expect(klass.some_record_map['record_1']).to eq(Schemas::ARecord.new(a_record_field: 'new field'))
200
+
201
+ klass.some_record_map['record_2'] = Schemas::ARecord.new(a_record_field: 'other field')
202
+ expect(klass.some_record_map['record_2']).to eq(Schemas::ARecord.new(a_record_field: 'other field'))
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'generators/deimos/schema_class_generator'
4
+ require 'fileutils'
5
+
6
+ RSpec.describe Deimos::Generators::SchemaClassGenerator do
7
+ let(:schema_class_path) { 'app/lib/schema_classes/com/my-namespace' }
8
+ let(:expected_files) { Dir['spec/schema_classes/*.rb'] }
9
+ let(:files) { Dir["#{schema_class_path}/*.rb"] }
10
+
11
+ before(:each) do
12
+ Deimos.config.reset!
13
+ Deimos.configure do
14
+ schema.path 'spec/schemas/'
15
+ schema.generated_class_path 'app/lib/schema_classes'
16
+ schema.backend :avro_local
17
+ end
18
+ end
19
+
20
+ after(:each) do
21
+ FileUtils.rm_rf(schema_class_path) if File.exist?(schema_class_path)
22
+ end
23
+
24
+ context 'with a Consumers Schema' do
25
+ before(:each) do
26
+ Deimos.configure do
27
+ consumer do
28
+ class_name 'ConsumerTest::MyConsumer'
29
+ topic 'MyTopic'
30
+ schema 'Generated'
31
+ namespace 'com.my-namespace'
32
+ key_config field: :a_string
33
+ end
34
+ end
35
+ described_class.start
36
+ end
37
+
38
+ it 'should generate the correct number of classes' do
39
+ expect(files.length).to eq(1)
40
+ end
41
+
42
+ it 'should generate a schema class for generated' do
43
+ generated_path = files.select { |f| f =~ /generated/ }.first
44
+ expected_path = expected_files.select { |f| f =~ /generated/ }.first
45
+
46
+ expect(File.read(generated_path)).to eq(File.read(expected_path))
47
+ end
48
+ end
49
+
50
+ context 'with a Consumers Schema with Complex types' do
51
+ before(:each) do
52
+ Deimos.configure do
53
+ consumer do
54
+ class_name 'ConsumerTest::MyConsumer'
55
+ topic 'MyTopic'
56
+ schema 'MySchemaWithComplexTypes'
57
+ namespace 'com.my-namespace'
58
+ key_config field: :a_string
59
+ end
60
+ end
61
+ described_class.start
62
+ end
63
+
64
+ it 'should generate the correct number of classes' do
65
+ expect(files.length).to eq(1)
66
+ end
67
+
68
+ it 'should generate a schema class for my_schema_with_complex_types' do
69
+ generated_path = files.select { |f| f =~ /my_schema_with_complex_types/ }.first
70
+ expected_path = expected_files.select { |f| f =~ /my_schema_with_complex_types/ }.first
71
+
72
+ expect(File.read(generated_path)).to eq(File.read(expected_path))
73
+ end
74
+ end
75
+
76
+ context 'with a Producers Schema and a Key' do
77
+ before(:each) do
78
+ Deimos.configure do
79
+ producer do
80
+ class_name 'ConsumerTest::MyConsumer'
81
+ topic 'MyTopic'
82
+ schema 'MySchema'
83
+ namespace 'com.my-namespace'
84
+ key_config schema: 'MySchema-key'
85
+ end
86
+ end
87
+ described_class.start
88
+ end
89
+
90
+ it 'should generate the correct number of classes' do
91
+ expect(files.length).to eq(2)
92
+ end
93
+
94
+ %w(my_schema my_schema_key).each do |klass|
95
+ it "should generate a schema class for #{klass}" do
96
+ generated_path = files.select { |f| f =~ /#{klass}\.rb/ }.first
97
+ expected_path = expected_files.select { |f| f =~ /#{klass}\.rb/ }.first
98
+
99
+ expect(File.read(generated_path)).to eq(File.read(expected_path))
100
+ end
101
+ end
102
+ end
103
+
104
+ context 'with a Consumers Nested Schema' do
105
+ before(:each) do
106
+ Deimos.configure do
107
+ consumer do
108
+ class_name 'ConsumerTest::MyConsumer'
109
+ topic 'MyTopic'
110
+ schema 'MyNestedSchema'
111
+ namespace 'com.my-namespace'
112
+ key_config field: :test_id
113
+ end
114
+ end
115
+ described_class.start
116
+ end
117
+
118
+ it 'should generate the correct number of classes' do
119
+ expect(files.length).to eq(1)
120
+ end
121
+
122
+ it 'should generate a schema class for my_nested_schema' do
123
+ generated_path = files.select { |f| f =~ /my_nested_schema/ }.first
124
+ expected_path = expected_files.select { |f| f =~ /my_nested_schema/ }.first
125
+
126
+ expect(File.read(generated_path)).to eq(File.read(expected_path))
127
+ end
128
+ end
129
+
130
+ context 'with a mix of Consumer and Producer Schemas' do
131
+ before(:each) do
132
+ Deimos.configure do
133
+ consumer do
134
+ class_name 'ConsumerTest::MyConsumer'
135
+ topic 'MyTopic'
136
+ schema 'Generated'
137
+ namespace 'com.my-namespace'
138
+ key_config field: :a_string
139
+ end
140
+
141
+ producer do
142
+ class_name 'ConsumerTest::MyConsumer'
143
+ topic 'MyTopic'
144
+ schema 'MySchema'
145
+ namespace 'com.my-namespace'
146
+ key_config schema: 'MySchema-key'
147
+ end
148
+
149
+ producer do
150
+ class_name 'ConsumerTest::MyConsumer'
151
+ topic 'MyTopic'
152
+ schema 'MyNestedSchema'
153
+ namespace 'com.my-namespace'
154
+ key_config field: :test_id
155
+ end
156
+ end
157
+ described_class.start
158
+ end
159
+
160
+ it 'should generate the correct number of classes' do
161
+ expect(files.length).to eq(4)
162
+ end
163
+
164
+ %w(generated my_schema my_schema_key my_nested_schema).each do |klass|
165
+ it "should generate a schema class for #{klass}" do
166
+ generated_path = files.select { |f| f =~ /#{klass}\.rb/ }.first
167
+ expected_path = expected_files.select { |f| f =~ /#{klass}\.rb/ }.first
168
+
169
+ expect(File.read(generated_path)).to eq(File.read(expected_path))
170
+ end
171
+ end
172
+ end
173
+
174
+ context 'with non-avro schema backends' do
175
+ before(:each) do
176
+ Deimos.config.schema.backend :mock
177
+ end
178
+
179
+ it 'should fail to start schema class generation' do
180
+ expect {
181
+ described_class.start
182
+ }.to raise_error(message='Schema Class Generation requires an Avro-based Schema Backend')
183
+ end
184
+ end
185
+
186
+ end