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
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
class FloatType < AbstractType
|
9
|
+
VALUE_CLASSES = [::Float].freeze
|
10
|
+
INPUT_CLASSES = [::Float, ::Integer].freeze
|
11
|
+
|
12
|
+
def value_classes
|
13
|
+
VALUE_CLASSES
|
14
|
+
end
|
15
|
+
|
16
|
+
def input_classes
|
17
|
+
INPUT_CLASSES
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
'float'
|
22
|
+
end
|
23
|
+
|
24
|
+
def coerce(input)
|
25
|
+
if input.nil? || input.is_a?(::Float)
|
26
|
+
input
|
27
|
+
elsif input.is_a?(::Integer)
|
28
|
+
input.to_f
|
29
|
+
else
|
30
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def coercible?(input)
|
35
|
+
input.nil? || input.is_a?(::Float) || input.is_a?(::Integer)
|
36
|
+
end
|
37
|
+
|
38
|
+
def coerced?(input)
|
39
|
+
input.nil? || input.is_a?(::Float)
|
40
|
+
end
|
41
|
+
|
42
|
+
def serialize(value, **)
|
43
|
+
value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
class IntegerType < AbstractType
|
9
|
+
VALUE_CLASSES = [::Integer].freeze
|
10
|
+
|
11
|
+
def value_classes
|
12
|
+
VALUE_CLASSES
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
'integer'
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce(input)
|
20
|
+
if input.nil? || input.is_a?(::Integer)
|
21
|
+
input
|
22
|
+
else
|
23
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def coercible?(input)
|
28
|
+
input.nil? || input.is_a?(::Integer)
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :coerced?, :coercible?
|
32
|
+
|
33
|
+
def serialize(value, **)
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
class MapType < AbstractType
|
9
|
+
VALUE_CLASSES = [::Hash].freeze
|
10
|
+
|
11
|
+
attr_reader :value_type, :key_type
|
12
|
+
|
13
|
+
def initialize(key_type:, value_type:)
|
14
|
+
@key_type = key_type
|
15
|
+
@value_type = value_type
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
"map[#{key_type.name} => #{value_type.name}]"
|
20
|
+
end
|
21
|
+
|
22
|
+
def value_classes
|
23
|
+
VALUE_CLASSES
|
24
|
+
end
|
25
|
+
|
26
|
+
def coerce(input)
|
27
|
+
if input.nil?
|
28
|
+
input
|
29
|
+
elsif input.is_a?(::Hash)
|
30
|
+
input.each_with_object({}) do |(key_input, value_input), result|
|
31
|
+
result[key_type.coerce(key_input)] = value_type.coerce(value_input)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def coercible?(input)
|
39
|
+
if input.nil?
|
40
|
+
true
|
41
|
+
elsif input.is_a?(Hash)
|
42
|
+
input.all? do |key_input, value_input|
|
43
|
+
key_type.coercible?(key_input) && value_type.coercible?(value_input)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def coerced?(value)
|
51
|
+
if value.nil?
|
52
|
+
true
|
53
|
+
elsif value.is_a?(Hash)
|
54
|
+
value.all? do |element_key, element_value|
|
55
|
+
key_type.coerced?(element_key) && value_type.coerced?(element_value)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def serialize(value, strict:)
|
63
|
+
if value.nil?
|
64
|
+
value
|
65
|
+
else
|
66
|
+
value.each_with_object({}) do |(element_key, element_value), result|
|
67
|
+
result[key_type.serialize(element_key, strict: strict)] = value_type.serialize(element_value, strict: strict)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
class NullType < AbstractType
|
9
|
+
VALUE_CLASSES = [::NilClass].freeze
|
10
|
+
|
11
|
+
def value_classes
|
12
|
+
VALUE_CLASSES
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
'null'
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce(input)
|
20
|
+
if input.nil?
|
21
|
+
nil
|
22
|
+
else
|
23
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def coercible?(input)
|
28
|
+
input.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :coerced?, :coercible?
|
32
|
+
|
33
|
+
def serialize(_value, **)
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
class RecordType < AbstractType
|
9
|
+
attr_reader :record_class, :value_classes, :input_classes
|
10
|
+
|
11
|
+
def initialize(record_class:)
|
12
|
+
@record_class = record_class
|
13
|
+
@value_classes = [record_class].freeze
|
14
|
+
@input_classes = [record_class, Hash].freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
record_class.name.to_s.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def coerce(input)
|
22
|
+
if input.nil? || input.is_a?(record_class)
|
23
|
+
input
|
24
|
+
elsif input.is_a?(Hash)
|
25
|
+
record_class.new(input)
|
26
|
+
else
|
27
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def coercible?(input)
|
32
|
+
# TODO: Is there a better way to figure this out?
|
33
|
+
input.nil? || input.is_a?(record_class) || coerce(input).valid?
|
34
|
+
rescue StandardError
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def coerced?(value)
|
39
|
+
value.nil? || value.is_a?(record_class)
|
40
|
+
end
|
41
|
+
|
42
|
+
def serialize(value, strict:)
|
43
|
+
if value.nil?
|
44
|
+
value
|
45
|
+
elsif !strict && Avromatic.use_custom_datum_writer && Avromatic.use_encoding_providers? && !record_class.config.mutable
|
46
|
+
# n.b. Ideally we'd just return value here instead of wrapping it in a
|
47
|
+
# hash but then we'd have no place to stash the union member index...
|
48
|
+
{ Avromatic::IO::ENCODING_PROVIDER => value }
|
49
|
+
else
|
50
|
+
# This is only used for recursive serialization so validation has already been done
|
51
|
+
strict ? value.avro_value_datum(validate: false) : value.value_attributes_for_avro(validate: false)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
class StringType
|
9
|
+
VALUE_CLASSES = [::String].freeze
|
10
|
+
INPUT_CLASSES = [::String, ::Symbol].freeze
|
11
|
+
|
12
|
+
def value_classes
|
13
|
+
VALUE_CLASSES
|
14
|
+
end
|
15
|
+
|
16
|
+
def input_classes
|
17
|
+
INPUT_CLASSES
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
'string'
|
22
|
+
end
|
23
|
+
|
24
|
+
def coerce(input)
|
25
|
+
if input.nil? || input.is_a?(::String)
|
26
|
+
input
|
27
|
+
elsif input.is_a?(::Symbol)
|
28
|
+
input.to_s
|
29
|
+
else
|
30
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def coercible?(input)
|
35
|
+
input.nil? || input.is_a?(::String) || input.is_a?(::Symbol)
|
36
|
+
end
|
37
|
+
|
38
|
+
def coerced?(value)
|
39
|
+
value.nil? || value.is_a?(::String)
|
40
|
+
end
|
41
|
+
|
42
|
+
def serialize(value, **)
|
43
|
+
value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_timestamp_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
|
9
|
+
# This subclass is used to truncate timestamp values to microseconds.
|
10
|
+
class TimestampMicrosType < Avromatic::Model::Types::AbstractTimestampType
|
11
|
+
|
12
|
+
def name
|
13
|
+
'timestamp-micros'
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def truncated?(value)
|
19
|
+
value.nsec % 1000 == 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def coerce_time(input)
|
23
|
+
# value is coerced to a local Time
|
24
|
+
# The Avro representation of a timestamp is Epoch seconds, independent
|
25
|
+
# of time zone.
|
26
|
+
::Time.at(input.to_i, input.usec)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/abstract_timestamp_type'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
|
9
|
+
# This subclass is used to truncate timestamp values to milliseconds.
|
10
|
+
class TimestampMillisType < Avromatic::Model::Types::AbstractTimestampType
|
11
|
+
|
12
|
+
def name
|
13
|
+
'timestamp-millis'
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def truncated?(value)
|
19
|
+
value.usec % 1000 == 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def coerce_time(input)
|
23
|
+
# value is coerced to a local Time
|
24
|
+
# The Avro representation of a timestamp is Epoch seconds, independent
|
25
|
+
# of time zone.
|
26
|
+
::Time.at(input.to_i, input.usec / 1000 * 1000)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/types/array_type'
|
4
|
+
require 'avromatic/model/types/boolean_type'
|
5
|
+
require 'avromatic/model/types/custom_type'
|
6
|
+
require 'avromatic/model/types/date_type'
|
7
|
+
require 'avromatic/model/types/enum_type'
|
8
|
+
require 'avromatic/model/types/fixed_type'
|
9
|
+
require 'avromatic/model/types/float_type'
|
10
|
+
require 'avromatic/model/types/integer_type'
|
11
|
+
require 'avromatic/model/types/map_type'
|
12
|
+
require 'avromatic/model/types/null_type'
|
13
|
+
require 'avromatic/model/types/record_type'
|
14
|
+
require 'avromatic/model/types/string_type'
|
15
|
+
require 'avromatic/model/types/timestamp_micros_type'
|
16
|
+
require 'avromatic/model/types/timestamp_millis_type'
|
17
|
+
require 'avromatic/model/types/union_type'
|
18
|
+
|
19
|
+
module Avromatic
|
20
|
+
module Model
|
21
|
+
module Types
|
22
|
+
module TypeFactory
|
23
|
+
extend self
|
24
|
+
|
25
|
+
SINGLETON_TYPES = {
|
26
|
+
'date' => Avromatic::Model::Types::DateType.new,
|
27
|
+
'timestamp-micros' => Avromatic::Model::Types::TimestampMicrosType.new,
|
28
|
+
'timestamp-millis' => Avromatic::Model::Types::TimestampMillisType.new,
|
29
|
+
'string' => Avromatic::Model::Types::StringType.new,
|
30
|
+
'bytes' => Avromatic::Model::Types::StringType.new,
|
31
|
+
'boolean' => Avromatic::Model::Types::BooleanType.new,
|
32
|
+
'int' => Avromatic::Model::Types::IntegerType.new,
|
33
|
+
'long' => Avromatic::Model::Types::IntegerType.new,
|
34
|
+
'float' => Avromatic::Model::Types::FloatType.new,
|
35
|
+
'double' => Avromatic::Model::Types::FloatType.new,
|
36
|
+
'null' => Avromatic::Model::Types::NullType.new
|
37
|
+
}.deep_freeze
|
38
|
+
|
39
|
+
def create(schema:, nested_models:, use_custom_types: true)
|
40
|
+
if use_custom_types && Avromatic.custom_type_registry.registered?(schema)
|
41
|
+
custom_type_configuration = Avromatic.custom_type_registry.fetch(schema)
|
42
|
+
default_type = create(
|
43
|
+
schema: schema,
|
44
|
+
nested_models: nested_models,
|
45
|
+
use_custom_types: false
|
46
|
+
)
|
47
|
+
Avromatic::Model::Types::CustomType.new(
|
48
|
+
custom_type_configuration: custom_type_configuration,
|
49
|
+
default_type: default_type
|
50
|
+
)
|
51
|
+
elsif schema.respond_to?(:logical_type) && SINGLETON_TYPES.include?(schema.logical_type)
|
52
|
+
SINGLETON_TYPES.fetch(schema.logical_type)
|
53
|
+
elsif SINGLETON_TYPES.include?(schema.type)
|
54
|
+
SINGLETON_TYPES.fetch(schema.type)
|
55
|
+
else
|
56
|
+
case schema.type_sym
|
57
|
+
when :fixed
|
58
|
+
Avromatic::Model::Types::FixedType.new(schema.size)
|
59
|
+
when :enum
|
60
|
+
Avromatic::Model::Types::EnumType.new(schema.symbols)
|
61
|
+
when :array
|
62
|
+
value_type = create(schema: schema.items, nested_models: nested_models, use_custom_types: use_custom_types)
|
63
|
+
Avromatic::Model::Types::ArrayType.new(value_type: value_type)
|
64
|
+
when :map
|
65
|
+
value_type = create(schema: schema.values, nested_models: nested_models, use_custom_types: use_custom_types)
|
66
|
+
Avromatic::Model::Types::MapType.new(
|
67
|
+
key_type: Avromatic::Model::Types::StringType.new,
|
68
|
+
value_type: value_type
|
69
|
+
)
|
70
|
+
when :union
|
71
|
+
null_index = schema.schemas.index { |member_schema| member_schema.type_sym == :null }
|
72
|
+
raise 'a null type in a union must be the first member' if null_index && null_index > 0
|
73
|
+
|
74
|
+
member_schemas = schema.schemas.reject { |member_schema| member_schema.type_sym == :null }
|
75
|
+
if member_schemas.size == 1
|
76
|
+
create(schema: member_schemas.first, nested_models: nested_models)
|
77
|
+
else
|
78
|
+
member_types = member_schemas.map do |member_schema|
|
79
|
+
create(schema: member_schema, nested_models: nested_models, use_custom_types: use_custom_types)
|
80
|
+
end
|
81
|
+
Avromatic::Model::Types::UnionType.new(member_types: member_types)
|
82
|
+
end
|
83
|
+
when :record
|
84
|
+
record_class = build_nested_model(schema: schema, nested_models: nested_models)
|
85
|
+
Avromatic::Model::Types::RecordType.new(record_class: record_class)
|
86
|
+
else
|
87
|
+
raise ArgumentError.new("Unsupported type #{schema.type_sym}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def build_nested_model(schema:, nested_models:)
|
95
|
+
fullname = nested_models.remove_prefix(schema.fullname)
|
96
|
+
|
97
|
+
if nested_models.registered?(fullname)
|
98
|
+
nested_model = nested_models[fullname]
|
99
|
+
unless schema_fingerprint(schema) == schema_fingerprint(nested_model.avro_schema)
|
100
|
+
raise "The #{nested_model.name} model is already registered with an incompatible version of the #{schema.fullname} schema"
|
101
|
+
end
|
102
|
+
nested_model
|
103
|
+
else
|
104
|
+
Avromatic::Model.model(schema: schema, nested_models: nested_models)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def schema_fingerprint(schema)
|
109
|
+
if schema.respond_to?(:sha256_resolution_fingerprint)
|
110
|
+
schema.sha256_resolution_fingerprint
|
111
|
+
else
|
112
|
+
schema.sha256_fingerprint
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/io'
|
4
|
+
require 'avromatic/model/types/abstract_type'
|
5
|
+
|
6
|
+
module Avromatic
|
7
|
+
module Model
|
8
|
+
module Types
|
9
|
+
class UnionType < AbstractType
|
10
|
+
MEMBER_INDEX = ::Avromatic::IO::DatumReader::UNION_MEMBER_INDEX
|
11
|
+
attr_reader :member_types, :value_classes, :input_classes
|
12
|
+
|
13
|
+
def initialize(member_types:)
|
14
|
+
@member_types = member_types
|
15
|
+
@value_classes = member_types.flat_map(&:value_classes)
|
16
|
+
@input_classes = member_types.flat_map(&:input_classes).uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
"union[#{member_types.map(&:name).join(', ')}]"
|
21
|
+
end
|
22
|
+
|
23
|
+
def coerce(input)
|
24
|
+
return input if coerced?(input)
|
25
|
+
|
26
|
+
result = nil
|
27
|
+
if input.is_a?(Hash) && input.key?(MEMBER_INDEX)
|
28
|
+
result = member_types[input.delete(MEMBER_INDEX)].coerce(input)
|
29
|
+
else
|
30
|
+
member_types.find do |member_type|
|
31
|
+
result = safe_coerce(member_type, input)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
unless result
|
36
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
37
|
+
end
|
38
|
+
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
def coerced?(value)
|
43
|
+
value.nil? || member_types.any? do |member_type|
|
44
|
+
member_type.coerced?(value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def coercible?(input)
|
49
|
+
coerced?(input) || member_types.any? do |member_type|
|
50
|
+
member_type.coercible?(input)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def serialize(value, strict:)
|
55
|
+
# Avromatic does not treat the null of an optional field as part of the union
|
56
|
+
return nil if value.nil?
|
57
|
+
|
58
|
+
member_index = find_index(value)
|
59
|
+
if member_index.nil?
|
60
|
+
raise ArgumentError.new("Expected #{value.inspect} to be one of #{value_classes.map(&:name)}")
|
61
|
+
end
|
62
|
+
|
63
|
+
hash = member_types[member_index].serialize(value, strict: strict)
|
64
|
+
if !strict && Avromatic.use_custom_datum_writer && value.is_a?(Avromatic::Model::Attributes)
|
65
|
+
hash[Avromatic::IO::UNION_MEMBER_INDEX] = member_index
|
66
|
+
end
|
67
|
+
hash
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def find_index(value)
|
73
|
+
# TODO: Cache this?
|
74
|
+
member_types.find_index do |member_type|
|
75
|
+
member_type.value_classes.any? { |value_class| value.is_a?(value_class) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def safe_coerce(member_type, input)
|
80
|
+
member_type.coerce(input) if member_type.coercible?(input)
|
81
|
+
rescue StandardError
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Avromatic
|
4
|
+
module Model
|
5
|
+
class UnknownAttributeError < StandardError
|
6
|
+
attr_reader :unknown_attributes, :allowed_attributes
|
7
|
+
|
8
|
+
def initialize(message, unknown_attributes:, allowed_attributes:)
|
9
|
+
super(message)
|
10
|
+
@unknown_attributes = unknown_attributes
|
11
|
+
@allowed_attributes = allowed_attributes
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|