avromatic 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|