deimos-ruby 2.5.3 → 2.6.0.pre.beta1

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +20 -9
  4. data/deimos-ruby.gemspec +1 -0
  5. data/docs/CONFIGURATION.md +11 -4
  6. data/lib/deimos/active_record_consume/batch_consumption.rb +1 -1
  7. data/lib/deimos/active_record_consume/message_consumption.rb +2 -2
  8. data/lib/deimos/active_record_consumer.rb +1 -1
  9. data/lib/deimos/active_record_producer.rb +1 -1
  10. data/lib/deimos/config/configuration.rb +34 -11
  11. data/lib/deimos/ext/producer_middleware.rb +2 -2
  12. data/lib/deimos/logging.rb +9 -0
  13. data/lib/deimos/producer.rb +5 -5
  14. data/lib/deimos/schema_backends/avro_base.rb +7 -36
  15. data/lib/deimos/schema_class.rb +72 -0
  16. data/lib/deimos/transcoder.rb +6 -6
  17. data/lib/deimos/utils/schema_class.rb +14 -47
  18. data/lib/deimos/version.rb +1 -1
  19. data/lib/deimos.rb +3 -3
  20. data/lib/tasks/deimos.rake +16 -4
  21. data/regenerate_test_schema_classes.rb +12 -3
  22. data/spec/active_record_batch_consumer_association_spec.rb +1 -1
  23. data/spec/active_record_batch_consumer_spec.rb +1 -1
  24. data/spec/active_record_consume/batch_consumption_spec.rb +1 -1
  25. data/spec/active_record_consumer_spec.rb +3 -3
  26. data/spec/active_record_producer_spec.rb +1 -1
  27. data/spec/batch_consumer_spec.rb +2 -2
  28. data/spec/consumer_spec.rb +9 -9
  29. data/spec/schema_class_spec.rb +65 -0
  30. data/spec/schemas/my_namespace/generated.rb +4 -4
  31. data/spec/schemas/my_namespace/my_long_namespace_schema.rb +2 -2
  32. data/spec/schemas/my_namespace/my_nested_schema.rb +3 -3
  33. data/spec/schemas/my_namespace/my_schema.rb +2 -2
  34. data/spec/schemas/my_namespace/my_schema_compound_key.rb +2 -2
  35. data/spec/schemas/my_namespace/my_schema_id_key.rb +2 -2
  36. data/spec/schemas/my_namespace/my_schema_key.rb +2 -2
  37. data/spec/schemas/my_namespace/my_schema_with_boolean.rb +2 -2
  38. data/spec/schemas/my_namespace/my_schema_with_circular_reference.rb +3 -3
  39. data/spec/schemas/my_namespace/my_schema_with_complex_type.rb +6 -6
  40. data/spec/schemas/my_namespace/my_schema_with_date_time.rb +2 -2
  41. data/spec/schemas/my_namespace/my_schema_with_id.rb +2 -2
  42. data/spec/schemas/my_namespace/my_schema_with_title.rb +2 -2
  43. data/spec/schemas/my_namespace/my_schema_with_union_type.rb +6 -6
  44. data/spec/schemas/my_namespace/my_schema_with_unique_id.rb +2 -2
  45. data/spec/schemas/my_namespace/my_updated_schema.rb +1 -1
  46. data/spec/schemas/my_namespace/request/create_topic.rb +2 -2
  47. data/spec/schemas/my_namespace/request/index.rb +2 -2
  48. data/spec/schemas/my_namespace/request/update_request.rb +2 -2
  49. data/spec/schemas/my_namespace/response/create_topic.rb +2 -2
  50. data/spec/schemas/my_namespace/response/index.rb +2 -2
  51. data/spec/schemas/my_namespace/response/update_response.rb +2 -2
  52. data/spec/schemas/my_namespace/wibble.rb +2 -2
  53. data/spec/schemas/my_namespace/widget.rb +2 -2
  54. data/spec/schemas/my_namespace/widget_the_second.rb +2 -2
  55. data/spec/schemas/my_namespace/widget_the_third.rb +2 -2
  56. data/spec/spec_helper.rb +2 -2
  57. data/spec/utils/db_poller_spec.rb +1 -1
  58. metadata +18 -27
  59. data/lib/deimos/schema_class/base.rb +0 -62
  60. data/lib/deimos/schema_class/enum.rb +0 -49
  61. data/lib/deimos/schema_class/record.rb +0 -100
  62. data/lib/generators/deimos/schema_class/templates/schema_class.rb.tt +0 -8
  63. data/lib/generators/deimos/schema_class/templates/schema_enum.rb.tt +0 -13
  64. data/lib/generators/deimos/schema_class/templates/schema_record.rb.tt +0 -102
  65. data/lib/generators/deimos/schema_class_generator.rb +0 -369
  66. data/spec/generators/schema_class/my_schema_spec.rb +0 -16
  67. data/spec/generators/schema_class/my_schema_with_circular_reference_spec.rb +0 -98
  68. data/spec/generators/schema_class/my_schema_with_complex_types_spec.rb +0 -237
  69. data/spec/generators/schema_class_generator_spec.rb +0 -283
  70. data/spec/snapshots/consumers-no-nest.snap +0 -1740
  71. data/spec/snapshots/consumers.snap +0 -1720
  72. data/spec/snapshots/consumers_and_producers-no-nest.snap +0 -1740
  73. data/spec/snapshots/consumers_and_producers.snap +0 -1720
  74. data/spec/snapshots/consumers_circular-no-nest.snap +0 -1740
  75. data/spec/snapshots/consumers_circular.snap +0 -1720
  76. data/spec/snapshots/consumers_complex_types-no-nest.snap +0 -1740
  77. data/spec/snapshots/consumers_complex_types.snap +0 -1720
  78. data/spec/snapshots/consumers_nested-no-nest.snap +0 -1740
  79. data/spec/snapshots/consumers_nested.snap +0 -1720
  80. data/spec/snapshots/namespace_folders.snap +0 -1800
  81. data/spec/snapshots/namespace_map.snap +0 -1800
  82. data/spec/snapshots/producers_with_key-no-nest.snap +0 -1740
  83. data/spec/snapshots/producers_with_key.snap +0 -1720
