avromatic 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/Appraisals +2 -0
  4. data/CHANGELOG.md +15 -0
  5. data/Gemfile +2 -0
  6. data/README.md +57 -25
  7. data/Rakefile +2 -0
  8. data/avromatic.gemspec +7 -3
  9. data/lib/avromatic/io/datum_reader.rb +2 -0
  10. data/lib/avromatic/io/datum_writer.rb +7 -1
  11. data/lib/avromatic/io.rb +4 -2
  12. data/lib/avromatic/messaging.rb +2 -0
  13. data/lib/avromatic/model/attributes.rb +112 -158
  14. data/lib/avromatic/model/builder.rb +4 -7
  15. data/lib/avromatic/model/coercion_error.rb +8 -0
  16. data/lib/avromatic/model/configurable.rb +2 -0
  17. data/lib/avromatic/model/configuration.rb +2 -0
  18. data/lib/avromatic/model/{custom_type.rb → custom_type_configuration.rb} +2 -2
  19. data/lib/avromatic/model/{type_registry.rb → custom_type_registry.rb} +15 -18
  20. data/lib/avromatic/model/field_helper.rb +20 -0
  21. data/lib/avromatic/model/message_decoder.rb +2 -0
  22. data/lib/avromatic/model/messaging_serialization.rb +2 -0
  23. data/lib/avromatic/model/nested_models.rb +2 -17
  24. data/lib/avromatic/model/raw_serialization.rb +21 -84
  25. data/lib/avromatic/model/types/abstract_timestamp_type.rb +57 -0
  26. data/lib/avromatic/model/types/abstract_type.rb +37 -0
  27. data/lib/avromatic/model/types/array_type.rb +53 -0
  28. data/lib/avromatic/model/types/boolean_type.rb +39 -0
  29. data/lib/avromatic/model/types/custom_type.rb +64 -0
  30. data/lib/avromatic/model/types/date_type.rb +41 -0
  31. data/lib/avromatic/model/types/enum_type.rb +56 -0
  32. data/lib/avromatic/model/types/fixed_type.rb +45 -0
  33. data/lib/avromatic/model/types/float_type.rb +48 -0
  34. data/lib/avromatic/model/types/integer_type.rb +39 -0
  35. data/lib/avromatic/model/types/map_type.rb +74 -0
  36. data/lib/avromatic/model/types/null_type.rb +39 -0
  37. data/lib/avromatic/model/types/record_type.rb +57 -0
  38. data/lib/avromatic/model/types/string_type.rb +48 -0
  39. data/lib/avromatic/model/types/timestamp_micros_type.rb +32 -0
  40. data/lib/avromatic/model/types/timestamp_millis_type.rb +32 -0
  41. data/lib/avromatic/model/types/type_factory.rb +118 -0
  42. data/lib/avromatic/model/types/union_type.rb +87 -0
  43. data/lib/avromatic/model/unknown_attribute_error.rb +15 -0
  44. data/lib/avromatic/model/validation.rb +57 -36
  45. data/lib/avromatic/model/validation_error.rb +8 -0
  46. data/lib/avromatic/model/value_object.rb +2 -0
  47. data/lib/avromatic/model.rb +6 -2
  48. data/lib/avromatic/model_registry.rb +2 -0
  49. data/lib/avromatic/patches/schema_validator_patch.rb +2 -0
  50. data/lib/avromatic/patches.rb +2 -0
  51. data/lib/avromatic/railtie.rb +2 -0
  52. data/lib/avromatic/rspec.rb +2 -0
  53. data/lib/avromatic/version.rb +3 -1
  54. data/lib/avromatic.rb +9 -4
  55. metadata +32 -21
  56. data/lib/avromatic/model/allowed_type_validator.rb +0 -7
  57. data/lib/avromatic/model/allowed_writer_methods_memoization.rb +0 -16
  58. data/lib/avromatic/model/attribute/abstract_timestamp.rb +0 -26
  59. data/lib/avromatic/model/attribute/record.rb +0 -26
  60. data/lib/avromatic/model/attribute/timestamp_micros.rb +0 -26
  61. data/lib/avromatic/model/attribute/timestamp_millis.rb +0 -26
  62. data/lib/avromatic/model/attribute/union.rb +0 -66
  63. data/lib/avromatic/model/attribute_type/union.rb +0 -29
  64. data/lib/avromatic/model/logical_types.rb +0 -19
  65. data/lib/avromatic/model/null_custom_type.rb +0 -21
  66. data/lib/avromatic/model/passthrough_serializer.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ef79f75351178c3006ab6c03ed41b05340be9f81acc032d43c977c1d428182b
