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,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'avro'
|
|
4
|
+
require 'schema_registry_client'
|
|
5
|
+
require_relative 'schema_field'
|
|
6
|
+
|
|
7
|
+
module AvroGen
|
|
8
|
+
# Loads an Avro schema from the configured schema path and exposes its fields.
|
|
9
|
+
# Used at runtime by generated Record classes (for #schema_fields) and as the
|
|
10
|
+
# schema loader for the generator.
|
|
11
|
+
class SchemaValidator
|
|
12
|
+
# @return [String]
|
|
13
|
+
attr_reader :schema
|
|
14
|
+
# @return [String]
|
|
15
|
+
attr_reader :namespace
|
|
16
|
+
|
|
17
|
+
@stores = {}
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# A schema store is an in-memory cache of parsed Avro schemas. Reuse one per
|
|
21
|
+
# path so repeated runtime lookups (e.g. Record#schema_fields across many
|
|
22
|
+
# records) don't re-read and re-parse the .avsc files each time.
|
|
23
|
+
# @param path [String]
|
|
24
|
+
# @return [SchemaRegistry::AvroSchemaStore]
|
|
25
|
+
def store_for(path)
|
|
26
|
+
(@stores ||= {})[path] ||= SchemaRegistry::AvroSchemaStore.new(path: path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Drop cached stores; call when the schemas on disk may have changed.
|
|
30
|
+
# @return [void]
|
|
31
|
+
def clear_store_cache!
|
|
32
|
+
@stores = {}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param schema [String]
|
|
37
|
+
# @param namespace [String]
|
|
38
|
+
# @param path [String] location of .avsc files; defaults to the configured schema_path
|
|
39
|
+
# @param store [SchemaRegistry::AvroSchemaStore] an existing store to reuse; when
|
|
40
|
+
# omitted a fresh, isolated store is created (the generator relies on this).
|
|
41
|
+
def initialize(schema:, namespace:, path: nil, store: nil)
|
|
42
|
+
@schema = schema
|
|
43
|
+
@namespace = namespace
|
|
44
|
+
@path = path || AvroGen.config.schema_path
|
|
45
|
+
@schema_store = store
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [SchemaRegistry::AvroSchemaStore]
|
|
49
|
+
def schema_store
|
|
50
|
+
@schema_store ||= SchemaRegistry::AvroSchemaStore.new(path: @path)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Forcefully loads the schema into memory.
|
|
54
|
+
# @return [Avro::Schema]
|
|
55
|
+
def load_schema
|
|
56
|
+
avro_schema
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Avro::Schema::NamedSchema]
|
|
60
|
+
def avro_schema(schema=nil)
|
|
61
|
+
schema ||= @schema
|
|
62
|
+
schema_store.find("#{@namespace}.#{schema}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Array<AvroGen::SchemaField>]
|
|
66
|
+
def schema_fields
|
|
67
|
+
avro_schema.fields.map do |field|
|
|
68
|
+
enum_values = field.type.type == 'enum' ? field.type.symbols : []
|
|
69
|
+
AvroGen::SchemaField.new(field.name, field.type, enum_values, field.default)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [void]
|
|
74
|
+
def validate(payload, schema: nil)
|
|
75
|
+
Avro::SchemaValidator.validate!(avro_schema(schema), payload,
|
|
76
|
+
recursive: true,
|
|
77
|
+
fail_on_extra_fields: true)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'configuration'
|
|
4
|
+
|
|
5
|
+
module AvroGen
|
|
6
|
+
# Rewrites references in checked-in generated schema-class files from the old
|
|
7
|
+
# Deimos constants to their AvroGen equivalents, so users can migrate off the
|
|
8
|
+
# deprecated `const_missing` fallback in one shot.
|
|
9
|
+
module Upgrader
|
|
10
|
+
REPLACEMENTS = {
|
|
11
|
+
'Deimos::SchemaClass::Record' => 'AvroGen::SchemaClass::Record',
|
|
12
|
+
'Deimos::SchemaClass::Enum' => 'AvroGen::SchemaClass::Enum',
|
|
13
|
+
'Deimos::SchemaClass::Base' => 'AvroGen::SchemaClass::Base',
|
|
14
|
+
'Deimos::Utils::SchemaClass' => 'AvroGen::SchemaClass'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# @param path [String] directory containing generated classes; defaults to config
|
|
19
|
+
# @return [Array<String>] the list of files that were rewritten
|
|
20
|
+
def run(path: nil)
|
|
21
|
+
path ||= AvroGen.config.generated_class_path
|
|
22
|
+
changed = []
|
|
23
|
+
Dir["#{path}/**/*.rb"].each do |file|
|
|
24
|
+
original = File.read(file)
|
|
25
|
+
updated = rewrite(original)
|
|
26
|
+
next if updated == original
|
|
27
|
+
|
|
28
|
+
File.write(file, updated)
|
|
29
|
+
changed << file
|
|
30
|
+
end
|
|
31
|
+
changed
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param contents [String]
|
|
35
|
+
# @return [String]
|
|
36
|
+
def rewrite(contents)
|
|
37
|
+
REPLACEMENTS.reduce(contents) do |text, (from, to)|
|
|
38
|
+
text.gsub(from, to)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/avro_gen.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'avro_gen/version'
|
|
4
|
+
require 'avro_gen/errors'
|
|
5
|
+
require 'avro_gen/configuration'
|
|
6
|
+
require 'avro_gen/schema_field'
|
|
7
|
+
require 'avro_gen/avro_parser'
|
|
8
|
+
require 'avro_gen/schema_validator'
|
|
9
|
+
require 'avro_gen/schema_class'
|
|
10
|
+
require 'avro_gen/schema_class/base'
|
|
11
|
+
require 'avro_gen/schema_class/enum'
|
|
12
|
+
require 'avro_gen/schema_class/record'
|
|
13
|
+
|
|
14
|
+
require 'avro_gen/railtie' if defined?(Rails::Railtie)
|
|
15
|
+
|
|
16
|
+
# Top-level namespace for the Avro schema-class generator.
|
|
17
|
+
module AvroGen
|
|
18
|
+
end
|
data/lib/tasks/avro.rake
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :avro do
|
|
4
|
+
desc 'Generate Ruby schema classes from Avro schemas'
|
|
5
|
+
task generate: :environment do
|
|
6
|
+
require 'avro_gen/generator'
|
|
7
|
+
Rails.logger&.info('Running avro:generate')
|
|
8
|
+
AvroGen::Generator.new.generate_from_path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc 'Rewrite generated schema classes to use AvroGen instead of Deimos constants'
|
|
12
|
+
task upgrade: :environment do
|
|
13
|
+
require 'avro_gen/upgrader'
|
|
14
|
+
changed = AvroGen::Upgrader.run
|
|
15
|
+
puts "Upgraded #{changed.size} file(s):"
|
|
16
|
+
changed.each { |f| puts " #{f}" }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Regenerates the committed runtime schema-class fixtures under spec/schemas/my_namespace.
|
|
5
|
+
# These are loaded by the behaviour specs (e.g. spec/my_schema_spec.rb).
|
|
6
|
+
require 'avro_gen'
|
|
7
|
+
require 'avro_gen/generator'
|
|
8
|
+
|
|
9
|
+
AvroGen.configure do |config|
|
|
10
|
+
config.schema_path = 'spec/schemas'
|
|
11
|
+
config.generated_class_path = 'spec/schemas'
|
|
12
|
+
config.nest_child_schemas = true
|
|
13
|
+
config.use_full_namespace = true
|
|
14
|
+
config.schema_namespace_map = {
|
|
15
|
+
'com' => 'Schemas',
|
|
16
|
+
'com.my-namespace.my-suborg' => %w(Schemas MyNamespace)
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Key configs mirror the Kafka topic configs Deimos used to generate these fixtures,
|
|
21
|
+
# so that keyed records get their tombstone/payload_key helpers.
|
|
22
|
+
ns = 'com.my-namespace'
|
|
23
|
+
configs = [
|
|
24
|
+
{ schema: 'Generated', namespace: ns, key_config: { field: :a_string } },
|
|
25
|
+
{ schema: 'MyNestedSchema', namespace: ns, key_config: { field: :test_id } },
|
|
26
|
+
{ schema: 'MySchema', namespace: ns, key_config: { schema: 'MySchema_key' } },
|
|
27
|
+
{ schema: 'MySchemaWithComplexTypes', namespace: ns, key_config: { field: :test_id } },
|
|
28
|
+
{ schema: 'MySchemaWithCircularReference', namespace: ns, key_config: { none: true } },
|
|
29
|
+
{ schema: 'MyLongNamespaceSchema', namespace: "#{ns}.my-suborg", key_config: { field: :test_id } }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
AvroGen::Generator.new.generate_from_configs(configs)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
# Serializes a hash of { filename => contents } into a stable snapshot string.
|
|
6
|
+
class MultiFileSerializer
|
|
7
|
+
def process_string(str)
|
|
8
|
+
# Ruby 3.4 changes how hashes are printed
|
|
9
|
+
if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('3.4.0')
|
|
10
|
+
str.gsub(/{"(.*)" => /, '{"\1"=>')
|
|
11
|
+
else
|
|
12
|
+
str
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def dump(value)
|
|
17
|
+
value.keys.sort.map { |k| "#{k}:\n#{process_string(value[k])}\n" }.join("\n")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
RSpec.describe AvroGen::Generator do
|
|
22
|
+
let(:schema_class_path) { 'spec/app/lib/schema_classes' }
|
|
23
|
+
let(:files) { Dir["#{schema_class_path}/**/*.rb"].to_h { |f| [f, File.read(f)] } }
|
|
24
|
+
|
|
25
|
+
# A schema with a field-based key, plus every other schema in the path
|
|
26
|
+
# generated as a plain class. This exercises records, enums, nested records,
|
|
27
|
+
# complex types (arrays/maps/unions) and circular references all at once.
|
|
28
|
+
let(:configs) do
|
|
29
|
+
[{ schema: 'Generated', namespace: 'com.my-namespace', key_config: { field: :a_string } }]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
before(:each) do
|
|
33
|
+
AvroGen.config.schema_path = 'spec/schemas'
|
|
34
|
+
AvroGen.config.generated_class_path = schema_class_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
after(:each) do
|
|
38
|
+
FileUtils.rm_rf('spec/app')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'generates the correct classes with child schemas nested' do
|
|
42
|
+
AvroGen.config.nest_child_schemas = true
|
|
43
|
+
described_class.new.generate_from_configs(configs)
|
|
44
|
+
expect(files).to match_snapshot('consumers', snapshot_serializer: MultiFileSerializer)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'generates the correct classes with child schemas in their own files' do
|
|
48
|
+
AvroGen.config.nest_child_schemas = false
|
|
49
|
+
described_class.new.generate_from_configs(configs)
|
|
50
|
+
expect(files).to match_snapshot('consumers-no-nest', snapshot_serializer: MultiFileSerializer)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'generates folders matching the full namespace' do
|
|
54
|
+
AvroGen.config.use_full_namespace = true
|
|
55
|
+
described_class.new.generate_from_configs(configs)
|
|
56
|
+
expect(files).to match_snapshot('namespace_folders', snapshot_serializer: MultiFileSerializer)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'generates modules according to the namespace map' do
|
|
60
|
+
AvroGen.config.use_full_namespace = true
|
|
61
|
+
AvroGen.config.schema_namespace_map = {
|
|
62
|
+
'com' => 'Schemas',
|
|
63
|
+
'com.my-namespace.my-suborg' => %w(Schemas MyNamespace)
|
|
64
|
+
}
|
|
65
|
+
described_class.new.generate_from_configs(configs)
|
|
66
|
+
expect(files).to match_snapshot('namespace_map', snapshot_serializer: MultiFileSerializer)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The snapshots above cover the full output, but these focused cases make it
|
|
70
|
+
# explicit that each notable Avro construct generates correctly (these mirror
|
|
71
|
+
# the per-schema scenarios that previously lived in Deimos).
|
|
72
|
+
describe 'specific Avro constructs' do
|
|
73
|
+
before(:each) do
|
|
74
|
+
AvroGen.config.nest_child_schemas = true
|
|
75
|
+
described_class.new.generate_from_path
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
'records with complex types (arrays, maps, nested records and enums)' => 'my_schema_with_complex_type',
|
|
80
|
+
'records with a circular reference' => 'my_schema_with_circular_reference',
|
|
81
|
+
'records with union types' => 'my_schema_with_union_type',
|
|
82
|
+
'records with nested child records' => 'my_nested_schema',
|
|
83
|
+
'records with date/time logical types' => 'my_schema_with_date_time',
|
|
84
|
+
'records with boolean fields' => 'my_schema_with_boolean'
|
|
85
|
+
}.each do |description, file|
|
|
86
|
+
it "generates #{description}" do
|
|
87
|
+
expect(files.slice("#{schema_class_path}/#{file}.rb")).
|
|
88
|
+
to match_snapshot(file, snapshot_serializer: MultiFileSerializer)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Schemas::MyNamespace::MySchema do
|
|
4
|
+
let(:key) { Schemas::MyNamespace::MySchemaKey.new(test_id: 123) }
|
|
5
|
+
|
|
6
|
+
it 'should produce a tombstone with a hash' do
|
|
7
|
+
result = described_class.tombstone({ test_id: 123 })
|
|
8
|
+
expect(result.payload_key).to eq(key)
|
|
9
|
+
expect(result.to_h).to eq({ payload_key: { 'test_id' => 123 } })
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'should work with a record' do
|
|
13
|
+
key = Schemas::MyNamespace::MySchemaKey.new(test_id: 123)
|
|
14
|
+
result = described_class.tombstone(key)
|
|
15
|
+
expect(result.payload_key).to eq(key)
|
|
16
|
+
expect(result.to_h).to eq({ payload_key: { 'test_id' => 123 } })
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
end
|
|
49
|
+
|
|
50
|
+
describe 'base class methods' do
|
|
51
|
+
let(:klass) do
|
|
52
|
+
described_class.new(**payload_hash)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'should return the name of the schema and namespace' do
|
|
56
|
+
expect(klass.schema).to eq('MySchemaWithCircularReference')
|
|
57
|
+
expect(klass.namespace).to eq('com.my-namespace')
|
|
58
|
+
expect(klass.full_schema).to eq('com.my-namespace.MySchemaWithCircularReference')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'should return a json version of the payload' do
|
|
62
|
+
described_class.new(**payload_hash)
|
|
63
|
+
payload_h = {
|
|
64
|
+
'properties' => {
|
|
65
|
+
a_boolean: {
|
|
66
|
+
'property' => true
|
|
67
|
+
},
|
|
68
|
+
an_integer: {
|
|
69
|
+
'property' => 1
|
|
70
|
+
},
|
|
71
|
+
a_float: {
|
|
72
|
+
'property' => 4.5
|
|
73
|
+
},
|
|
74
|
+
a_string: {
|
|
75
|
+
'property' => 'string'
|
|
76
|
+
},
|
|
77
|
+
an_array: {
|
|
78
|
+
'property' => [1, 2, 3]
|
|
79
|
+
},
|
|
80
|
+
an_hash: {
|
|
81
|
+
'property' => {
|
|
82
|
+
'a_key' => 'a_value'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(klass.as_json).to eq(payload_h)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'should return a JSON string of the payload' do
|
|
92
|
+
s = '{"properties":{"a_boolean":{"property":true},"an_integer":{"property":1},"a_float":{"property":4.5},"a_str' \
|
|
93
|
+
'ing":{"property":"string"},"an_array":{"property":[1,2,3]},"an_hash":{"property":{"a_key":"a_value"}}}}'
|
|
94
|
+
expect(klass.to_json).to eq(s)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# For testing the generated class.
|
|
4
|
+
RSpec.describe Schemas::MyNamespace::MySchemaWithComplexType 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: described_class::ARecord.new(a_record_field: 'field 1'),
|
|
13
|
+
some_optional_record: described_class::ARecord.new(a_record_field: 'field 2'),
|
|
14
|
+
some_record_array: [described_class::ARecord.new(a_record_field: 'field 3'),
|
|
15
|
+
described_class::ARecord.new(a_record_field: 'field 4')],
|
|
16
|
+
some_record_map: {
|
|
17
|
+
'record_1' => described_class::ARecord.new(a_record_field: 'field 5'),
|
|
18
|
+
'record_2' => described_class::ARecord.new(a_record_field: 'field 6')
|
|
19
|
+
},
|
|
20
|
+
some_enum_array: [described_class::AnEnum.new('sym1'),
|
|
21
|
+
described_class::AnEnum.new('sym2')]
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'class initialization' do
|
|
26
|
+
it 'should initialize the class from keyword arguments' do
|
|
27
|
+
klass = described_class.new(
|
|
28
|
+
test_id: payload_hash[:test_id],
|
|
29
|
+
test_float: payload_hash[:test_float],
|
|
30
|
+
test_string_array: payload_hash[:test_string_array],
|
|
31
|
+
some_record: payload_hash[:some_record],
|
|
32
|
+
some_optional_record: payload_hash[:some_optional_record],
|
|
33
|
+
some_record_array: payload_hash[:some_record_array],
|
|
34
|
+
some_record_map: payload_hash[:some_record_map],
|
|
35
|
+
some_enum_array: payload_hash[:some_enum_array]
|
|
36
|
+
)
|
|
37
|
+
expect(klass).to be_instance_of(described_class)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'should initialize the class from a hash with symbols as keys' do
|
|
41
|
+
klass = described_class.new(**payload_hash)
|
|
42
|
+
expect(klass).to be_instance_of(described_class)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'should initialize the class when missing attributes' do
|
|
46
|
+
payload_hash.delete(:test_id)
|
|
47
|
+
klass = described_class.new(**payload_hash)
|
|
48
|
+
expect(klass).to be_instance_of(described_class)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '.tombstone' do
|
|
53
|
+
it 'should return a tombstone' do
|
|
54
|
+
record = described_class.tombstone('foo')
|
|
55
|
+
expect(record.tombstone_key).to eq('foo')
|
|
56
|
+
expect(record.to_h).to eq({ payload_key: 'foo' })
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe 'base class methods' do
|
|
61
|
+
let(:klass) do
|
|
62
|
+
described_class.new(**payload_hash)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
let(:schema_fields) do
|
|
66
|
+
%w(test_id test_float test_int_array test_optional_int test_string_array some_integer_map
|
|
67
|
+
some_record some_optional_record some_record_array some_record_map some_enum_array
|
|
68
|
+
some_optional_enum some_enum_with_default union_string)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'should return the name of the schema and namespace' do
|
|
72
|
+
expect(klass.schema).to eq('MySchemaWithComplexTypes')
|
|
73
|
+
expect(klass.namespace).to eq('com.my-namespace')
|
|
74
|
+
expect(klass.full_schema).to eq('com.my-namespace.MySchemaWithComplexTypes')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'should return an array of all fields in the schema' do
|
|
78
|
+
expect(klass.schema_fields).to match_array(schema_fields)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'should return a json version of the payload' do
|
|
82
|
+
described_class.new(**payload_hash)
|
|
83
|
+
payload_h = {
|
|
84
|
+
'test_id' => 'test id',
|
|
85
|
+
'test_float' => 1.2,
|
|
86
|
+
'test_string_array' => %w(abc def),
|
|
87
|
+
'test_int_array' => [123, 456],
|
|
88
|
+
'some_optional_enum' => nil,
|
|
89
|
+
'test_optional_int' => 123,
|
|
90
|
+
'some_integer_map' => { 'int_1' => 1, 'int_2' => 2 },
|
|
91
|
+
'some_record' => { 'a_record_field' => 'field 1' },
|
|
92
|
+
'some_optional_record' => { 'a_record_field' => 'field 2' },
|
|
93
|
+
'some_record_array' => [
|
|
94
|
+
{ 'a_record_field' => 'field 3' },
|
|
95
|
+
{ 'a_record_field' => 'field 4' }
|
|
96
|
+
],
|
|
97
|
+
'some_record_map' => {
|
|
98
|
+
'record_1' => { 'a_record_field' => 'field 5' },
|
|
99
|
+
'record_2' => { 'a_record_field' => 'field 6' }
|
|
100
|
+
},
|
|
101
|
+
'some_enum_array' => %w(sym1 sym2),
|
|
102
|
+
'some_enum_with_default' => 'sym6',
|
|
103
|
+
'union_string' => ''
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
expect(klass.as_json).to eq(payload_h)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'should return a JSON string of the payload' do
|
|
110
|
+
s = '{"test_id":"test ' \
|
|
111
|
+
'id","union_string":"","test_float":1.2,"test_string_array":["abc","def"],"test_int_array":[123,456],"test_' \
|
|
112
|
+
'optional_i' \
|
|
113
|
+
'nt":123,"some_integer_map":{"int_1":1,"int_2":2},"some_record":{"a_record_field":"field ' \
|
|
114
|
+
'1"},"some_optional_record":{"a_record_field":"field 2"},"some_record_array":[{"a_record_field":"field ' \
|
|
115
|
+
'3"},{"a_record_field":"field 4"}],"some_record_map":{"record_1":{"a_record_field":"field ' \
|
|
116
|
+
'5"},"record_2":{"a_record_field":"field ' \
|
|
117
|
+
'6"}},"some_enum_array":["sym1","sym2"],"some_optional_enum":null,"some_enum_with_default":"sym6"}'
|
|
118
|
+
expect(klass.to_json).to eq(s)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe 'defaults' do
|
|
123
|
+
it 'should set an_optional_int if it is not provided' do
|
|
124
|
+
payload_hash.delete(:an_optional_int)
|
|
125
|
+
klass = described_class.new(**payload_hash)
|
|
126
|
+
expect(klass.test_optional_int).to eq(123)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'should set some_record if it is not provided' do
|
|
130
|
+
payload_hash.delete(:some_record)
|
|
131
|
+
klass = described_class.new(**payload_hash)
|
|
132
|
+
expect(klass.some_record).
|
|
133
|
+
to eq(described_class::ARecord.new(a_record_field: 'Test String'))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'should set some_record to nil' do
|
|
137
|
+
klass = described_class.new(**payload_hash, some_record: nil)
|
|
138
|
+
expect(klass.some_record).to be_nil
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe 'getters and setters' do
|
|
143
|
+
let(:klass) do
|
|
144
|
+
described_class.new(**payload_hash)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
context 'when getting attributes' do
|
|
148
|
+
it 'should get of values of primitive types' do
|
|
149
|
+
expect(klass.test_id).to eq('test id')
|
|
150
|
+
expect(klass.test_float).to eq(1.2)
|
|
151
|
+
expect(klass.test_string_array).to eq(%w(abc def))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'should get the value of some_record_array' do
|
|
155
|
+
some_record_array = klass.some_record_array
|
|
156
|
+
expect(some_record_array.first).to be_instance_of(described_class::ARecord)
|
|
157
|
+
expect(some_record_array.first.a_record_field).to eq('field 3')
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'should get the value of some_record_map' do
|
|
161
|
+
some_record_map = klass.some_record_map
|
|
162
|
+
expect(some_record_map['record_1']).
|
|
163
|
+
to be_instance_of(described_class::ARecord)
|
|
164
|
+
expect(some_record_map['record_1'].a_record_field).to eq('field 5')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'should get the value of some_enum_array' do
|
|
168
|
+
some_enum_array = klass.some_enum_array
|
|
169
|
+
expect(some_enum_array.first).to be_instance_of(described_class::AnEnum)
|
|
170
|
+
expect(some_enum_array.first.value).to eq('sym1')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'should get the value of some_record' do
|
|
174
|
+
record = klass.some_record
|
|
175
|
+
expect(record).to be_instance_of(described_class::ARecord)
|
|
176
|
+
expect(record.a_record_field).to eq('field 1')
|
|
177
|
+
expect(record.to_h).to eq({ 'a_record_field' => 'field 1' })
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'should support Hash-style element access of values' do
|
|
181
|
+
expect(klass['test_id']).to eq('test id')
|
|
182
|
+
expect(klass['test_float']).to eq(1.2)
|
|
183
|
+
expect(klass['test_string_array']).to eq(%w(abc def))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
context 'when setting attributes' do
|
|
188
|
+
it 'should modify the value of test_id' do
|
|
189
|
+
expect(klass.test_id).to eq('test id')
|
|
190
|
+
|
|
191
|
+
klass.test_id = 'something different'
|
|
192
|
+
expect(klass.test_id).to eq('something different')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'should modify the value of some_optional_record' do
|
|
196
|
+
expect(klass.some_optional_record).
|
|
197
|
+
to eq(described_class::ARecord.new(a_record_field: 'field 2'))
|
|
198
|
+
klass.some_optional_record = described_class::ARecord.
|
|
199
|
+
new(a_record_field: 'new field')
|
|
200
|
+
|
|
201
|
+
expect(klass.some_optional_record).to eq(described_class::ARecord.
|
|
202
|
+
new(a_record_field: 'new field'))
|
|
203
|
+
expect(klass.some_optional_record.as_json).to eq({ 'a_record_field' => 'new field' })
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'should accept a hash object inner records' do
|
|
207
|
+
klass.some_optional_record = { a_record_field: 'new field' }
|
|
208
|
+
expect(klass.some_optional_record).
|
|
209
|
+
to eq(described_class::ARecord.new(a_record_field: 'new field'))
|
|
210
|
+
expect(klass.some_optional_record.as_json).to eq({ 'a_record_field' => 'new field' })
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'should modify the value of some_enum_array' do
|
|
214
|
+
klass.some_enum_array.first.value = 'new_sym'
|
|
215
|
+
expect(klass.some_enum_array.first).
|
|
216
|
+
to eq(described_class::AnEnum.new('new_sym'))
|
|
217
|
+
|
|
218
|
+
klass.some_enum_array.second.value = described_class::AnEnum.
|
|
219
|
+
new('other_sym')
|
|
220
|
+
expect(klass.some_enum_array.second.value).to eq('other_sym')
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'should modify the value of some_record_map' do
|
|
224
|
+
klass.some_record_map['record_1'].a_record_field = 'new field'
|
|
225
|
+
expect(klass.some_record_map['record_1']).to eq(described_class::ARecord.
|
|
226
|
+
new(a_record_field: 'new field'))
|
|
227
|
+
|
|
228
|
+
klass.some_record_map['record_2'] = described_class::ARecord.
|
|
229
|
+
new(a_record_field: 'other field')
|
|
230
|
+
expect(klass.some_record_map['record_2']).to eq(described_class::ARecord.
|
|
231
|
+
new(a_record_field: 'other field'))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|