@@ -1,369 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rails/generators'
4
- require 'deimos'
5
- require 'deimos/schema_backends/avro_base'
6
-
7
- # Generates new schema classes.
8
- module Deimos
9
- module Generators
10
- # Generator for Schema Classes used for the IDE and consumer/producer interfaces
11
- class SchemaClassGenerator < Rails::Generators::Base
12
-
13
- # @return [Array<Symbol>]
14
- SPECIAL_TYPES = %i(record enum).freeze
15
- # @return [String]
16
- INITIALIZE_WHITESPACE = "\n#{' ' * 19}".freeze
17
- # @return [Array<String>]
18
- IGNORE_DEFAULTS = %w(message_id timestamp).freeze
19
- # @return [String]
20
- SCHEMA_CLASS_FILE = 'schema_class.rb'
21
- # @return [String]
22
- SCHEMA_RECORD_PATH = File.expand_path('schema_class/templates/schema_record.rb.tt', __dir__).freeze
23
- # @return [String]
24
- SCHEMA_ENUM_PATH = File.expand_path('schema_class/templates/schema_enum.rb.tt', __dir__).freeze
25
-
26
- source_root File.expand_path('schema_class/templates', __dir__)
27
-
28
- no_commands do
29
- # Retrieve the fields from this Avro Schema
30
- # @param schema [Avro::Schema::NamedSchema]
31
- # @return [Array<SchemaField>]
32
- def fields(schema)
33
- schema.fields.map do |field|
34
- Deimos::SchemaField.new(field.name, field.type, [], field.default)
35
- end
36
- end
37
-
38
- # Converts Deimos::SchemaField's to String form for generated YARD docs
39
- # @param schema_field [Deimos::SchemaField]
40
- # @return [String] A string representation of the Type of this SchemaField
41
- def deimos_field_type(schema_field)
42
- _field_type(schema_field.type)
43
- end
44
-
45
- # Generate a Schema Model Class and all of its Nested Records from a
46
- # Deimos Consumer or Producer Configuration object
47
- # @param schema_name [String]
48
- # @param namespace [String]
49
- # @param key_config [Hash,nil]
50
- # @return [void]
51
- def generate_classes(schema_name, namespace, key_config, backend: nil)
52
- schema_base = Deimos.schema_backend(schema: schema_name,
53
- namespace: namespace,
54
- backend: backend)
55
- return unless schema_base.supports_class_generation?
56
-
57
- schema_base.load_schema
58
- if key_config&.dig(:schema)
59
- key_schema_base = Deimos.schema_backend(schema: key_config[:schema],
60
- namespace: namespace,
61
- backend: backend)
62
- key_schema_base.load_schema
63
- generate_class_from_schema_base(key_schema_base, key_config: nil)
64
- end
65
- generate_class_from_schema_base(schema_base, key_config: key_config)
66
- end
67
-
68
- # @param schema [Avro::Schema::NamedSchema]
69
- # @return [Array<Avro::Schema::NamedSchema]
70
- def child_schemas(schema)
71
- if schema.respond_to?(:fields)
72
- schema.fields.map(&:type)
73
- elsif schema.respond_to?(:values)
74
- [schema.values]
75
- elsif schema.respond_to?(:items)
76
- [schema.items]
77
- elsif schema.respond_to?(:schemas)
78
- schema.schemas.reject { |s| s.instance_of?(Avro::Schema::PrimitiveSchema) }
79
- else
80
- []
81
- end
82
- end
83
-
84
- # @param schemas [Array<Avro::Schema::NamedSchema>]
85
- # @return [Array<Avro::Schema::NamedSchema>]
86
- def collect_all_schemas(schemas)
87
- schemas.dup.each do |schema|
88
- next if @discovered_schemas.include?(schema)
89
-
90
- @discovered_schemas << schema
91
- schemas.concat(collect_all_schemas(child_schemas(schema)))
92
- end
93
-
94
- schemas.select { |s| s.respond_to?(:name) }.uniq
95
- end
96
-
97
- # @param schema_base [Deimos::SchemaBackends::Base]
98
- # @param key_config [Hash,nil]
99
- # @return [void]
100
- def generate_class_from_schema_base(schema_base, key_config: nil)
101
- @discovered_schemas = Set.new
102
- @sub_schema_templates = []
103
- schemas = collect_all_schemas(schema_base.schema_store.schemas.values)
104
-
105
- main_schema = schemas.find { |s| s.name == schema_base.schema }
106
- sub_schemas = schemas.reject { |s| s.name == schema_base.schema }.sort_by(&:name)
107
- if Deimos.config.schema.nest_child_schemas
108
- @sub_schema_templates = sub_schemas.map do |schema|
109
- _generate_class_template_from_schema(schema, nil)
110
- end
111
- write_file(main_schema, key_config)
112
- else
113
- write_file(main_schema, key_config)
114
- sub_schemas.each do |schema|
115
- write_file(schema, nil)
116
- end
117
- end
118
- end
119
-
120
- # @param schema [Avro::Schema::NamedSchema]
121
- # @param key_config [Hash,nil]
122
- # @return [void]
123
- def write_file(schema, key_config)
124
- class_template = _generate_class_template_from_schema(schema, key_config)
125
- @modules = Utils::SchemaClass.modules_for(schema.namespace)
126
- @main_class_definition = class_template
127
-
128
- file_prefix = schema.name.underscore.singularize
129
- if Deimos.config.schema.use_full_namespace
130
- # Use entire namespace for folders
131
- # but don't add directories that are already in the path
132
- directories = @modules.map(&:underscore).select do |m|
133
- Deimos.config.schema.generated_class_path.exclude?(m)
134
- end
135
-
136
- file_prefix = "#{directories.join('/')}/#{file_prefix}"
137
- end
138
-
139
- filename = "#{Deimos.config.schema.generated_class_path}/#{file_prefix}.rb"
140
- template(SCHEMA_CLASS_FILE, filename, force: true)
141
- end
142
-
143
- # Format a given field into its appropriate to_h representation.
144
- # @param field[Deimos::SchemaField]
145
- # @return [String]
146
- def field_as_json(field)
147
- res = "'#{field.name}' => @#{field.name}"
148
- field_base_type = _schema_base_class(field.type).type_sym
149
-
150
- if %i(record enum).include?(field_base_type)
151
- res += case field.type.type_sym
152
- when :array
153
- '.map { |v| v&.as_json }'
154
- when :map
155
- '.transform_values { |v| v&.as_json }'
156
- else
157
- '&.as_json'
158
- end
159
- end
160
-
161
- res + (field.name == @fields.last.name ? '' : ',')
162
- end
163
-
164
- end
165
-
166
- desc 'Generate a class based on configured consumer and producers.'
167
- # @return [void]
168
- def generate
169
- _validate
170
- Rails.logger.info("Generating schemas from Deimos.config to #{Deimos.config.schema.generated_class_path}")
171
- found_schemas = {}
172
- Deimos.karafka_configs.each do |config|
173
- schema_name = config.schema
174
- next if schema_name.nil?
175
-
176
- namespace = config.namespace || Deimos.config.producers.schema_namespace
177
- key_schema_name = config.key_config[:schema]
178
-
179
- # don't regenerate if the schema was already found and had a payload key
180
- next if found_schemas["#{namespace}.#{schema_name}"].present?
181
-
182
- found_schemas["#{namespace}.#{schema_name}"] = key_schema_name
183
- found_schemas["#{namespace}.#{key_schema_name}"] = nil
184
- generate_classes(schema_name, namespace, config.key_config, backend: config.schema_backend)
185
- end
186
-
187
- generate_from_schema_files(found_schemas.keys)
188
-
189
- end
190
-
191
- private
192
-
193
- def generate_from_schema_files(found_schemas)
194
- path = Deimos.config.schema.path || Deimos.config.schema.paths[:avro].first
195
- schema_store = SchemaRegistry::AvroSchemaStore.new(path: path)
196
- schema_store.load_schemas!
197
- schema_store.schemas.values.sort_by { |s| "#{s.namespace}#{s.name}" }.each do |schema|
198
- name = "#{schema.namespace}.#{schema.name}"
199
- next if found_schemas.include?(name)
200
-
201
- generate_classes(schema.name, schema.namespace, nil)
202
- end
203
- end
204
-
205
- # Determines if Schema Class Generation can be run.
206
- # @raise if Schema Backend is not of a Avro-based class
207
- def _validate
208
- backend = Deimos.config.schema.backend.to_s
209
- raise 'Schema Class Generation requires an Avro-based Schema Backend' if backend !~ /^avro/
210
- end
211
-
212
- # @param schema[Avro::Schema::NamedSchema]
213
- # @param key_config[Hash,nil]
214
- # @return [String]
215
- def _generate_class_template_from_schema(schema, key_config)
216
- _set_instance_variables(schema, key_config)
217
-
218
- temp = schema.is_a?(Avro::Schema::RecordSchema) ? _record_class_template : _enum_class_template
219
- res = ERB.new(temp, trim_mode: '-')
220
- res.result(binding)
221
- end
222
-
223
- # @param schema[Avro::Schema::NamedSchema]
224
- # @param key_config [Hash,nil]
225
- def _set_instance_variables(schema, key_config)
226
- schema_is_record = schema.is_a?(Avro::Schema::RecordSchema)
227
- @current_schema = schema
228
- return unless schema_is_record
229
-
230
- @fields = fields(schema)
231
- key_schema = nil
232
- if key_config&.dig(:schema)
233
- key_schema_base = Deimos.schema_backend(schema: key_config[:schema], namespace: schema.namespace)
234
- key_schema_base.load_schema
235
- key_schema = key_schema_base.schema_store.schemas.values.first
236
- @fields << Deimos::SchemaField.new('payload_key', key_schema, [], nil)
237
- end
238
- @initialization_definition = _initialization_definition
239
- @field_assignments = _field_assignments
240
- @tombstone_assignment = _tombstone_assignment(key_config, key_schema)
241
- end
242
-
243
- def _tombstone_assignment(key_config, key_schema)
244
- return nil unless key_config
245
-
246
- if key_config[:plain]
247
- "record.tombstone_key = key"
248
- elsif key_config[:field]
249
- "record.tombstone_key = key\n record.#{key_config[:field]} = key"
250
- elsif key_schema
251
- field_base_type = _field_type(key_schema)
252
- "record.tombstone_key = #{field_base_type}.initialize_from_value(key)\n record.payload_key = key"
253
- else
254
- ''
255
- end
256
- end
257
-
258
- # Defines the initialization method for Schema Records with one keyword argument per line
259
- # @return [String] A string which defines the method signature for the initialize method
260
- def _initialization_definition
261
- arguments = @fields.map do |schema_field|
262
- arg = "#{schema_field.name}:"
263
- arg += _field_default(schema_field)
264
- arg.strip
265
- end
266
-
267
- result = "def initialize(_from_message: false, #{arguments.first}"
268
- arguments[1..].each_with_index do |arg, _i|
269
- result += ",#{INITIALIZE_WHITESPACE}#{arg}"
270
- end
271
- "#{result})"
272
- end
273
-
274
- # @param field [SchemaField]
275
- # @return [String]
276
- def _field_default(field)
277
- default = field.default
278
- return ' nil' if default == :no_default || default.nil? || IGNORE_DEFAULTS.include?(field.name)
279
-
280
- type_sym = field.type.type_sym
281
- if type_sym == :union
282
- type_sym = field.type.schemas.find { |s| s.type_sym != :null }&.type_sym
283
- end
284
- case type_sym
285
- when :string, :enum
286
- " \"#{default}\""
287
- when :record
288
- schema_name = Deimos::SchemaBackends::AvroBase.schema_classname(field.type)
289
- class_instance = Utils::SchemaClass.instance(field.default, schema_name)
290
- " #{class_instance.to_h}"
291
- else
292
- " #{default}"
293
- end
294
- end
295
-
296
- # Overrides default attr accessor methods
297
- # @return [Array<String>]
298
- def _field_assignments
299
- result = []
300
- @fields.each do |field|
301
- field_type = field.type.type_sym # Record, Union, Enum, Array or Map
302
- schema_base_type = _schema_base_class(field.type)
303
- field_base_type = _field_type(schema_base_type)
304
- method_argument = %i(array map).include?(field_type) ? 'values' : 'value'
305
- is_schema_class = %i(record enum).include?(schema_base_type.type_sym)
306
-
307
- field_initialization = method_argument
308
-
309
- if _is_complex_union?(field)
310
- field_initialization = "initialize_#{field.name}_type(value, from_message: self._from_message)"
311
- elsif is_schema_class
312
- field_initialization = "#{field_base_type}.initialize_from_value(value, from_message: self._from_message)"
313
- end
314
-
315
- result << {
316
- field: field,
317
- field_type: field_type,
318
- is_schema_class: is_schema_class,
319
- method_argument: method_argument,
320
- deimos_type: deimos_field_type(field),
321
- field_initialization: field_initialization,
322
- is_complex_union: _is_complex_union?(field)
323
- }
324
- end
325
-
326
- result
327
- end
328
-
329
- # Helper method to detect if a field is a complex union type with multiple record schemas
330
- # @param field [Deimos::SchemaField]
331
- # @return [Boolean]
332
- def _is_complex_union?(field)
333
- return false unless field.type.type_sym == :union
334
-
335
- non_null_schemas = field.type.schemas.reject { |s| s.type_sym == :null }
336
-
337
- record_schemas = non_null_schemas.select { |s| s.type_sym == :record }
338
- record_schemas.length > 1
339
- end
340
-
341
- # Converts Avro::Schema::NamedSchema's to String form for generated YARD docs.
342
- # Recursively handles the typing for Arrays, Maps and Unions.
343
- # @param avro_schema [Avro::Schema::NamedSchema]
344
- # @return [String] A string representation of the Type of this SchemaField
345
- def _field_type(avro_schema)
346
- Deimos::SchemaBackends::AvroBase.field_type(avro_schema)
347
- end
348
-
349
- # Returns the base class for this schema. Decodes Arrays, Maps and Unions
350
- # @param avro_schema [Avro::Schema::NamedSchema]
351
- # @return [Avro::Schema::NamedSchema]
352
- def _schema_base_class(avro_schema)
353
- Deimos::SchemaBackends::AvroBase.schema_base_class(avro_schema)
354
- end
355
-
356
- # An ERB template for schema record classes
357
- # @return [String]
358
- def _record_class_template
359
- File.read(SCHEMA_RECORD_PATH).strip
360
- end
361
-
362
- # An ERB template for schema enum classes
363
- # @return [String]
364
- def _enum_class_template
365
- File.read(SCHEMA_ENUM_PATH).strip
366
- end
367
- end
368
- end
369
- end
@@ -1,16 +0,0 @@
1
- RSpec.describe Schemas::MyNamespace::MySchema do
2
- let(:key) { Schemas::MyNamespace::MySchemaKey.new(test_id: 123) }
3
-
4
- it 'should produce a tombstone with a hash' do
5
- result = described_class.tombstone({ test_id: 123 })
6
- expect(result.payload_key).to eq(key)
7
- expect(result.to_h).to eq({ payload_key: { 'test_id' => 123 } })
8
- end
9
-
10
- it 'should work with a record' do
11
- key = Schemas::MyNamespace::MySchemaKey.new(test_id: 123)
12
- result = described_class.tombstone(key)
13
- expect(result.payload_key).to eq(key)
14
- expect(result.to_h).to eq({ payload_key: { 'test_id' => 123 } })
15
- end
16
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # For testing the generated class.
4
- RSpec.describe Schemas::MyNamespace::MySchemaWithCircularReference do
5
- let(:payload_hash) do
6
- {
7
- properties: {
8
- a_boolean: {
9
- 'property' => true
10
- },
11
- an_integer: {
12
- 'property' => 1
13
- },
14
- a_float: {
15
- 'property' => 4.5
16
- },
17
- a_string: {
18
- 'property' => 'string'
19
- },
20
- an_array: {
21
- 'property' => [1, 2, 3]
22
- },
23
- an_hash: {
24
- 'property' => {
25
- 'a_key' => 'a_value'
26
- }
27
- }
28
- }
29
- }
30
- end
31
-
32
- describe 'class initialization' do
33
- it 'should initialize the class from keyword arguments' do
34
- klass = described_class.new(properties: payload_hash[:properties])
35
- expect(klass).to be_instance_of(described_class)
36
- end
37
-
38
- it 'should initialize the class from a hash with symbols as keys' do
39
- klass = described_class.new(**payload_hash)
40
- expect(klass).to be_instance_of(described_class)
41
- end
42
-
43
- it 'should initialize the class when missing attributes' do
44
- payload_hash.delete(:properties)
45
- klass = described_class.new(**payload_hash)
46
- expect(klass).to be_instance_of(described_class)
47
- end
48
-
49
- end
50
-
51
- describe 'base class methods' do
52
- let(:klass) do
53
- described_class.new(**payload_hash)
54
- end
55
-
56
- it 'should return the name of the schema and namespace' do
57
- expect(klass.schema).to eq('MySchemaWithCircularReference')
58
- expect(klass.namespace).to eq('com.my-namespace')
59
- expect(klass.full_schema).to eq('com.my-namespace.MySchemaWithCircularReference')
60
- end
61
-
62
- it 'should return a json version of the payload' do
63
- described_class.new(**payload_hash)
64
- payload_h = {
65
- 'properties' => {
66
- a_boolean: {
67
- 'property' =>true
68
- },
69
- an_integer: {
70
- 'property' =>1
71
- },
72
- a_float: {
73
- 'property' =>4.5
74
- },
75
- a_string: {
76
- 'property' =>'string'
77
- },
78
- an_array: {
79
- 'property' =>[1, 2, 3]
80
- },
81
- an_hash: {
82
- 'property' =>{
83
- 'a_key' => 'a_value'
84
- }
85
- }
86
- }
87
- }
88
-
89
- expect(klass.as_json).to eq(payload_h)
90
- end
91
-
92
- it 'should return a JSON string of the payload' do
93
- s = '{"properties":{"a_boolean":{"property":true},"an_integer":{"property":1},"a_float":{"property":4.5},"a_str' \
94
- 'ing":{"property":"string"},"an_array":{"property":[1,2,3]},"an_hash":{"property":{"a_key":"a_value"}}}}'
95
- expect(klass.to_json).to eq(s)
96
- end
97
- end
98
- end