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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -2
- data/Gemfile.lock +8 -8
- data/README.md +103 -0
- data/deimos-ruby.gemspec +1 -1
- data/docs/CONFIGURATION.md +4 -0
- data/lib/deimos/active_record_consume/batch_consumption.rb +7 -5
- data/lib/deimos/active_record_consume/message_consumption.rb +4 -3
- data/lib/deimos/active_record_consumer.rb +2 -2
- data/lib/deimos/active_record_producer.rb +3 -0
- data/lib/deimos/config/configuration.rb +29 -0
- data/lib/deimos/consume/batch_consumption.rb +2 -2
- data/lib/deimos/consume/message_consumption.rb +2 -2
- data/lib/deimos/consumer.rb +10 -0
- data/lib/deimos/producer.rb +4 -3
- data/lib/deimos/schema_backends/avro_base.rb +64 -1
- data/lib/deimos/schema_backends/avro_schema_registry.rb +1 -1
- data/lib/deimos/schema_backends/base.rb +18 -2
- data/lib/deimos/schema_class/base.rb +62 -0
- data/lib/deimos/schema_class/enum.rb +24 -0
- data/lib/deimos/schema_class/record.rb +66 -0
- data/lib/deimos/shared_config.rb +5 -0
- data/lib/deimos/test_helpers.rb +43 -7
- data/lib/deimos/utils/schema_class.rb +29 -0
- data/lib/deimos/version.rb +1 -1
- data/lib/deimos.rb +23 -0
- data/lib/generators/deimos/schema_class/templates/schema_class.rb.tt +15 -0
- data/lib/generators/deimos/schema_class/templates/schema_enum.rb.tt +21 -0
- data/lib/generators/deimos/schema_class/templates/schema_record.rb.tt +65 -0
- data/lib/generators/deimos/schema_class_generator.rb +247 -0
- data/lib/tasks/deimos.rake +8 -0
- data/spec/active_record_batch_consumer_spec.rb +120 -110
- data/spec/active_record_consumer_spec.rb +97 -88
- data/spec/active_record_producer_spec.rb +38 -27
- data/spec/batch_consumer_spec.rb +37 -28
- data/spec/config/configuration_spec.rb +10 -3
- data/spec/consumer_spec.rb +95 -84
- data/spec/generators/active_record_generator_spec.rb +1 -0
- data/spec/generators/schema_class/my_schema_with_complex_types_spec.rb +206 -0
- data/spec/generators/schema_class_generator_spec.rb +186 -0
- data/spec/producer_spec.rb +110 -0
- data/spec/schema_classes/generated.rb +156 -0
- data/spec/schema_classes/my_nested_schema.rb +114 -0
- data/spec/schema_classes/my_schema.rb +53 -0
- data/spec/schema_classes/my_schema_key.rb +35 -0
- data/spec/schema_classes/my_schema_with_complex_types.rb +172 -0
- data/spec/schemas/com/my-namespace/Generated.avsc +6 -0
- data/spec/schemas/com/my-namespace/MySchemaWithComplexTypes.avsc +95 -0
- data/spec/spec_helper.rb +6 -1
- 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: {
|
data/spec/consumer_spec.rb
CHANGED
@@ -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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
74
|
+
it 'should fail on invalid message' do
|
75
|
+
test_consume_invalid_message(MyConsumer, 'invalid' => 'key')
|
76
|
+
end
|
74
77
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
@@ -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
|