deimos-ruby 1.11.1 → 1.12.2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -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 -5
  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 +23 -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