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
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avromatic/model/custom_type_configuration'
|
2
4
|
|
3
5
|
module Avromatic
|
4
6
|
module Model
|
5
|
-
class
|
7
|
+
class CustomTypeRegistry
|
6
8
|
|
7
9
|
delegate :clear, to: :custom_types
|
8
10
|
|
@@ -16,35 +18,30 @@ module Avromatic
|
|
16
18
|
# @block If a block is specified then the CustomType is yielded for
|
17
19
|
# additional configuration.
|
18
20
|
def register_type(fullname, value_class = nil)
|
19
|
-
custom_types[fullname.to_s] = Avromatic::Model::
|
21
|
+
custom_types[fullname.to_s] = Avromatic::Model::CustomTypeConfiguration.new(value_class).tap do |type|
|
20
22
|
yield(type) if block_given?
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
24
26
|
# @object [Avro::Schema] Custom type may be fetched based on a Avro field
|
25
|
-
# or schema.
|
26
|
-
|
27
|
-
def fetch(object, field_class = nil)
|
27
|
+
# or schema.
|
28
|
+
def registered?(object)
|
28
29
|
field_type = object.is_a?(Avro::Schema::Field) ? object.type : object
|
30
|
+
custom_types.include?(field_type.fullname) if field_type.is_a?(Avro::Schema::NamedSchema)
|
31
|
+
end
|
29
32
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
33
|
+
# @object [Avro::Schema] Custom type may be fetched based on a Avro field
|
34
|
+
# or schema. If there is no custom type, then an exception will be thrown.
|
35
|
+
# @field_class [Object] Value class that has been determined for a field.
|
36
|
+
def fetch(object)
|
37
|
+
field_type = object.is_a?(Avro::Schema::Field) ? object.type : object
|
34
38
|
fullname = field_type.fullname if field_type.is_a?(Avro::Schema::NamedSchema)
|
35
|
-
custom_types.fetch(fullname
|
39
|
+
custom_types.fetch(fullname)
|
36
40
|
end
|
37
41
|
|
38
42
|
private
|
39
43
|
|
40
44
|
attr_reader :custom_types
|
41
|
-
|
42
|
-
# The type that is used to define a Virtus attribute may be a Class or
|
43
|
-
# for an array or map field it may be an instance of an Array or Hash.
|
44
|
-
# This method safely checks if a Union class has been selected.
|
45
|
-
def union_attribute?(attribute_type)
|
46
|
-
attribute_type.is_a?(Class) && attribute_type < Avromatic::Model::AttributeType::Union
|
47
|
-
end
|
48
45
|
end
|
49
46
|
end
|
50
47
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Avromatic
|
4
|
+
module Model
|
5
|
+
module FieldHelper
|
6
|
+
extend self
|
7
|
+
|
8
|
+
# An optional field is represented as a union where the first member
|
9
|
+
# is null.
|
10
|
+
def optional?(field)
|
11
|
+
field.type.type_sym == :union &&
|
12
|
+
field.type.schemas.first.type_sym == :null
|
13
|
+
end
|
14
|
+
|
15
|
+
def required?(field)
|
16
|
+
!optional?(field)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/inflector/methods'
|
2
4
|
|
3
5
|
module Avromatic
|
@@ -8,26 +10,9 @@ module Avromatic
|
|
8
10
|
extend ActiveSupport::Concern
|
9
11
|
|
10
12
|
module ClassMethods
|
11
|
-
def build_nested_model(schema)
|
12
|
-
fullname = nested_models.remove_prefix(schema.fullname)
|
13
|
-
|
14
|
-
if nested_models.registered?(fullname)
|
15
|
-
nested_models[fullname]
|
16
|
-
else
|
17
|
-
Avromatic::Model.model(schema: schema,
|
18
|
-
nested_models: nested_models)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
13
|
# Register this model if it can be used as a nested model.
|
23
14
|
def register!
|
24
15
|
if key_avro_schema.nil? && value_avro_schema.type_sym == :record
|
25
|
-
# Register the generated model with Axiom to prevent it being
|
26
|
-
# treated as a BasicObject.
|
27
|
-
# See https://github.com/solnic/virtus/issues/284#issuecomment-56405137
|
28
|
-
nested_model = self
|
29
|
-
Axiom::Types::Object.new { primitive(nested_model) }
|
30
|
-
|
31
16
|
nested_models.register(self)
|
32
17
|
end
|
33
18
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Avromatic
|
4
4
|
module Model
|
@@ -11,108 +11,52 @@ module Avromatic
|
|
11
11
|
module Encode
|
12
12
|
extend ActiveSupport::Concern
|
13
13
|
|
14
|
-
delegate :
|
15
|
-
|
16
|
-
private :avro_serializer, :datum_writer, :datum_reader
|
14
|
+
delegate :datum_writer, :datum_reader, to: :class
|
15
|
+
private :datum_writer, :datum_reader
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
included do
|
21
|
-
@attribute_member_types = {}
|
22
|
-
end
|
23
|
-
|
24
|
-
module ClassMethods
|
25
|
-
def recursive_serialize(value, name: nil, member_types: nil, strict: false) # rubocop:disable Lint/ShadowedArgument
|
26
|
-
member_types = attribute_member_types(name) if name
|
27
|
-
member_types ||= EMPTY_ARRAY
|
28
|
-
|
29
|
-
if value.is_a?(Avromatic::Model::Attributes)
|
30
|
-
hash = strict ? value.avro_value_datum : value.value_attributes_for_avro
|
31
|
-
if !strict && Avromatic.use_custom_datum_writer
|
32
|
-
if Avromatic.use_encoding_providers? && !value.class.config.mutable
|
33
|
-
# n.b. Ideally we'd just return value here instead of wrapping it in a
|
34
|
-
# hash but then we'd have no place to stash the union member index...
|
35
|
-
hash = { Avromatic::IO::ENCODING_PROVIDER => value }
|
36
|
-
end
|
37
|
-
member_index = member_types.index(value.class) if member_types.any?
|
38
|
-
hash[Avromatic::IO::UNION_MEMBER_INDEX] = member_index if member_index
|
39
|
-
end
|
40
|
-
hash
|
41
|
-
elsif value.is_a?(Array)
|
42
|
-
value.map { |v| recursive_serialize(v, member_types: member_types, strict: strict) }
|
43
|
-
elsif value.is_a?(Hash)
|
44
|
-
value.each_with_object({}) do |(k, v), map|
|
45
|
-
map[k] = recursive_serialize(v, member_types: member_types, strict: strict)
|
46
|
-
end
|
47
|
-
else
|
48
|
-
avro_serializer[name].call(value)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def attribute_member_types(name)
|
55
|
-
@attribute_member_types.fetch(name) do
|
56
|
-
member_types = nil
|
57
|
-
attribute = attribute_set[name] if name
|
58
|
-
if attribute
|
59
|
-
if attribute.primitive == Array &&
|
60
|
-
attribute.member_type.is_a?(Avromatic::Model::Attribute::Union)
|
61
|
-
member_types = attribute.member_type.primitive.types
|
62
|
-
elsif attribute.primitive == Hash &&
|
63
|
-
attribute.value_type.is_a?(Avromatic::Model::Attribute::Union)
|
64
|
-
member_types = attribute.value_type.primitive.types
|
65
|
-
elsif attribute.options[:primitive] == Avromatic::Model::AttributeType::Union
|
66
|
-
member_types = attribute.primitive.types
|
67
|
-
end
|
68
|
-
end
|
69
|
-
@attribute_member_types[name] = member_types
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def avro_raw_value
|
17
|
+
def avro_raw_value(validate: true)
|
75
18
|
if self.class.config.mutable
|
76
|
-
avro_raw_encode(value_attributes_for_avro, :value)
|
19
|
+
avro_raw_encode(value_attributes_for_avro(validate: validate), :value)
|
77
20
|
else
|
78
|
-
@avro_raw_value ||= avro_raw_encode(value_attributes_for_avro, :value)
|
21
|
+
@avro_raw_value ||= avro_raw_encode(value_attributes_for_avro(validate: validate), :value)
|
79
22
|
end
|
80
23
|
end
|
81
24
|
|
82
|
-
def avro_raw_key
|
25
|
+
def avro_raw_key(validate: true)
|
83
26
|
raise 'Model has no key schema' unless key_avro_schema
|
84
|
-
avro_raw_encode(key_attributes_for_avro, :key)
|
27
|
+
avro_raw_encode(key_attributes_for_avro(validate: validate), :key)
|
85
28
|
end
|
86
29
|
|
87
|
-
def value_attributes_for_avro
|
30
|
+
def value_attributes_for_avro(validate: true)
|
88
31
|
if self.class.config.mutable
|
89
|
-
avro_hash(value_avro_field_names)
|
32
|
+
avro_hash(value_avro_field_names, validate: validate)
|
90
33
|
else
|
91
|
-
@value_attributes_for_avro ||= avro_hash(value_avro_field_names)
|
34
|
+
@value_attributes_for_avro ||= avro_hash(value_avro_field_names, validate: validate)
|
92
35
|
end
|
93
36
|
end
|
94
37
|
|
95
|
-
def key_attributes_for_avro
|
96
|
-
avro_hash(key_avro_field_names)
|
38
|
+
def key_attributes_for_avro(validate: true)
|
39
|
+
avro_hash(key_avro_field_names, validate: validate)
|
97
40
|
end
|
98
41
|
|
99
|
-
def avro_value_datum
|
42
|
+
def avro_value_datum(validate: true)
|
100
43
|
if self.class.config.mutable
|
101
|
-
avro_hash(value_avro_field_names, strict: true)
|
44
|
+
avro_hash(value_avro_field_names, strict: true, validate: validate)
|
102
45
|
else
|
103
|
-
@avro_datum ||= avro_hash(value_avro_field_names, strict: true)
|
46
|
+
@avro_datum ||= avro_hash(value_avro_field_names, strict: true, validate: validate)
|
104
47
|
end
|
105
48
|
end
|
106
49
|
|
107
|
-
def avro_key_datum
|
108
|
-
avro_hash(key_avro_field_names, strict: true)
|
50
|
+
def avro_key_datum(validate: true)
|
51
|
+
avro_hash(key_avro_field_names, strict: true, validate: validate)
|
109
52
|
end
|
110
53
|
|
111
54
|
private
|
112
55
|
|
113
|
-
def avro_hash(fields, strict: false)
|
56
|
+
def avro_hash(fields, strict: false, validate:)
|
57
|
+
avro_validate! if validate
|
114
58
|
attributes.slice(*fields).each_with_object(Hash.new) do |(key, value), result|
|
115
|
-
result[key.to_s] =
|
59
|
+
result[key.to_s] = attribute_definitions[key].serialize(value, strict: strict)
|
116
60
|
end
|
117
61
|
end
|
118
62
|
|
@@ -161,13 +105,6 @@ module Avromatic
|
|
161
105
|
Avromatic.use_custom_datum_writer ? Avromatic::IO::DatumWriter : Avro::IO::DatumWriter
|
162
106
|
end
|
163
107
|
|
164
|
-
# Store a hash of Procs by field name (as a symbol) to convert
|
165
|
-
# the value before Avro serialization.
|
166
|
-
# Returns the default PassthroughSerializer if a key is not present.
|
167
|
-
def avro_serializer
|
168
|
-
@avro_serializer ||= Hash.new(PassthroughSerializer)
|
169
|
-
end
|
170
|
-
|
171
108
|
def datum_writer
|
172
109
|
@datum_writer ||= begin
|
173
110
|
hash = { value: datum_writer_class.new(value_avro_schema) }
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/time_with_zone'
|
4
|
+
|
5
|
+
module Avromatic
|
6
|
+
module Model
|
7
|
+
module Types
|
8
|
+
class AbstractTimestampType < AbstractType
|
9
|
+
VALUE_CLASSES = [::Time].freeze
|
10
|
+
INPUT_CLASSES = [::Time, ::DateTime, ::ActiveSupport::TimeWithZone].freeze
|
11
|
+
|
12
|
+
def value_classes
|
13
|
+
VALUE_CLASSES
|
14
|
+
end
|
15
|
+
|
16
|
+
def input_classes
|
17
|
+
INPUT_CLASSES
|
18
|
+
end
|
19
|
+
|
20
|
+
def coerce(input)
|
21
|
+
if input.nil? || coerced?(input)
|
22
|
+
input
|
23
|
+
elsif input.is_a?(::Time) || input.is_a?(::DateTime)
|
24
|
+
coerce_time(input)
|
25
|
+
else
|
26
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def coercible?(input)
|
31
|
+
input.nil? || input.is_a?(::Time) || input.is_a?(::DateTime)
|
32
|
+
end
|
33
|
+
|
34
|
+
def coerced?(value)
|
35
|
+
# ActiveSupport::TimeWithZone overrides is_a? is to make it look like a Time
|
36
|
+
# even though it's not which can lead to unexpected behavior if we don't force
|
37
|
+
# a coercion
|
38
|
+
value.is_a?(::Time) && value.class != ActiveSupport::TimeWithZone && truncated?(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def serialize(value, **)
|
42
|
+
value
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def truncated?(_value)
|
48
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def coerce_time(_input)
|
52
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Avromatic
|
4
|
+
module Model
|
5
|
+
module Types
|
6
|
+
class AbstractType
|
7
|
+
def value_classes
|
8
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def input_classes
|
12
|
+
value_classes
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce(_input)
|
20
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def coercible?(_input)
|
24
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def coerced?(_value)
|
28
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize(_value, **)
|
32
|
+
raise "#{__method__} must be overridden by #{self.class.name}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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 ArrayType < AbstractType
|
9
|
+
VALUE_CLASSES = [::Array].freeze
|
10
|
+
|
11
|
+
attr_reader :value_type
|
12
|
+
|
13
|
+
def initialize(value_type:)
|
14
|
+
@value_type = value_type
|
15
|
+
end
|
16
|
+
|
17
|
+
def value_classes
|
18
|
+
VALUE_CLASSES
|
19
|
+
end
|
20
|
+
|
21
|
+
def name
|
22
|
+
"array[#{value_type.name}]"
|
23
|
+
end
|
24
|
+
|
25
|
+
def coerce(input)
|
26
|
+
if input.nil?
|
27
|
+
input
|
28
|
+
elsif input.is_a?(::Array)
|
29
|
+
input.map { |element_input| value_type.coerce(element_input) }
|
30
|
+
else
|
31
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def coercible?(input)
|
36
|
+
input.nil? || (input.is_a?(::Array) && input.all? { |element_input| value_type.coercible?(element_input) })
|
37
|
+
end
|
38
|
+
|
39
|
+
def coerced?(value)
|
40
|
+
value.nil? || (value.is_a?(::Array) && value.all? { |element| value_type.coerced?(element) })
|
41
|
+
end
|
42
|
+
|
43
|
+
def serialize(value, strict:)
|
44
|
+
if value.nil?
|
45
|
+
value
|
46
|
+
else
|
47
|
+
value.map { |element| value_type.serialize(element, strict: strict) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
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 BooleanType < AbstractType
|
9
|
+
VALUE_CLASSES = [::TrueClass, ::FalseClass].freeze
|
10
|
+
|
11
|
+
def value_classes
|
12
|
+
VALUE_CLASSES
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
'boolean'
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce(input)
|
20
|
+
if coercible?(input)
|
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?(::TrueClass) || input.is_a?(::FalseClass)
|
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,64 @@
|
|
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 CustomType < AbstractType
|
9
|
+
IDENTITY_PROC = Proc.new { |value| value }
|
10
|
+
|
11
|
+
attr_reader :custom_type_configuration, :value_classes, :default_type
|
12
|
+
|
13
|
+
def initialize(custom_type_configuration:, default_type:)
|
14
|
+
@custom_type_configuration = custom_type_configuration
|
15
|
+
@default_type = default_type
|
16
|
+
@deserializer = custom_type_configuration.deserializer || IDENTITY_PROC
|
17
|
+
@serializer = custom_type_configuration.serializer || IDENTITY_PROC
|
18
|
+
@value_classes = if custom_type_configuration.value_class
|
19
|
+
[custom_type_configuration.value_class].freeze
|
20
|
+
else
|
21
|
+
default_type.value_classes
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def input_classes
|
26
|
+
# We don't know the valid input classes for a custom type
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
custom_type_configuration.value_class ? custom_type_configuration.value_class.name.to_s.freeze : default_type.name
|
31
|
+
end
|
32
|
+
|
33
|
+
def coerce(input)
|
34
|
+
if input.nil?
|
35
|
+
input
|
36
|
+
else
|
37
|
+
@deserializer.call(input)
|
38
|
+
end
|
39
|
+
rescue StandardError => e
|
40
|
+
# TODO: Don't swallow this
|
41
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}: #{e.message}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def coercible?(input)
|
45
|
+
# TODO: Delegate this to optional configuration
|
46
|
+
input.nil? || !coerce(input).nil?
|
47
|
+
rescue ArgumentError
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def coerced?(value)
|
52
|
+
# TODO: Delegate this to optional configuration
|
53
|
+
coerce(value) == value
|
54
|
+
rescue ArgumentError
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def serialize(value, **)
|
59
|
+
@serializer.call(value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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 DateType < AbstractType
|
9
|
+
VALUE_CLASSES = [::Date].freeze
|
10
|
+
|
11
|
+
def value_classes
|
12
|
+
VALUE_CLASSES
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
'date'
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce(input)
|
20
|
+
if input.is_a?(::Time) || input.is_a?(::DateTime)
|
21
|
+
::Date.new(input.year, input.month, input.day)
|
22
|
+
elsif input.nil? || input.is_a?(::Date)
|
23
|
+
input
|
24
|
+
else
|
25
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def coercible?(input)
|
30
|
+
input.nil? || input.is_a?(::Date) || input.is_a?(::Time)
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :coerced?, :coercible?
|
34
|
+
|
35
|
+
def serialize(value, **)
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,56 @@
|
|
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 EnumType < AbstractType
|
9
|
+
VALUE_CLASSES = [::String].freeze
|
10
|
+
INPUT_CLASSES = [::String, ::Symbol].freeze
|
11
|
+
|
12
|
+
attr_reader :allowed_values
|
13
|
+
|
14
|
+
def initialize(allowed_values)
|
15
|
+
@allowed_values = allowed_values.to_set
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
"enum#{allowed_values.to_a}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def value_classes
|
23
|
+
VALUE_CLASSES
|
24
|
+
end
|
25
|
+
|
26
|
+
def input_classes
|
27
|
+
INPUT_CLASSES
|
28
|
+
end
|
29
|
+
|
30
|
+
def coerce(input)
|
31
|
+
if input.nil?
|
32
|
+
input
|
33
|
+
elsif coercible?(input)
|
34
|
+
input.to_s
|
35
|
+
else
|
36
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def coerced?(input)
|
41
|
+
input.nil? || (input.is_a?(::String) && allowed_values.include?(input))
|
42
|
+
end
|
43
|
+
|
44
|
+
def coercible?(input)
|
45
|
+
input.nil? ||
|
46
|
+
(input.is_a?(::String) && allowed_values.include?(input)) ||
|
47
|
+
(input.is_a?(::Symbol) && allowed_values.include?(input.to_s))
|
48
|
+
end
|
49
|
+
|
50
|
+
def serialize(value, **)
|
51
|
+
value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,45 @@
|
|
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 FixedType < AbstractType
|
9
|
+
VALUE_CLASSES = [::String].freeze
|
10
|
+
|
11
|
+
attr_reader :size
|
12
|
+
|
13
|
+
def initialize(size)
|
14
|
+
@size = size
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
"fixed(#{size})"
|
19
|
+
end
|
20
|
+
|
21
|
+
def value_classes
|
22
|
+
VALUE_CLASSES
|
23
|
+
end
|
24
|
+
|
25
|
+
def coerce(input)
|
26
|
+
if coercible?(input)
|
27
|
+
input
|
28
|
+
else
|
29
|
+
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def coercible?(input)
|
34
|
+
input.nil? || (input.is_a?(::String) && input.length == size)
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :coerced?, :coercible?
|
38
|
+
|
39
|
+
def serialize(value, **)
|
40
|
+
value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|