4
- data.tar.gz: 94df5ca22cbf1689f47547347663a6a23e75adc2b3a02b010580267af5319f2a
3
+ metadata.gz: 25174d8c67bde4e8a9d7a53dc357ed72db2e1faa08bbf8945d1f93dd69c46942
4
+ data.tar.gz: b0b2fce3ddb92a463fade05c2cb29cdadd60037357432c99ece5f892d3b54f45
5
5
  SHA512:
6
- metadata.gz: f837a8b95d1328788bbff5b55bd2ef2c19f9ad66416855e0767b196d79cea7b08461049d64dcd8cb93e61418aadf5c248662e54ed4e588d00122ede2157057ab
7
- data.tar.gz: 5917d121753d94057e7fb7c9d658f9a59e00d58fb058644762a0a3c8a711049de41e605a969f5d410cc48af58418c7b50f20b39014393b4d10f29ac62fa788b8
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.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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  appraise 'rails5_0' do
2
4
  gem 'avro', '1.8.2'
3
5
  gem 'activesupport', '~> 5.0.6'
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in avromatic.gemspec
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 [Virtus](https://github.com/solnic/virtus) value
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
- `Virtus` attributes are added for each field in the Avro schema
139
- including any default values defined in the schema. `ActiveModel` validations
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 Virtus attributes
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 replaced to better
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
- The following validations are supported:
387
-
388
- - The size of the value for a fixed type field.
389
- - The value for an enum type field is in the declared set of values.
390
- - Presence of a value for required fields. Empty arrays and maps are considered
391
- valid for required fields.
392
- - Validity of nested records, including records embedded in array, maps, and
393
- unions.
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
  require 'appraisal/task'
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.add_runtime_dependency 'activemodel', '>= 4.1', '< 5.3'
22
- spec.add_runtime_dependency 'activesupport', '>= 4.1', '< 5.3'
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 'virtus'
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
  # rubocop:disable Style/WhenThen
2
4
  module Avromatic
3
5
  module IO
@@ -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
- encoder.write(datum[Avromatic::IO::ENCODING_PROVIDER].avro_raw_value)
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'.freeze
4
- ENCODING_PROVIDER = '__avromatic_encoding_provider'.freeze
5
+ UNION_MEMBER_INDEX = '__avromatic_member_index'
6
+ ENCODING_PROVIDER = '__avromatic_encoding_provider'
5
7
  end
6
8
  end
7
9
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'avro_turf/messaging'
2
4
  require 'avromatic/io'
3
5
 
@@ -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
- def self.first_union_schema(field_type)
24
- # TODO: This is a hack until I find a better solution for unions with
25
- # Virtus. This only handles a union for an optional field with :null
26
- # and one other type.
27
- # This hack lives on for now because custom type coercion is not pushed
28
- # down into unions. This means that custom types can only be optional
29
- # fields, not members of real unions.
30
- field_type.schemas.reject { |schema| schema.type_sym == :null }.first
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
- def add_validation(field, field_class)
94
- case field.type.type_sym
95
- when :enum
96
- validates(field.name,
97
- inclusion: { in: Set.new(field.type.symbols.map(&:freeze)).freeze })
98
- when :fixed
99
- validates(field.name, length: { is: field.type.size })
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
- def add_type_validation(name, field_class)
110
- allowed_types = if field_class == Axiom::Types::Boolean
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
- validates(name, allowed_type: allowed_types, allow_blank: true)
119
- end
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
- # An optional field is represented as a union where the first member
133
- # is null.
134
- def optional?(field)
135
- field.type.type_sym == :union &&
136
- field.type.schemas.first.type_sym == :null
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
- # TODO: the methods below are temporary until support for custom types
223
- # as union members are supported.
224
- def member_uses_custom_type?(field)
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 prevent_union_including_custom_type!(field, field_class)
231
- if field_class.is_a?(Class) &&
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
- require 'virtus'
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/attribute/union'
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,
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avromatic
4
+ module Model
5
+ class CoercionError < StandardError
6
+ end
7
+ end
8
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Avromatic
2
4
  module Model
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Avromatic
2
4
  module Model
3
5
 
@@ -1,11 +1,11 @@
1
- require 'avromatic/model/null_custom_type'
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 CustomType
8
+ class CustomTypeConfiguration
9
9
 
10
10
  attr_accessor :to_avro, :from_avro, :value_class
11
11