avro-gen-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +34 -0
- data/.github/workflows/release.yml +31 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +100 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +61 -0
- data/Rakefile +11 -0
- data/avro-gen-ruby.gemspec +32 -0
- data/lib/avro_gen/avro_parser.rb +64 -0
- data/lib/avro_gen/configuration.rb +60 -0
- data/lib/avro_gen/errors.rb +6 -0
- data/lib/avro_gen/generator/templates/schema_class.rb.tt +8 -0
- data/lib/avro_gen/generator/templates/schema_enum.rb.tt +13 -0
- data/lib/avro_gen/generator/templates/schema_record.rb.tt +102 -0
- data/lib/avro_gen/generator.rb +375 -0
- data/lib/avro_gen/railtie.rb +12 -0
- data/lib/avro_gen/schema_class/base.rb +62 -0
- data/lib/avro_gen/schema_class/enum.rb +48 -0
- data/lib/avro_gen/schema_class/record.rb +108 -0
- data/lib/avro_gen/schema_class.rb +62 -0
- data/lib/avro_gen/schema_field.rb +26 -0
- data/lib/avro_gen/schema_validator.rb +80 -0
- data/lib/avro_gen/upgrader.rb +43 -0
- data/lib/avro_gen/version.rb +5 -0
- data/lib/avro_gen.rb +18 -0
- data/lib/tasks/avro.rake +18 -0
- data/regenerate_test_schema_classes.rb +32 -0
- data/spec/generator_spec.rb +92 -0
- data/spec/my_schema_spec.rb +18 -0
- data/spec/my_schema_with_circular_reference_spec.rb +97 -0
- data/spec/my_schema_with_complex_types_spec.rb +235 -0
- data/spec/schema_validator_spec.rb +42 -0
- data/spec/schemas/com/my-namespace/Generated.avsc +77 -0
- data/spec/schemas/com/my-namespace/MyNestedSchema.avsc +62 -0
- data/spec/schemas/com/my-namespace/MySchema.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaCompound_key.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaId_key.avsc +12 -0
- data/spec/schemas/com/my-namespace/MySchemaWithBooleans.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaWithCircularReference.avsc +39 -0
- data/spec/schemas/com/my-namespace/MySchemaWithComplexTypes.avsc +120 -0
- data/spec/schemas/com/my-namespace/MySchemaWithDateTimes.avsc +33 -0
- data/spec/schemas/com/my-namespace/MySchemaWithId.avsc +28 -0
- data/spec/schemas/com/my-namespace/MySchemaWithTitle.avsc +22 -0
- data/spec/schemas/com/my-namespace/MySchemaWithUnionType.avsc +91 -0
- data/spec/schemas/com/my-namespace/MySchemaWithUniqueId.avsc +32 -0
- data/spec/schemas/com/my-namespace/MySchema_key.avsc +13 -0
- data/spec/schemas/com/my-namespace/Wibble.avsc +43 -0
- data/spec/schemas/com/my-namespace/Widget.avsc +27 -0
- data/spec/schemas/com/my-namespace/WidgetTheSecond.avsc +27 -0
- data/spec/schemas/com/my-namespace/WidgetTheThird.avsc +27 -0
- data/spec/schemas/com/my-namespace/my-suborg/MyLongNamespaceSchema.avsc +18 -0
- data/spec/schemas/com/my-namespace/request/CreateTopic.avsc +11 -0
- data/spec/schemas/com/my-namespace/request/Index.avsc +11 -0
- data/spec/schemas/com/my-namespace/request/UpdateRequest.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/CreateTopic.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/Index.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/UpdateResponse.avsc +11 -0
- data/spec/schemas/my_namespace/generated.rb +164 -0
- data/spec/schemas/my_namespace/my_long_namespace_schema.rb +49 -0
- data/spec/schemas/my_namespace/my_nested_schema.rb +126 -0
- data/spec/schemas/my_namespace/my_schema.rb +62 -0
- data/spec/schemas/my_namespace/my_schema_compound_key.rb +42 -0
- data/spec/schemas/my_namespace/my_schema_id_key.rb +37 -0
- data/spec/schemas/my_namespace/my_schema_key.rb +37 -0
- data/spec/schemas/my_namespace/my_schema_with_boolean.rb +42 -0
- data/spec/schemas/my_namespace/my_schema_with_circular_reference.rb +84 -0
- data/spec/schemas/my_namespace/my_schema_with_complex_type.rb +241 -0
- data/spec/schemas/my_namespace/my_schema_with_date_time.rb +57 -0
- data/spec/schemas/my_namespace/my_schema_with_id.rb +52 -0
- data/spec/schemas/my_namespace/my_schema_with_title.rb +47 -0
- data/spec/schemas/my_namespace/my_schema_with_union_type.rb +205 -0
- data/spec/schemas/my_namespace/my_schema_with_unique_id.rb +57 -0
- data/spec/schemas/my_namespace/request/create_topic.rb +37 -0
- data/spec/schemas/my_namespace/request/index.rb +37 -0
- data/spec/schemas/my_namespace/request/update_request.rb +37 -0
- data/spec/schemas/my_namespace/response/create_topic.rb +37 -0
- data/spec/schemas/my_namespace/response/index.rb +37 -0
- data/spec/schemas/my_namespace/response/update_response.rb +37 -0
- data/spec/schemas/my_namespace/wibble.rb +77 -0
- data/spec/schemas/my_namespace/widget.rb +57 -0
- data/spec/schemas/my_namespace/widget_the_second.rb +57 -0
- data/spec/schemas/my_namespace/widget_the_third.rb +57 -0
- data/spec/snapshots/consumers-no-nest.snap +1740 -0
- data/spec/snapshots/consumers.snap +1720 -0
- data/spec/snapshots/my_nested_schema.snap +121 -0
- data/spec/snapshots/my_schema_with_boolean.snap +44 -0
- data/spec/snapshots/my_schema_with_circular_reference.snap +81 -0
- data/spec/snapshots/my_schema_with_complex_type.snap +236 -0
- data/spec/snapshots/my_schema_with_date_time.snap +59 -0
- data/spec/snapshots/my_schema_with_union_type.snap +207 -0
- data/spec/snapshots/namespace_folders.snap +1800 -0
- data/spec/snapshots/namespace_map.snap +1800 -0
- data/spec/spec_helper.rb +23 -0
- metadata +265 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'rails/generators'
|
|
5
|
+
require 'schema_registry_client'
|
|
6
|
+
require 'active_support/core_ext'
|
|
7
|
+
require_relative 'configuration'
|
|
8
|
+
require_relative 'avro_parser'
|
|
9
|
+
require_relative 'schema_field'
|
|
10
|
+
require_relative 'schema_class'
|
|
11
|
+
require_relative 'schema_validator'
|
|
12
|
+
|
|
13
|
+
module AvroGen
|
|
14
|
+
# Generator for Schema Classes used for the IDE and consumer/producer interfaces.
|
|
15
|
+
class Generator < Rails::Generators::Base
|
|
16
|
+
# @return [Array<Symbol>]
|
|
17
|
+
SPECIAL_TYPES = %i(record enum).freeze
|
|
18
|
+
# @return [String]
|
|
19
|
+
INITIALIZE_WHITESPACE = "\n#{' ' * 19}".freeze
|
|
20
|
+
# @return [Array<String>]
|
|
21
|
+
IGNORE_DEFAULTS = %w(message_id timestamp).freeze
|
|
22
|
+
# @return [String]
|
|
23
|
+
SCHEMA_CLASS_FILE = 'schema_class.rb'
|
|
24
|
+
# @return [String]
|
|
25
|
+
SCHEMA_RECORD_PATH = File.expand_path('generator/templates/schema_record.rb.tt', __dir__).freeze
|
|
26
|
+
# @return [String]
|
|
27
|
+
SCHEMA_ENUM_PATH = File.expand_path('generator/templates/schema_enum.rb.tt', __dir__).freeze
|
|
28
|
+
|
|
29
|
+
source_root File.expand_path('generator/templates', __dir__)
|
|
30
|
+
|
|
31
|
+
no_commands do
|
|
32
|
+
# Retrieve the fields from this Avro Schema
|
|
33
|
+
# @param schema [Avro::Schema::NamedSchema]
|
|
34
|
+
# @return [Array<SchemaField>]
|
|
35
|
+
def fields(schema)
|
|
36
|
+
schema.fields.map do |field|
|
|
37
|
+
AvroGen::SchemaField.new(field.name, field.type, [], field.default)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Converts AvroGen::SchemaField's to String form for generated YARD docs
|
|
42
|
+
# @param schema_field [AvroGen::SchemaField]
|
|
43
|
+
# @return [String] A string representation of the Type of this SchemaField
|
|
44
|
+
def deimos_field_type(schema_field)
|
|
45
|
+
_field_type(schema_field.type)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate a Schema Model Class and all of its Nested Records from a
|
|
49
|
+
# schema name and namespace.
|
|
50
|
+
# @param schema_name [String]
|
|
51
|
+
# @param namespace [String]
|
|
52
|
+
# @param key_config [Hash,nil]
|
|
53
|
+
# @return [void]
|
|
54
|
+
def generate_classes(schema_name, namespace, key_config=nil)
|
|
55
|
+
schema_base = _schema_loader(schema_name, namespace)
|
|
56
|
+
schema_base.load_schema
|
|
57
|
+
if key_config&.dig(:schema)
|
|
58
|
+
key_schema_base = _schema_loader(key_config[:schema], namespace)
|
|
59
|
+
key_schema_base.load_schema
|
|
60
|
+
generate_class_from_schema_base(key_schema_base, key_config: nil)
|
|
61
|
+
end
|
|
62
|
+
generate_class_from_schema_base(schema_base, key_config: key_config)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Generate classes for an explicit list of schema configs (each a Hash with
|
|
66
|
+
# :schema, :namespace and optional :key_config), then generate every other
|
|
67
|
+
# schema found in the configured path. This is the orchestration entry point
|
|
68
|
+
# used by Deimos (which derives configs from its Kafka topics).
|
|
69
|
+
# @param configs [Array<Hash>] [{ schema:, namespace:, key_config: }]
|
|
70
|
+
# @return [void]
|
|
71
|
+
def generate_from_configs(configs)
|
|
72
|
+
found_schemas = {}
|
|
73
|
+
configs.each do |config|
|
|
74
|
+
schema_name = config[:schema]
|
|
75
|
+
next if schema_name.nil?
|
|
76
|
+
|
|
77
|
+
namespace = config[:namespace]
|
|
78
|
+
key_config = config[:key_config] || {}
|
|
79
|
+
key_schema_name = key_config[:schema]
|
|
80
|
+
|
|
81
|
+
# don't regenerate if the schema was already found and had a payload key
|
|
82
|
+
next if found_schemas["#{namespace}.#{schema_name}"].present?
|
|
83
|
+
|
|
84
|
+
found_schemas["#{namespace}.#{schema_name}"] = key_schema_name
|
|
85
|
+
found_schemas["#{namespace}.#{key_schema_name}"] = nil
|
|
86
|
+
generate_classes(schema_name, namespace, key_config)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
generate_from_path(skip: found_schemas.keys)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generate classes for every schema found in the configured schema path.
|
|
93
|
+
# @param skip [Array<String>] full schema names ("namespace.name") to skip
|
|
94
|
+
# @return [void]
|
|
95
|
+
def generate_from_path(skip: [])
|
|
96
|
+
path = AvroGen.config.schema_path
|
|
97
|
+
schema_store = SchemaRegistry::AvroSchemaStore.new(path: path)
|
|
98
|
+
schema_store.load_schemas!
|
|
99
|
+
schema_store.schemas.values.sort_by { |s| "#{s.namespace}#{s.name}" }.each do |schema|
|
|
100
|
+
name = "#{schema.namespace}.#{schema.name}"
|
|
101
|
+
next if skip.include?(name)
|
|
102
|
+
|
|
103
|
+
generate_classes(schema.name, schema.namespace, nil)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @param schema [Avro::Schema::NamedSchema]
|
|
108
|
+
# @return [Array<Avro::Schema::NamedSchema]
|
|
109
|
+
def child_schemas(schema)
|
|
110
|
+
if schema.respond_to?(:fields)
|
|
111
|
+
schema.fields.map(&:type)
|
|
112
|
+
elsif schema.respond_to?(:values)
|
|
113
|
+
[schema.values]
|
|
114
|
+
elsif schema.respond_to?(:items)
|
|
115
|
+
[schema.items]
|
|
116
|
+
elsif schema.respond_to?(:schemas)
|
|
117
|
+
schema.schemas.reject { |s| s.instance_of?(Avro::Schema::PrimitiveSchema) }
|
|
118
|
+
else
|
|
119
|
+
[]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @param schemas [Array<Avro::Schema::NamedSchema>]
|
|
124
|
+
# @return [Array<Avro::Schema::NamedSchema>]
|
|
125
|
+
def collect_all_schemas(schemas)
|
|
126
|
+
schemas.dup.each do |schema|
|
|
127
|
+
next if @discovered_schemas.include?(schema)
|
|
128
|
+
|
|
129
|
+
@discovered_schemas << schema
|
|
130
|
+
schemas.concat(collect_all_schemas(child_schemas(schema)))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
schemas.select { |s| s.respond_to?(:name) }.uniq
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @param schema_base [AvroGen::SchemaValidator]
|
|
137
|
+
# @param key_config [Hash,nil]
|
|
138
|
+
# @return [void]
|
|
139
|
+
def generate_class_from_schema_base(schema_base, key_config: nil)
|
|
140
|
+
@discovered_schemas = Set.new
|
|
141
|
+
@sub_schema_templates = []
|
|
142
|
+
schemas = collect_all_schemas(schema_base.schema_store.schemas.values)
|
|
143
|
+
|
|
144
|
+
main_schema = schemas.find { |s| s.name == schema_base.schema }
|
|
145
|
+
sub_schemas = schemas.reject { |s| s.name == schema_base.schema }.sort_by(&:name)
|
|
146
|
+
if AvroGen.config.nest_child_schemas
|
|
147
|
+
@sub_schema_templates = sub_schemas.map do |schema|
|
|
148
|
+
_generate_class_template_from_schema(schema, nil)
|
|
149
|
+
end
|
|
150
|
+
write_file(main_schema, key_config)
|
|
151
|
+
else
|
|
152
|
+
write_file(main_schema, key_config)
|
|
153
|
+
sub_schemas.each do |schema|
|
|
154
|
+
write_file(schema, nil)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @param schema [Avro::Schema::NamedSchema]
|
|
160
|
+
# @param key_config [Hash,nil]
|
|
161
|
+
# @return [void]
|
|
162
|
+
def write_file(schema, key_config)
|
|
163
|
+
class_template = _generate_class_template_from_schema(schema, key_config)
|
|
164
|
+
@modules = AvroGen::SchemaClass.modules_for(schema.namespace)
|
|
165
|
+
@main_class_definition = class_template
|
|
166
|
+
|
|
167
|
+
file_prefix = schema.name.underscore.singularize
|
|
168
|
+
if AvroGen.config.use_full_namespace
|
|
169
|
+
# Use entire namespace for folders
|
|
170
|
+
# but don't add directories that are already in the path
|
|
171
|
+
directories = @modules.map(&:underscore).select do |m|
|
|
172
|
+
AvroGen.config.generated_class_path.exclude?(m)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
file_prefix = "#{directories.join('/')}/#{file_prefix}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
filename = "#{AvroGen.config.generated_class_path}/#{file_prefix}.rb"
|
|
179
|
+
template(SCHEMA_CLASS_FILE, filename, force: true)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Format a given field into its appropriate to_h representation.
|
|
183
|
+
# @param field[AvroGen::SchemaField]
|
|
184
|
+
# @return [String]
|
|
185
|
+
def field_as_json(field)
|
|
186
|
+
res = "'#{field.name}' => @#{field.name}"
|
|
187
|
+
field_base_type = _schema_base_class(field.type).type_sym
|
|
188
|
+
|
|
189
|
+
if %i(record enum).include?(field_base_type)
|
|
190
|
+
res += case field.type.type_sym
|
|
191
|
+
when :array
|
|
192
|
+
'.map { |v| v&.as_json }'
|
|
193
|
+
when :map
|
|
194
|
+
'.transform_values { |v| v&.as_json }'
|
|
195
|
+
else
|
|
196
|
+
'&.as_json'
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
res + (field.name == @fields.last.name ? '' : ',')
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
desc 'Generate schema classes from the configured schema path.'
|
|
205
|
+
# @return [void]
|
|
206
|
+
def generate
|
|
207
|
+
Rails.logger&.info("Generating schema classes to #{AvroGen.config.generated_class_path}")
|
|
208
|
+
generate_from_path
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
# @param schema_name [String]
|
|
214
|
+
# @param namespace [String]
|
|
215
|
+
# @return [AvroGen::SchemaValidator]
|
|
216
|
+
def _schema_loader(schema_name, namespace)
|
|
217
|
+
AvroGen::SchemaValidator.new(schema: schema_name, namespace: namespace)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @param schema[Avro::Schema::NamedSchema]
|
|
221
|
+
# @param key_config[Hash,nil]
|
|
222
|
+
# @return [String]
|
|
223
|
+
def _generate_class_template_from_schema(schema, key_config)
|
|
224
|
+
_set_instance_variables(schema, key_config)
|
|
225
|
+
|
|
226
|
+
temp = schema.is_a?(Avro::Schema::RecordSchema) ? _record_class_template : _enum_class_template
|
|
227
|
+
res = ERB.new(temp, trim_mode: '-')
|
|
228
|
+
res.result(binding)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# @param schema[Avro::Schema::NamedSchema]
|
|
232
|
+
# @param key_config [Hash,nil]
|
|
233
|
+
def _set_instance_variables(schema, key_config)
|
|
234
|
+
schema_is_record = schema.is_a?(Avro::Schema::RecordSchema)
|
|
235
|
+
@current_schema = schema
|
|
236
|
+
return unless schema_is_record
|
|
237
|
+
|
|
238
|
+
@fields = fields(schema)
|
|
239
|
+
key_schema = nil
|
|
240
|
+
if key_config&.dig(:schema)
|
|
241
|
+
key_schema_base = _schema_loader(key_config[:schema], schema.namespace)
|
|
242
|
+
key_schema_base.load_schema
|
|
243
|
+
key_schema = key_schema_base.schema_store.schemas.values.first
|
|
244
|
+
@fields << AvroGen::SchemaField.new('payload_key', key_schema, [], nil)
|
|
245
|
+
end
|
|
246
|
+
@initialization_definition = _initialization_definition
|
|
247
|
+
@field_assignments = _field_assignments
|
|
248
|
+
@tombstone_assignment = _tombstone_assignment(key_config, key_schema)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def _tombstone_assignment(key_config, key_schema)
|
|
252
|
+
return nil unless key_config
|
|
253
|
+
|
|
254
|
+
if key_config[:plain]
|
|
255
|
+
'record.tombstone_key = key'
|
|
256
|
+
elsif key_config[:field]
|
|
257
|
+
"record.tombstone_key = key\n record.#{key_config[:field]} = key"
|
|
258
|
+
elsif key_schema
|
|
259
|
+
field_base_type = _field_type(key_schema)
|
|
260
|
+
"record.tombstone_key = #{field_base_type}.initialize_from_value(key)\n record.payload_key = key"
|
|
261
|
+
else
|
|
262
|
+
''
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Defines the initialization method for Schema Records with one keyword argument per line
|
|
267
|
+
# @return [String] A string which defines the method signature for the initialize method
|
|
268
|
+
def _initialization_definition
|
|
269
|
+
arguments = @fields.map do |schema_field|
|
|
270
|
+
arg = "#{schema_field.name}:"
|
|
271
|
+
arg += _field_default(schema_field)
|
|
272
|
+
arg.strip
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
result = "def initialize(_from_message: false, #{arguments.first}"
|
|
276
|
+
arguments[1..].each_with_index do |arg, _i|
|
|
277
|
+
result += ",#{INITIALIZE_WHITESPACE}#{arg}"
|
|
278
|
+
end
|
|
279
|
+
"#{result})"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# @param field [SchemaField]
|
|
283
|
+
# @return [String]
|
|
284
|
+
def _field_default(field)
|
|
285
|
+
default = field.default
|
|
286
|
+
return ' nil' if default == :no_default || default.nil? || IGNORE_DEFAULTS.include?(field.name)
|
|
287
|
+
|
|
288
|
+
type_sym = field.type.type_sym
|
|
289
|
+
if type_sym == :union
|
|
290
|
+
type_sym = field.type.schemas.find { |s| s.type_sym != :null }&.type_sym
|
|
291
|
+
end
|
|
292
|
+
case type_sym
|
|
293
|
+
when :string, :enum
|
|
294
|
+
" \"#{default}\""
|
|
295
|
+
when :record
|
|
296
|
+
schema_name = AvroGen::AvroParser.schema_classname(field.type)
|
|
297
|
+
class_instance = AvroGen::SchemaClass.instance(field.default, schema_name)
|
|
298
|
+
" #{class_instance.to_h}"
|
|
299
|
+
else
|
|
300
|
+
" #{default}"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Overrides default attr accessor methods
|
|
305
|
+
# @return [Array<String>]
|
|
306
|
+
def _field_assignments
|
|
307
|
+
result = []
|
|
308
|
+
@fields.each do |field|
|
|
309
|
+
field_type = field.type.type_sym # Record, Union, Enum, Array or Map
|
|
310
|
+
schema_base_type = _schema_base_class(field.type)
|
|
311
|
+
field_base_type = _field_type(schema_base_type)
|
|
312
|
+
method_argument = %i(array map).include?(field_type) ? 'values' : 'value'
|
|
313
|
+
is_schema_class = %i(record enum).include?(schema_base_type.type_sym)
|
|
314
|
+
|
|
315
|
+
field_initialization = method_argument
|
|
316
|
+
|
|
317
|
+
if _is_complex_union?(field)
|
|
318
|
+
field_initialization = "initialize_#{field.name}_type(value, from_message: self._from_message)"
|
|
319
|
+
elsif is_schema_class
|
|
320
|
+
field_initialization = "#{field_base_type}.initialize_from_value(value, from_message: self._from_message)"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
result << {
|
|
324
|
+
field: field,
|
|
325
|
+
field_type: field_type,
|
|
326
|
+
is_schema_class: is_schema_class,
|
|
327
|
+
method_argument: method_argument,
|
|
328
|
+
deimos_type: deimos_field_type(field),
|
|
329
|
+
field_initialization: field_initialization,
|
|
330
|
+
is_complex_union: _is_complex_union?(field)
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
result
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Helper method to detect if a field is a complex union type with multiple record schemas
|
|
338
|
+
# @param field [AvroGen::SchemaField]
|
|
339
|
+
# @return [Boolean]
|
|
340
|
+
def _is_complex_union?(field)
|
|
341
|
+
return false unless field.type.type_sym == :union
|
|
342
|
+
|
|
343
|
+
non_null_schemas = field.type.schemas.reject { |s| s.type_sym == :null }
|
|
344
|
+
|
|
345
|
+
record_schemas = non_null_schemas.select { |s| s.type_sym == :record }
|
|
346
|
+
record_schemas.length > 1
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Converts Avro::Schema::NamedSchema's to String form for generated YARD docs.
|
|
350
|
+
# @param avro_schema [Avro::Schema::NamedSchema]
|
|
351
|
+
# @return [String] A string representation of the Type of this SchemaField
|
|
352
|
+
def _field_type(avro_schema)
|
|
353
|
+
AvroGen::AvroParser.field_type(avro_schema)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Returns the base class for this schema. Decodes Arrays, Maps and Unions
|
|
357
|
+
# @param avro_schema [Avro::Schema::NamedSchema]
|
|
358
|
+
# @return [Avro::Schema::NamedSchema]
|
|
359
|
+
def _schema_base_class(avro_schema)
|
|
360
|
+
AvroGen::AvroParser.schema_base_class(avro_schema)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# An ERB template for schema record classes
|
|
364
|
+
# @return [String]
|
|
365
|
+
def _record_class_template
|
|
366
|
+
File.read(SCHEMA_RECORD_PATH).strip
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# An ERB template for schema enum classes
|
|
370
|
+
# @return [String]
|
|
371
|
+
def _enum_class_template
|
|
372
|
+
File.read(SCHEMA_ENUM_PATH).strip
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module AvroGen
|
|
6
|
+
# Exposes the avro:* rake tasks to a host Rails application.
|
|
7
|
+
class Railtie < Rails::Railtie
|
|
8
|
+
rake_tasks do
|
|
9
|
+
load File.expand_path('../tasks/avro.rake', __dir__)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
|
|
6
|
+
module AvroGen
|
|
7
|
+
module SchemaClass
|
|
8
|
+
# Base Class for Schema Classes generated from Avro.
|
|
9
|
+
class Base
|
|
10
|
+
# @param _args [Array<Object>]
|
|
11
|
+
def initialize(*_args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Converts the object to a hash which can be used for debugging or comparing objects.
|
|
15
|
+
# @param _opts [Hash]
|
|
16
|
+
# @return [Hash] a hash representation of the payload
|
|
17
|
+
def as_json(_opts={})
|
|
18
|
+
raise MissingImplementationError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param key [String,Symbol]
|
|
22
|
+
# @param val [Object]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def []=(key, val)
|
|
25
|
+
self.send("#{key}=", val)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param other [SchemaClass::Base]
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def ==(other)
|
|
31
|
+
comparison = other
|
|
32
|
+
if other.class == self.class
|
|
33
|
+
comparison = other.as_json
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
comparison == self.as_json
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias_method :eql?, :==
|
|
40
|
+
|
|
41
|
+
# @return [String]
|
|
42
|
+
def inspect
|
|
43
|
+
klass = self.class
|
|
44
|
+
"#{klass}(#{self.as_json})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Initializes this class from a given value
|
|
48
|
+
# @param value [Object]
|
|
49
|
+
# @return [SchemaClass::Base]
|
|
50
|
+
def self.initialize_from_value(_value)
|
|
51
|
+
raise MissingImplementationError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
def hash
|
|
58
|
+
as_json.hash
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module AvroGen
|
|
7
|
+
module SchemaClass
|
|
8
|
+
# Base Class for Enum Classes generated from Avro.
|
|
9
|
+
class Enum < Base
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_accessor :value
|
|
12
|
+
|
|
13
|
+
# @param other [AvroGen::SchemaClass::Enum]
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def ==(other)
|
|
16
|
+
other.is_a?(self.class) ? other.value == @value : other == @value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [String]
|
|
20
|
+
def to_s
|
|
21
|
+
@value.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param value [String]
|
|
25
|
+
def initialize(value)
|
|
26
|
+
@value = value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns all the valid symbols for this enum.
|
|
30
|
+
# @return [Array<String>]
|
|
31
|
+
def symbols
|
|
32
|
+
raise MissingImplementationError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String]
|
|
36
|
+
def as_json(_opts={})
|
|
37
|
+
@value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [SchemaClass::Enum]
|
|
41
|
+
def self.initialize_from_value(value, from_message: false)
|
|
42
|
+
return nil if value.nil?
|
|
43
|
+
|
|
44
|
+
value.is_a?(self) ? value : self.new(value)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../schema_validator'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'active_support/core_ext/hash/keys'
|
|
7
|
+
require 'active_support/core_ext/object/try'
|
|
8
|
+
|
|
9
|
+
module AvroGen
|
|
10
|
+
module SchemaClass
|
|
11
|
+
# Base Class of Record Classes generated from Avro.
|
|
12
|
+
class Record < Base
|
|
13
|
+
attr_accessor :tombstone_key, :_from_message
|
|
14
|
+
|
|
15
|
+
# Converts the object attributes to a hash which can be used for Kafka
|
|
16
|
+
# @return [Hash] the payload as a hash.
|
|
17
|
+
def to_h
|
|
18
|
+
if self.tombstone_key
|
|
19
|
+
{ payload_key: self.tombstone_key&.as_json }
|
|
20
|
+
else
|
|
21
|
+
self.as_json
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Merge a hash or an identical schema object with this one and return a new object.
|
|
26
|
+
# @param other_hash [Hash,SchemaClass::Base]
|
|
27
|
+
# @return [SchemaClass::Base]
|
|
28
|
+
def merge(other_hash)
|
|
29
|
+
obj = self.class.new(**self.to_h.symbolize_keys)
|
|
30
|
+
other_hash.to_h.each do |k, v|
|
|
31
|
+
obj.send("#{k}=", v)
|
|
32
|
+
end
|
|
33
|
+
obj
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Element access method as if this Object were a hash
|
|
37
|
+
# @param key[String,Symbol]
|
|
38
|
+
# @return [Object] The value of the attribute if exists, nil otherwise
|
|
39
|
+
def [](key)
|
|
40
|
+
self.try(key.to_sym)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [SchemaClass::Record]
|
|
44
|
+
def with_indifferent_access
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the schema name of the inheriting class.
|
|
49
|
+
# @return [String]
|
|
50
|
+
def schema
|
|
51
|
+
raise MissingImplementationError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the namespace for the schema of the inheriting class.
|
|
55
|
+
# @return [String]
|
|
56
|
+
def namespace
|
|
57
|
+
raise MissingImplementationError
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the full schema name of the inheriting class.
|
|
61
|
+
# @return [String]
|
|
62
|
+
def full_schema
|
|
63
|
+
"#{namespace}.#{schema}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns a validator that can load the schema and its fields. Reuses a
|
|
67
|
+
# shared, path-scoped schema store so repeated lookups don't re-parse the
|
|
68
|
+
# .avsc files.
|
|
69
|
+
# @return [AvroGen::SchemaValidator]
|
|
70
|
+
def validator
|
|
71
|
+
@validator ||= AvroGen::SchemaValidator.new(
|
|
72
|
+
schema: schema,
|
|
73
|
+
namespace: namespace,
|
|
74
|
+
store: AvroGen::SchemaValidator.store_for(AvroGen.config.schema_path)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Array<String>] an array of fields names in the schema.
|
|
79
|
+
def schema_fields
|
|
80
|
+
validator.schema_fields.map(&:name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Used internally so that we don't crash on unknown fields that come from
|
|
84
|
+
# a backwards compatible schema.
|
|
85
|
+
# @param kwargs [Hash] the attributes to set on the new object.
|
|
86
|
+
# @return [SchemaClass::Record]
|
|
87
|
+
def self.new_from_message(**kwargs)
|
|
88
|
+
record = self.new
|
|
89
|
+
attrs = kwargs.select { |k, _v| record.respond_to?("#{k}=") }
|
|
90
|
+
self.new(_from_message: true, **attrs)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [SchemaClass::Record]
|
|
94
|
+
# @param from_message [Boolean] whether it's being initialized from a real Avro message.
|
|
95
|
+
def self.initialize_from_value(value, from_message: false)
|
|
96
|
+
return nil if value.nil?
|
|
97
|
+
|
|
98
|
+
return value if value.is_a?(self)
|
|
99
|
+
|
|
100
|
+
if from_message
|
|
101
|
+
self.new_from_message(**value&.symbolize_keys || {})
|
|
102
|
+
else
|
|
103
|
+
self.new(**value&.symbolize_keys || {})
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/string'
|
|
4
|
+
require 'active_support/core_ext/object/blank'
|
|
5
|
+
require 'active_support/core_ext/hash/keys'
|
|
6
|
+
|
|
7
|
+
module AvroGen
|
|
8
|
+
# Helpers used by the generator and by consumer/producer interfaces to
|
|
9
|
+
# locate and instantiate generated schema classes.
|
|
10
|
+
module SchemaClass
|
|
11
|
+
class << self
|
|
12
|
+
# @param namespace [String]
|
|
13
|
+
# @return [Array<String>]
|
|
14
|
+
def modules_for(namespace)
|
|
15
|
+
modules = [AvroGen.config.root_module]
|
|
16
|
+
namespace_override = nil
|
|
17
|
+
module_namespace = namespace
|
|
18
|
+
|
|
19
|
+
if AvroGen.config.use_full_namespace
|
|
20
|
+
if AvroGen.config.schema_namespace_map.present?
|
|
21
|
+
namespace_keys = AvroGen.config.schema_namespace_map.keys.sort_by { |k| -k.length }
|
|
22
|
+
namespace_override = namespace_keys.find { |k| module_namespace.include?(k) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if namespace_override.present?
|
|
26
|
+
# override default module
|
|
27
|
+
modules = Array(AvroGen.config.schema_namespace_map[namespace_override])
|
|
28
|
+
module_namespace = module_namespace.gsub(/#{namespace_override}\.?/, '')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
namespace_folders = module_namespace.split('.').map { |f| f.underscore.camelize }
|
|
32
|
+
modules.concat(namespace_folders) if namespace_folders.any?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
modules
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Converts a raw payload into an instance of the Schema Class
|
|
39
|
+
# @param payload [Hash, AvroGen::SchemaClass::Base]
|
|
40
|
+
# @param schema [String]
|
|
41
|
+
# @param namespace [String]
|
|
42
|
+
# @return [AvroGen::SchemaClass::Record]
|
|
43
|
+
def instance(payload, schema, namespace='')
|
|
44
|
+
return payload if payload.is_a?(AvroGen::SchemaClass::Base)
|
|
45
|
+
|
|
46
|
+
klass = klass(schema, namespace)
|
|
47
|
+
return payload if klass.nil? || payload.nil?
|
|
48
|
+
|
|
49
|
+
klass.new_from_message(**payload.symbolize_keys)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Determine and return the SchemaClass with the provided schema and namespace
|
|
53
|
+
# @param schema [String]
|
|
54
|
+
# @param namespace [String]
|
|
55
|
+
# @return [Class, nil]
|
|
56
|
+
def klass(schema, namespace)
|
|
57
|
+
constants = modules_for(namespace) + [schema.underscore.camelize.singularize]
|
|
58
|
+
constants.join('::').safe_constantize
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AvroGen
|
|
4
|
+
# Represents a field in the schema.
|
|
5
|
+
class SchemaField
|
|
6
|
+
# @return [String]
|
|
7
|
+
attr_accessor :name
|
|
8
|
+
# @return [Object]
|
|
9
|
+
attr_accessor :type
|
|
10
|
+
# @return [Array<String>]
|
|
11
|
+
attr_accessor :enum_values
|
|
12
|
+
# @return [Object]
|
|
13
|
+
attr_accessor :default
|
|
14
|
+
|
|
15
|
+
# @param name [String]
|
|
16
|
+
# @param type [Object]
|
|
17
|
+
# @param enum_values [Array<String>]
|
|
18
|
+
# @param default [Object]
|
|
19
|
+
def initialize(name, type, enum_values=[], default=:no_default)
|
|
20
|
+
@name = name
|
|
21
|
+
@type = type
|
|
22
|
+
@enum_values = enum_values
|
|
23
|
+
@default = default
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|