avromatic 1.0.0 → 2.0.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 +4 -4
- data/.rubocop.yml +4 -1
- data/Appraisals +2 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -0
- data/README.md +57 -25
- data/Rakefile +2 -0
- data/avromatic.gemspec +7 -3
- data/lib/avromatic/io/datum_reader.rb +2 -0
- data/lib/avromatic/io/datum_writer.rb +7 -1
- data/lib/avromatic/io.rb +4 -2
- data/lib/avromatic/messaging.rb +2 -0
- data/lib/avromatic/model/attributes.rb +112 -158
- data/lib/avromatic/model/builder.rb +4 -7
- data/lib/avromatic/model/coercion_error.rb +8 -0
- data/lib/avromatic/model/configurable.rb +2 -0
- data/lib/avromatic/model/configuration.rb +2 -0
- data/lib/avromatic/model/{custom_type.rb → custom_type_configuration.rb} +2 -2
- data/lib/avromatic/model/{type_registry.rb → custom_type_registry.rb} +15 -18
- data/lib/avromatic/model/field_helper.rb +20 -0
- data/lib/avromatic/model/message_decoder.rb +2 -0
- data/lib/avromatic/model/messaging_serialization.rb +2 -0
- data/lib/avromatic/model/nested_models.rb +2 -17
- data/lib/avromatic/model/raw_serialization.rb +21 -84
- data/lib/avromatic/model/types/abstract_timestamp_type.rb +57 -0
- data/lib/avromatic/model/types/abstract_type.rb +37 -0
- data/lib/avromatic/model/types/array_type.rb +53 -0
- data/lib/avromatic/model/types/boolean_type.rb +39 -0
- data/lib/avromatic/model/types/custom_type.rb +64 -0
- data/lib/avromatic/model/types/date_type.rb +41 -0
- data/lib/avromatic/model/types/enum_type.rb +56 -0
- data/lib/avromatic/model/types/fixed_type.rb +45 -0
- data/lib/avromatic/model/types/float_type.rb +48 -0
- data/lib/avromatic/model/types/integer_type.rb +39 -0
- data/lib/avromatic/model/types/map_type.rb +74 -0
- data/lib/avromatic/model/types/null_type.rb +39 -0
- data/lib/avromatic/model/types/record_type.rb +57 -0
- data/lib/avromatic/model/types/string_type.rb +48 -0
- data/lib/avromatic/model/types/timestamp_micros_type.rb +32 -0
- data/lib/avromatic/model/types/timestamp_millis_type.rb +32 -0
- data/lib/avromatic/model/types/type_factory.rb +118 -0
- data/lib/avromatic/model/types/union_type.rb +87 -0
- data/lib/avromatic/model/unknown_attribute_error.rb +15 -0
- data/lib/avromatic/model/validation.rb +57 -36
- data/lib/avromatic/model/validation_error.rb +8 -0
- data/lib/avromatic/model/value_object.rb +2 -0
- data/lib/avromatic/model.rb +6 -2
- data/lib/avromatic/model_registry.rb +2 -0
- data/lib/avromatic/patches/schema_validator_patch.rb +2 -0
- data/lib/avromatic/patches.rb +2 -0
- data/lib/avromatic/railtie.rb +2 -0
- data/lib/avromatic/rspec.rb +2 -0
- data/lib/avromatic/version.rb +3 -1
- data/lib/avromatic.rb +9 -4
- metadata +32 -21
- data/lib/avromatic/model/allowed_type_validator.rb +0 -7
- data/lib/avromatic/model/allowed_writer_methods_memoization.rb +0 -16
- data/lib/avromatic/model/attribute/abstract_timestamp.rb +0 -26
- data/lib/avromatic/model/attribute/record.rb +0 -26
- data/lib/avromatic/model/attribute/timestamp_micros.rb +0 -26
- data/lib/avromatic/model/attribute/timestamp_millis.rb +0 -26
- data/lib/avromatic/model/attribute/union.rb +0 -66
- data/lib/avromatic/model/attribute_type/union.rb +0 -29
- data/lib/avromatic/model/logical_types.rb +0 -19
- data/lib/avromatic/model/null_custom_type.rb +0 -21
- data/lib/avromatic/model/passthrough_serializer.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25174d8c67bde4e8a9d7a53dc357ed72db2e1faa08bbf8945d1f93dd69c46942
|
4
|
+
data.tar.gz: b0b2fce3ddb92a463fade05c2cb29cdadd60037357432c99ece5f892d3b54f45
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6566f793b850761a3d7dc2fa88b403c20ab8402e57d062322843038b473c7daf98b5e2484107063dbc2e9014919fabe1b5ac2e140c8e78f81a552356cbf8c5a2
|
7
|
+
data.tar.gz: 25071f9751683a41bb23fc7932e8570b663c87e903a4ad5d953da863e92c47f0e464355278ec712af41490b7abdc6ac6f11f004472ef3b802e274c8ddf83f503
|
data/.rubocop.yml
CHANGED
@@ -2,10 +2,13 @@ inherit_gem:
|
|
2
2
|
salsify_rubocop: conf/rubocop.yml
|
3
3
|
|
4
4
|
AllCops:
|
5
|
-
TargetRubyVersion: 2.
|
5
|
+
TargetRubyVersion: 2.3
|
6
6
|
|
7
7
|
Style/MultilineBlockChain:
|
8
8
|
Enabled: false
|
9
9
|
|
10
10
|
Style/NumericPredicate:
|
11
11
|
Enabled: false
|
12
|
+
|
13
|
+
Style/FrozenStringLiteralComment:
|
14
|
+
Enabled: true
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# avromatic changelog
|
2
2
|
|
3
|
+
## v2.0.0 (unreleased)
|
4
|
+
- Remove [virtus](https://github.com/solnic/virtus) dependency resulting in a 3x performance improvement in model instantation and 1.4x - 2.0x performance improvement in Avro serialization and Avromatic code simplification.
|
5
|
+
- Raise `Avromatic::Model::CoercionError` when attribute values can't be coerced to the target type in model constructors and attribute setters. Previously coercion errors weren't detected until Avro serialization or an explicit call to `valid?`.
|
6
|
+
- Prevent model instances from being constructed with unknown attributes. Previously unknown attributes were ignored.
|
7
|
+
This can be disabled by setting `Avromatic.allow_unknown_attributes` to `true`.
|
8
|
+
WARNING: Setting `Avromatic.allow_unknown_attributes` to `true` will result in incorrect union member coercions
|
9
|
+
if an earlier union member is satisfied by a subset of the latter union member's attributes.
|
10
|
+
- Validate required attributes are present when serializing to Avro for better error messages. Explicit
|
11
|
+
validation can still be done by calling the `valid?` or `invalid?` methods from the
|
12
|
+
[ActiveModel::Validations](https://edgeapi.rubyonrails.org/classes/ActiveModel/Validations.html) interface
|
13
|
+
but errors will now appear under the `:base` key. Previously these errors were detected late in the Avro serialization process resulting in hard to understand error messages.
|
14
|
+
- Support for custom types in unions with more than one non-null type.
|
15
|
+
- Drop support for Ruby < 2.3 and Rails < 5.0.
|
16
|
+
- Call `super()` in model constructor making it easier to define class/module hierarchies for models.
|
17
|
+
|
3
18
|
## v1.0.0
|
4
19
|
- No changes.
|
5
20
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -8,6 +8,9 @@
|
|
8
8
|
`Avromatic` generates Ruby models from [Avro](http://avro.apache.org/) schemas
|
9
9
|
and provides utilities to encode and decode them.
|
10
10
|
|
11
|
+
**This README reflects unreleased changes in Avromatic 2.0. Please see the
|
12
|
+
[1-0-stable](https://github.com/salsify/avromatic/blob/1-0-stable/README.md) branch for the latest stable release.**
|
13
|
+
|
11
14
|
## Installation
|
12
15
|
|
13
16
|
Add this line to your application's Gemfile:
|
@@ -49,6 +52,10 @@ Avromatic with unreleased Avro features.
|
|
49
52
|
`Avromatic.configure` and during code reloading in Rails applications. This
|
50
53
|
option is useful for defining models that will be extended when the load order
|
51
54
|
is important.
|
55
|
+
* **allow_unknown_attributes**: Optionally allow model constructors to silently
|
56
|
+
ignore unknown attributes. Defaults to `false`. WARNING: Setting this to `true`
|
57
|
+
will result in incorrect union member coercions if an earlier union member is
|
58
|
+
satisfied by a subset of the latter union member's attributes.
|
52
59
|
|
53
60
|
#### Custom Types
|
54
61
|
|
@@ -116,6 +123,26 @@ The Avro schema can be specified by name and loaded using the schema store:
|
|
116
123
|
class MyModel
|
117
124
|
include Avromatic::Model.build(schema_name :my_model)
|
118
125
|
end
|
126
|
+
|
127
|
+
# Construct instances by passing in a hash of attributes
|
128
|
+
instance = MyModel.new(id: 123, name: 'Tesla Model 3', enabled: true)
|
129
|
+
|
130
|
+
# Access attribute values with readers
|
131
|
+
instance.name # => "Tesla Model 3"
|
132
|
+
|
133
|
+
# Models are immutable by default
|
134
|
+
instance.name = 'Tesla Model X' # => NoMethodError (private method `name=' called for #<MyModel:0x00007ff711e64e60>)
|
135
|
+
|
136
|
+
# Booleans can also be accessed by '?' readers that coerce nil to false
|
137
|
+
instance.enabled? # => true
|
138
|
+
|
139
|
+
# Models implement ===, eql? and hash
|
140
|
+
instance == MyModel.new(id: 123, name: 'Tesla Model 3', enabled: true) # => true
|
141
|
+
instance.eql?(MyModel.new(id: 123, name: 'Tesla Model 3', enabled: true)) # => true
|
142
|
+
instance.hash # => -1279155042741869898
|
143
|
+
|
144
|
+
# Retrieve a hash of the model's attributes via to_h, to_hash or attributes
|
145
|
+
instance .to_h # => {:id=>123, :name=>"Tesla Model 3", :enabled=>true}
|
119
146
|
```
|
120
147
|
|
121
148
|
Or an `Avro::Schema` object can be specified directly:
|
@@ -126,7 +153,7 @@ class MyModel
|
|
126
153
|
end
|
127
154
|
```
|
128
155
|
|
129
|
-
Models are generated as
|
156
|
+
Models are generated as immutable value
|
130
157
|
objects by default, but can optionally be defined as mutable:
|
131
158
|
|
132
159
|
```ruby
|
@@ -135,9 +162,8 @@ class MyModel
|
|
135
162
|
end
|
136
163
|
```
|
137
164
|
|
138
|
-
|
139
|
-
including any default values defined in the schema.
|
140
|
-
are used to define validations on certain types of fields ([see below](#validations)).
|
165
|
+
Generated models include attributes for each field in the Avro schema
|
166
|
+
including any default values defined in the schema.
|
141
167
|
|
142
168
|
A model may be defined with both a key and a value schema:
|
143
169
|
|
@@ -176,7 +202,7 @@ MyModel = Avromatic::Model.model(schema_name :my_model)
|
|
176
202
|
#### Experimental: Union Support
|
177
203
|
|
178
204
|
Avromatic contains experimental support for unions containing more than one
|
179
|
-
non-null member type. This feature is experimental because
|
205
|
+
non-null member type. This feature is experimental because Avromatic
|
180
206
|
may attempt to coerce between types too aggressively.
|
181
207
|
|
182
208
|
For now, if a union contains [nested models](#nested-models) then it is
|
@@ -186,7 +212,7 @@ Some combination of the ordering of member types in the union and relying on
|
|
186
212
|
model validation may be required so that the correct member is selected,
|
187
213
|
especially when deserializing from Avro.
|
188
214
|
|
189
|
-
In the future, the type coercion used in the gem will be
|
215
|
+
In the future, the type coercion used in the gem will be enhanced to better
|
190
216
|
support the union use case.
|
191
217
|
|
192
218
|
#### Nested Models
|
@@ -251,9 +277,6 @@ These customizations are registered on the `Avromatic` module. Once a custom typ
|
|
251
277
|
is registered, it is used for all models with a schema that references that type.
|
252
278
|
It is recommended to register types within a block passed to `Avromatic.configure`:
|
253
279
|
|
254
|
-
Note: custom types are not currently supported on members of unions with more
|
255
|
-
than one non-null type.
|
256
|
-
|
257
280
|
```ruby
|
258
281
|
Avromatic.configure do |config|
|
259
282
|
config.register_type('com.example.my_string', MyString)
|
@@ -381,16 +404,31 @@ decoder.decode(model2_message_value)
|
|
381
404
|
# => instance of MyModel2
|
382
405
|
```
|
383
406
|
|
384
|
-
### Validations
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
407
|
+
### Validations and Coercions
|
408
|
+
|
409
|
+
An exception will be thrown if an attribute value cannot be coerced to the corresponding Avro schema field's type.
|
410
|
+
The following coercions are supported:
|
411
|
+
|
412
|
+
| Ruby Type | Avro Type |
|
413
|
+
| --------- | --------- |
|
414
|
+
| String, Symbol | string |
|
415
|
+
| Array | array |
|
416
|
+
| Hash | map |
|
417
|
+
| Integer, Float | int |
|
418
|
+
| Integer | long |
|
419
|
+
| Float | float |
|
420
|
+
| Float | double |
|
421
|
+
| String | bytes |
|
422
|
+
| Date, Time, DateTime | date |
|
423
|
+
| Time, DateTime | timestamp-millis |
|
424
|
+
| Time, DateTime | timestamp-micros |
|
425
|
+
| TrueClass, FalseClass | boolean |
|
426
|
+
| NilClass | null |
|
427
|
+
| Hash | record |
|
428
|
+
|
429
|
+
Validation of required fields is done automatically when serializing a model to Avro. It can also be done
|
430
|
+
explicitly by calling the `valid?` or `invalid?` methods from the
|
431
|
+
[ActiveModel::Validations](https://edgeapi.rubyonrails.org/classes/ActiveModel/Validations.html) interface.
|
394
432
|
|
395
433
|
### Logical Types
|
396
434
|
|
@@ -418,12 +456,6 @@ Requiring this file configures a RSpec before hook that directs any schema
|
|
418
456
|
registry requests to a fake, in-memory schema registry and rebuilds the
|
419
457
|
`Avromatic::Messaging` object for each example.
|
420
458
|
|
421
|
-
### Unsupported/Future
|
422
|
-
|
423
|
-
The following types/features are not supported for generated models:
|
424
|
-
|
425
|
-
- Custom types for members within a union.
|
426
|
-
|
427
459
|
## Development
|
428
460
|
|
429
461
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
data/avromatic.gemspec
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
lib = File.expand_path('../lib', __FILE__)
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
5
|
require 'avromatic/version'
|
@@ -18,12 +20,14 @@ Gem::Specification.new do |spec|
|
|
18
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
21
|
spec.require_paths = ['lib']
|
20
22
|
|
21
|
-
spec.
|
22
|
-
|
23
|
+
spec.required_ruby_version = '>= 2.3'
|
24
|
+
|
25
|
+
spec.add_runtime_dependency 'activemodel', '>= 5.0', '< 5.3'
|
26
|
+
spec.add_runtime_dependency 'activesupport', '>= 5.0', '< 5.3'
|
23
27
|
spec.add_runtime_dependency 'avro', '>= 1.7.7'
|
24
28
|
spec.add_runtime_dependency 'avro_schema_registry-client', '>= 0.3.0'
|
25
29
|
spec.add_runtime_dependency 'avro_turf'
|
26
|
-
spec.add_runtime_dependency '
|
30
|
+
spec.add_runtime_dependency 'ice_nine'
|
27
31
|
|
28
32
|
spec.add_development_dependency 'avro-builder', '>= 0.12.0'
|
29
33
|
spec.add_development_dependency 'bundler', '~> 1.11'
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Avromatic
|
2
4
|
module IO
|
3
5
|
# Subclass DatumWriter to use additional information about union member
|
@@ -9,6 +11,9 @@ module Avromatic
|
|
9
11
|
index_of_schema = datum[Avromatic::IO::UNION_MEMBER_INDEX]
|
10
12
|
# Avromatic does not treat the null of an optional field as part of the union
|
11
13
|
index_of_schema += 1 if optional
|
14
|
+
elsif optional && writers_schema.schemas.size == 2
|
15
|
+
# Optimize for the common case of a union that's just an optional field
|
16
|
+
index_of_schema = datum.nil? ? 0 : 1
|
12
17
|
else
|
13
18
|
index_of_schema = writers_schema.schemas.find_index do |schema|
|
14
19
|
Avro::Schema.validate(schema, datum)
|
@@ -23,7 +28,8 @@ module Avromatic
|
|
23
28
|
|
24
29
|
def write_record(writers_schema, datum, encoder)
|
25
30
|
if datum.is_a?(Hash) && datum.key?(Avromatic::IO::ENCODING_PROVIDER)
|
26
|
-
|
31
|
+
# This is only used for recursive serialization so validation has already been done
|
32
|
+
encoder.write(datum[Avromatic::IO::ENCODING_PROVIDER].avro_raw_value(validate: false))
|
27
33
|
else
|
28
34
|
super
|
29
35
|
end
|
data/lib/avromatic/io.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Avromatic
|
2
4
|
module IO
|
3
|
-
UNION_MEMBER_INDEX = '__avromatic_member_index'
|
4
|
-
ENCODING_PROVIDER = '__avromatic_encoding_provider'
|
5
|
+
UNION_MEMBER_INDEX = '__avromatic_member_index'
|
6
|
+
ENCODING_PROVIDER = '__avromatic_encoding_provider'
|
5
7
|
end
|
6
8
|
end
|
7
9
|
|
data/lib/avromatic/messaging.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext/object/duplicable'
|
2
4
|
require 'active_support/time'
|
3
|
-
require 'ice_nine/core_ext/object'
|
4
|
-
require 'avromatic/model/allowed_type_validator'
|
5
5
|
|
6
6
|
module Avromatic
|
7
7
|
module Model
|
@@ -20,14 +20,94 @@ module Avromatic
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
class AttributeDefinition
|
24
|
+
attr_reader :name, :type, :field, :default, :owner
|
25
|
+
delegate :serialize, to: :type
|
26
|
+
|
27
|
+
def initialize(owner:, field:, type:)
|
28
|
+
@owner = owner
|
29
|
+
@field = field
|
30
|
+
@type = type
|
31
|
+
@name = field.name.to_sym
|
32
|
+
@default = if field.default == :no_default
|
33
|
+
nil
|
34
|
+
elsif field.default.duplicable?
|
35
|
+
field.default.dup.deep_freeze
|
36
|
+
else
|
37
|
+
field.default
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def required?
|
42
|
+
FieldHelper.required?(field)
|
43
|
+
end
|
44
|
+
|
45
|
+
def coerce(input)
|
46
|
+
type.coerce(input)
|
47
|
+
rescue Avromatic::Model::UnknownAttributeError => e
|
48
|
+
raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
|
49
|
+
"because the following unexpected attributes were provided: #{e.unknown_attributes.join(', ')}. " \
|
50
|
+
"Only the following attributes are allowed: #{e.allowed_attributes.join(', ')}. " \
|
51
|
+
"Provided argument: #{input.inspect}")
|
52
|
+
rescue StandardError
|
53
|
+
if type.input_classes && type.input_classes.none? { |input_class| input.is_a?(input_class) }
|
54
|
+
raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
|
55
|
+
"because a #{input.class.name} was provided but expected a #{type.input_classes.map(&:name).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')}. " \
|
56
|
+
"Provided argument: #{input.inspect}")
|
57
|
+
elsif input.is_a?(Hash) && type.is_a?(Avromatic::Model::Types::UnionType)
|
58
|
+
raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
|
59
|
+
"because no union member type matches the provided attributes: #{input.inspect}")
|
60
|
+
else
|
61
|
+
raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name}. " \
|
62
|
+
"Provided argument: #{input.inspect}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
included do
|
68
|
+
class_attribute :attribute_definitions, instance_writer: false
|
69
|
+
self.attribute_definitions = {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(data = {})
|
73
|
+
super()
|
74
|
+
|
75
|
+
valid_keys = []
|
76
|
+
attribute_definitions.each do |attribute_name, attribute_definition|
|
77
|
+
if data.include?(attribute_name)
|
78
|
+
valid_keys << attribute_name
|
79
|
+
value = data.fetch(attribute_name)
|
80
|
+
_attributes[attribute_name] = attribute_definition.coerce(value)
|
81
|
+
elsif data.include?(attribute_name.to_s)
|
82
|
+
valid_keys << attribute_name
|
83
|
+
value = data[attribute_name.to_s]
|
84
|
+
_attributes[attribute_name] = attribute_definition.coerce(value)
|
85
|
+
elsif !attributes.include?(attribute_name)
|
86
|
+
_attributes[attribute_name] = attribute_definition.default
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
unless Avromatic.allow_unknown_attributes || valid_keys.size == data.size
|
91
|
+
unknown_attributes = (data.keys.map(&:to_s) - valid_keys.map(&:to_s)).sort
|
92
|
+
allowed_attributes = attribute_definitions.keys.map(&:to_s).sort
|
93
|
+
message = "Unexpected arguments for #{self.class.name}#initialize: #{unknown_attributes.join(', ')}. " \
|
94
|
+
"Only the following arguments are allowed: #{allowed_attributes.join(', ')}. Provided arguments: #{data.inspect}"
|
95
|
+
raise Avromatic::Model::UnknownAttributeError.new(message, unknown_attributes: unknown_attributes,
|
96
|
+
allowed_attributes: allowed_attributes)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_h
|
101
|
+
_attributes.dup
|
102
|
+
end
|
103
|
+
|
104
|
+
alias_method :to_hash, :to_h
|
105
|
+
alias_method :attributes, :to_h
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def _attributes
|
110
|
+
@attributes ||= {}
|
31
111
|
end
|
32
112
|
|
33
113
|
module ClassMethods
|
@@ -77,165 +157,39 @@ module Avromatic
|
|
77
157
|
end
|
78
158
|
|
79
159
|
schema.fields.each do |field|
|
80
|
-
raise OptionalFieldError.new(field) if !allow_optional && optional?(field)
|
81
|
-
|
82
|
-
field_class = avro_field_class(field.type)
|
83
|
-
|
84
|
-
attribute(field.name,
|
85
|
-
field_class,
|
86
|
-
avro_field_options(field, field_class))
|
87
|
-
|
88
|
-
add_validation(field, field_class)
|
89
|
-
add_serializer(field, field_class)
|
90
|
-
end
|
91
|
-
end
|
160
|
+
raise OptionalFieldError.new(field) if !allow_optional && FieldHelper.optional?(field)
|
92
161
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
when :record, :array, :map, :union
|
101
|
-
validate_complex(field.name)
|
102
|
-
else
|
103
|
-
add_type_validation(field.name, field_class)
|
104
|
-
end
|
105
|
-
|
106
|
-
add_required_validation(field)
|
107
|
-
end
|
162
|
+
symbolized_field_name = field.name.to_sym
|
163
|
+
attribute_definition = AttributeDefinition.new(
|
164
|
+
owner: self,
|
165
|
+
field: field,
|
166
|
+
type: create_type(field)
|
167
|
+
)
|
168
|
+
attribute_definitions[symbolized_field_name] = attribute_definition
|
108
169
|
|
109
|
-
|
110
|
-
|
111
|
-
[TrueClass, FalseClass]
|
112
|
-
elsif field_class < Avromatic::Model::Attribute::AbstractTimestamp
|
113
|
-
[Time]
|
114
|
-
else
|
115
|
-
[field_class]
|
116
|
-
end
|
170
|
+
define_method(field.name) { _attributes[symbolized_field_name] }
|
171
|
+
define_method("#{field.name}?") { !!_attributes[symbolized_field_name] } if boolean?(field)
|
117
172
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
def add_required_validation(field)
|
122
|
-
if required?(field) && field.default == :no_default
|
123
|
-
case field.type.type_sym
|
124
|
-
when :array, :map, :boolean
|
125
|
-
validates(field.name, exclusion: { in: [nil], message: "can't be nil" })
|
126
|
-
else
|
127
|
-
validates(field.name, presence: true)
|
173
|
+
define_method("#{field.name}=") do |value|
|
174
|
+
_attributes[symbolized_field_name] = attribute_definitions[symbolized_field_name].coerce(value)
|
128
175
|
end
|
129
|
-
end
|
130
|
-
end
|
131
176
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
end
|
138
|
-
|
139
|
-
def required?(field)
|
140
|
-
!optional?(field)
|
141
|
-
end
|
142
|
-
|
143
|
-
def avro_field_class(field_type)
|
144
|
-
custom_type = Avromatic.type_registry.fetch(field_type)
|
145
|
-
return custom_type.value_class if custom_type.value_class
|
146
|
-
|
147
|
-
if field_type.respond_to?(:logical_type)
|
148
|
-
value_class = Avromatic::Model::LogicalTypes.value_class(field_type.logical_type)
|
149
|
-
return value_class if value_class
|
150
|
-
end
|
151
|
-
|
152
|
-
case field_type.type_sym
|
153
|
-
when :string, :bytes, :fixed
|
154
|
-
String
|
155
|
-
when :boolean
|
156
|
-
Axiom::Types::Boolean
|
157
|
-
when :int, :long
|
158
|
-
Integer
|
159
|
-
when :float, :double
|
160
|
-
Float
|
161
|
-
when :enum
|
162
|
-
String
|
163
|
-
when :null
|
164
|
-
NilClass
|
165
|
-
when :array
|
166
|
-
Array[avro_field_class(field_type.items)]
|
167
|
-
when :map
|
168
|
-
Hash[String => avro_field_class(field_type.values)]
|
169
|
-
when :union
|
170
|
-
union_field_class(field_type)
|
171
|
-
when :record
|
172
|
-
build_nested_model(field_type)
|
173
|
-
else
|
174
|
-
raise "Unsupported type #{field_type}"
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def union_field_class(field_type)
|
179
|
-
null_index = field_type.schemas.index { |schema| schema.type_sym == :null }
|
180
|
-
raise 'a null type in a union must be the first member' if null_index && null_index > 0
|
181
|
-
|
182
|
-
field_classes = field_type.schemas.reject { |schema| schema.type_sym == :null }
|
183
|
-
.map { |schema| avro_field_class(schema) }
|
184
|
-
|
185
|
-
if field_classes.size == 1
|
186
|
-
field_classes.first
|
187
|
-
else
|
188
|
-
Avromatic::Model::AttributeType::Union[*field_classes]
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
def avro_field_options(field, field_class)
|
193
|
-
options = {}
|
194
|
-
|
195
|
-
prevent_union_including_custom_type!(field, field_class)
|
196
|
-
|
197
|
-
custom_type = Avromatic.type_registry.fetch(field, field_class)
|
198
|
-
coercer = custom_type.deserializer
|
199
|
-
options[:coercer] = coercer if coercer
|
200
|
-
|
201
|
-
# See: https://github.com/dasch/avro_turf/pull/36
|
202
|
-
if field.default != :no_default
|
203
|
-
options.merge!(default: default_for(field.default), lazy: true)
|
177
|
+
unless config.mutable # rubocop:disable Style/Next
|
178
|
+
private("#{field.name}=")
|
179
|
+
define_method(:clone) { self }
|
180
|
+
define_method(:dup) { self }
|
181
|
+
end
|
204
182
|
end
|
205
|
-
|
206
|
-
options
|
207
|
-
end
|
208
|
-
|
209
|
-
def add_serializer(field, field_class)
|
210
|
-
prevent_union_including_custom_type!(field, field_class)
|
211
|
-
|
212
|
-
custom_type = Avromatic.type_registry.fetch(field, field_class)
|
213
|
-
serializer = custom_type.serializer
|
214
|
-
|
215
|
-
avro_serializer[field.name.to_sym] = serializer if serializer
|
216
|
-
end
|
217
|
-
|
218
|
-
def default_for(value)
|
219
|
-
value.duplicable? ? value.dup.deep_freeze : value
|
220
183
|
end
|
221
184
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
field.type.schemas.any? do |klass|
|
226
|
-
Avromatic.type_registry.fetch(klass) != NullCustomType
|
227
|
-
end
|
185
|
+
def boolean?(field)
|
186
|
+
field.type.type_sym == :boolean ||
|
187
|
+
(FieldHelper.optional?(field) && field.type.schemas.last.type_sym == :boolean)
|
228
188
|
end
|
229
189
|
|
230
|
-
def
|
231
|
-
|
232
|
-
field_class < Avromatic::Model::AttributeType::Union &&
|
233
|
-
member_uses_custom_type?(field)
|
234
|
-
|
235
|
-
raise 'custom types within unions are currently unsupported'
|
236
|
-
end
|
190
|
+
def create_type(field)
|
191
|
+
Avromatic::Model::Types::TypeFactory.create(schema: field.type, nested_models: nested_models)
|
237
192
|
end
|
238
|
-
|
239
193
|
end
|
240
194
|
|
241
195
|
end
|
@@ -1,15 +1,15 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'active_support/concern'
|
3
4
|
require 'active_model'
|
4
|
-
require 'avromatic/model/allowed_writer_methods_memoization'
|
5
5
|
require 'avromatic/model/configuration'
|
6
6
|
require 'avromatic/model/value_object'
|
7
7
|
require 'avromatic/model/configurable'
|
8
|
+
require 'avromatic/model/field_helper'
|
8
9
|
require 'avromatic/model/nested_models'
|
9
10
|
require 'avromatic/model/validation'
|
10
|
-
require 'avromatic/model/
|
11
|
+
require 'avromatic/model/types/type_factory'
|
11
12
|
require 'avromatic/model/attributes'
|
12
|
-
require 'avromatic/model/attribute/record'
|
13
13
|
require 'avromatic/model/raw_serialization'
|
14
14
|
require 'avromatic/model/messaging_serialization'
|
15
15
|
|
@@ -42,9 +42,6 @@ module Avromatic
|
|
42
42
|
|
43
43
|
def inclusions
|
44
44
|
[
|
45
|
-
ActiveModel::Validations,
|
46
|
-
config.mutable ? Virtus.model : Virtus.value_object,
|
47
|
-
Avromatic::Model::AllowedWriterMethodsMemoization,
|
48
45
|
Avromatic::Model::Configurable,
|
49
46
|
Avromatic::Model::NestedModels,
|
50
47
|
Avromatic::Model::Validation,
|
@@ -1,11 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Avromatic
|
4
4
|
module Model
|
5
5
|
|
6
6
|
# Instances of this class contains the configuration for custom handling of
|
7
7
|
# a named type (record, enum, fixed).
|
8
|
-
class
|
8
|
+
class CustomTypeConfiguration
|
9
9
|
|
10
10
|
attr_accessor :to_avro, :from_avro, :value_class
|
11
11
|
|