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
@@ -1,8 +1,10 @@
1
- require 'avromatic/model/custom_type'
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 TypeRegistry
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::CustomType.new(value_class).tap do |type|
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. If there is no custom type, then NullCustomType is returned.
26
- # @field_class [Object] Value class that has been determined for a field.
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
- if field_class && field_type.type_sym == :union && !union_attribute?(field_class)
31
- field_type = Avromatic::Model::Attributes.first_union_schema(field_type)
32
- end
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, NullCustomType)
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
  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,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
- require 'avromatic/model/passthrough_serializer'
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 :avro_serializer, :datum_writer, :datum_reader, :attribute_set,
15
- to: :class
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
- EMPTY_ARRAY = [].freeze
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] = self.class.recursive_serialize(value, name: key, strict: strict)
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