ama-entity-mapper 0.1.0.beta.2

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/docs/algorithm.md +61 -0
  3. data/docs/basic-usage.md +179 -0
  4. data/docs/dsl.md +90 -0
  5. data/docs/generics.md +55 -0
  6. data/docs/handlers.md +196 -0
  7. data/docs/index.md +23 -0
  8. data/docs/installation.md +27 -0
  9. data/docs/logging.md +18 -0
  10. data/docs/wildcards.md +16 -0
  11. data/lib/ama-entity-mapper.rb +42 -0
  12. data/lib/ama-entity-mapper/aux/null_stream.rb +30 -0
  13. data/lib/ama-entity-mapper/context.rb +61 -0
  14. data/lib/ama-entity-mapper/dsl.rb +21 -0
  15. data/lib/ama-entity-mapper/dsl/class_methods.rb +100 -0
  16. data/lib/ama-entity-mapper/engine.rb +88 -0
  17. data/lib/ama-entity-mapper/engine/recursive_mapper.rb +164 -0
  18. data/lib/ama-entity-mapper/engine/recursive_normalizer.rb +74 -0
  19. data/lib/ama-entity-mapper/error.rb +11 -0
  20. data/lib/ama-entity-mapper/error/compliance_error.rb +15 -0
  21. data/lib/ama-entity-mapper/error/mapping_error.rb +14 -0
  22. data/lib/ama-entity-mapper/error/validation_error.rb +14 -0
  23. data/lib/ama-entity-mapper/handler/attribute/validator.rb +107 -0
  24. data/lib/ama-entity-mapper/handler/entity/denormalizer.rb +97 -0
  25. data/lib/ama-entity-mapper/handler/entity/enumerator.rb +76 -0
  26. data/lib/ama-entity-mapper/handler/entity/factory.rb +86 -0
  27. data/lib/ama-entity-mapper/handler/entity/injector.rb +69 -0
  28. data/lib/ama-entity-mapper/handler/entity/normalizer.rb +68 -0
  29. data/lib/ama-entity-mapper/handler/entity/validator.rb +66 -0
  30. data/lib/ama-entity-mapper/mixin/errors.rb +55 -0
  31. data/lib/ama-entity-mapper/mixin/handler_support.rb +69 -0
  32. data/lib/ama-entity-mapper/mixin/reflection.rb +67 -0
  33. data/lib/ama-entity-mapper/mixin/suppression_support.rb +37 -0
  34. data/lib/ama-entity-mapper/path.rb +91 -0
  35. data/lib/ama-entity-mapper/path/segment.rb +51 -0
  36. data/lib/ama-entity-mapper/type.rb +243 -0
  37. data/lib/ama-entity-mapper/type/analyzer.rb +27 -0
  38. data/lib/ama-entity-mapper/type/any.rb +66 -0
  39. data/lib/ama-entity-mapper/type/attribute.rb +197 -0
  40. data/lib/ama-entity-mapper/type/aux/hash_tuple.rb +35 -0
  41. data/lib/ama-entity-mapper/type/builtin/array_type.rb +28 -0
  42. data/lib/ama-entity-mapper/type/builtin/datetime_type.rb +65 -0
  43. data/lib/ama-entity-mapper/type/builtin/enumerable_type.rb +74 -0
  44. data/lib/ama-entity-mapper/type/builtin/hash_tuple_type.rb +33 -0
  45. data/lib/ama-entity-mapper/type/builtin/hash_type.rb +82 -0
  46. data/lib/ama-entity-mapper/type/builtin/primitive_type.rb +61 -0
  47. data/lib/ama-entity-mapper/type/builtin/primitive_type/denormalizer.rb +62 -0
  48. data/lib/ama-entity-mapper/type/builtin/rational_type.rb +59 -0
  49. data/lib/ama-entity-mapper/type/builtin/set_type.rb +74 -0
  50. data/lib/ama-entity-mapper/type/parameter.rb +70 -0
  51. data/lib/ama-entity-mapper/type/registry.rb +117 -0
  52. data/lib/ama-entity-mapper/type/resolver.rb +105 -0
  53. data/lib/ama-entity-mapper/version.rb +17 -0
  54. metadata +194 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMA
4
+ module Entity
5
+ class Mapper
6
+ class Type
7
+ module Aux
8
+ # Simple class to store paired data items
9
+ class HashTuple
10
+ attr_accessor :key
11
+ attr_accessor :value
12
+
13
+ def initialize(key: nil, value: nil)
14
+ @key = key
15
+ @value = value
16
+ end
17
+
18
+ def hash
19
+ @key.hash ^ @value.hash
20
+ end
21
+
22
+ def eql?(other)
23
+ return false unless other.is_a?(HashTuple)
24
+ @key == other.key && @value == other.value
25
+ end
26
+
27
+ def ==(other)
28
+ eql?(other)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'enumerable_type'
4
+
5
+ module AMA
6
+ module Entity
7
+ class Mapper
8
+ class Type
9
+ module BuiltIn
10
+ # Even though it is functionally unnecessary, end users are more
11
+ # likely to call `Mapper.map(input, Array)` rather than
12
+ # `Mapper.map(input, Enumerable)`. Because mapper has no right to
13
+ # make assumptions about type children, it would have to back off to
14
+ # standard hash-based normalization/denormalization, and that would
15
+ # cause end-user frustration
16
+ class ArrayType < EnumerableType
17
+ def initialize
18
+ super
19
+ self.type = Array
20
+ end
21
+
22
+ INSTANCE = new
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ require_relative '../../type'
6
+ require_relative '../../mixin/errors'
7
+
8
+ module AMA
9
+ module Entity
10
+ class Mapper
11
+ class Type
12
+ module BuiltIn
13
+ # DateTime type description
14
+ class DateTimeType < Type
15
+ def initialize
16
+ super(DateTime)
17
+
18
+ normalizer_block do |entity, *|
19
+ entity.iso8601(3)
20
+ end
21
+
22
+ define_denormalizer
23
+ define_factory
24
+
25
+ enumerator_block do |*|
26
+ ::Enumerator.new { |*| }
27
+ end
28
+
29
+ injector_block { |*| }
30
+ end
31
+
32
+ private
33
+
34
+ def define_denormalizer
35
+ denormalizer_block do |input, _, ctx|
36
+ break input if input.is_a?(DateTime)
37
+ input = input.to_s if input.is_a?(Symbol)
38
+ break DateTime.iso8601(input, 3) if input.is_a?(String)
39
+ if input.is_a?(Integer)
40
+ break DateTime.strptime(input.to_s, '%s')
41
+ end
42
+ singleton_class.send(:include, Mixin::Errors)
43
+ message = 'String input expected (like ' \
44
+ "'2001-02-03T04:05:06.123+04:00'), " \
45
+ "#{input.class} received: #{input}"
46
+ mapping_error(message, context: ctx)
47
+ end
48
+ end
49
+
50
+ def define_factory
51
+ factory_block do |_, _, ctx|
52
+ singleton_class.send(:include, Mixin::Errors)
53
+ message = 'DateTime type could not be instantiated directly, ' \
54
+ 'it only supports normalization and denormalization'
55
+ compliance_error(message, context: ctx)
56
+ end
57
+ end
58
+
59
+ INSTANCE = new
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../type'
4
+ require_relative '../../path/segment'
5
+ require_relative '../../mixin/errors'
6
+
7
+ module AMA
8
+ module Entity
9
+ class Mapper
10
+ class Type
11
+ module BuiltIn
12
+ # Default Enumerable handler
13
+ class EnumerableType < Type
14
+ include Mixin::Errors
15
+
16
+ def initialize
17
+ super(::Enumerable)
18
+ attribute!(:_value, parameter!(:T), virtual: true)
19
+
20
+ define_factory
21
+ define_normalizer
22
+ define_denormalizer
23
+ define_enumerator
24
+ define_injector
25
+ end
26
+
27
+ private
28
+
29
+ def define_factory
30
+ factory_block do |*|
31
+ []
32
+ end
33
+ end
34
+
35
+ def define_normalizer
36
+ normalizer_block do |input, *|
37
+ input.map(&:itself)
38
+ end
39
+ end
40
+
41
+ def define_denormalizer
42
+ denormalizer_block do |data, type, context = nil, *|
43
+ if data.is_a?(Hash) || !data.is_a?(Enumerable)
44
+ message = "Can't denormalize Enumerable from #{data.class}"
45
+ type.mapping_error(message, context: context)
46
+ end
47
+ data.map(&:itself)
48
+ end
49
+ end
50
+
51
+ def define_enumerator
52
+ enumerator_block do |entity, type, *|
53
+ ::Enumerator.new do |yielder|
54
+ attribute = type.attributes[:_value]
55
+ entity.each_with_index do |value, index|
56
+ yielder << [attribute, value, Path::Segment.index(index)]
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def define_injector
63
+ injector_block do |entity, _, _, value, context|
64
+ entity[context.path.current.name] = value
65
+ end
66
+ end
67
+
68
+ INSTANCE = new
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../type'
4
+ require_relative '../aux/hash_tuple'
5
+
6
+ module AMA
7
+ module Entity
8
+ class Mapper
9
+ class Type
10
+ module BuiltIn
11
+ # Pair class definition
12
+ class HashTupleType < Type
13
+ def initialize
14
+ super(Aux::HashTuple, virtual: true)
15
+
16
+ attribute!(:key, parameter!(:K))
17
+ attribute!(:value, parameter!(:V))
18
+
19
+ enumerator_block do |entity, type, *|
20
+ ::Enumerator.new do |yielder|
21
+ yielder << [type.attributes[:key], entity.key, nil]
22
+ yielder << [type.attributes[:value], entity.value, nil]
23
+ end
24
+ end
25
+ end
26
+
27
+ INSTANCE = new
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../type'
4
+ require_relative '../../path/segment'
5
+ require_relative '../../mixin/errors'
6
+ require_relative '../../mixin/reflection'
7
+ require_relative 'hash_tuple_type'
8
+ require_relative '../aux/hash_tuple'
9
+
10
+ module AMA
11
+ module Entity
12
+ class Mapper
13
+ class Type
14
+ module BuiltIn
15
+ # Predefined type for Hash class
16
+ class HashType < Type
17
+ include Mixin::Errors
18
+ extend Mixin::Errors
19
+
20
+ def initialize
21
+ super(::Hash)
22
+ define_attribute
23
+ define_enumerator
24
+ define_injector
25
+ define_normalizer
26
+ define_denormalizer
27
+ end
28
+
29
+ private
30
+
31
+ def define_attribute
32
+ type = HashTupleType.new
33
+ type = type.resolve(
34
+ type.parameter!(:K) => parameter!(:K),
35
+ type.parameter!(:V) => parameter!(:V)
36
+ )
37
+ attribute!(:_tuple, type, virtual: true)
38
+ end
39
+
40
+ def define_enumerator
41
+ enumerator_block do |entity, type, *|
42
+ ::Enumerator.new do |yielder|
43
+ entity.each do |key, value|
44
+ tuple = Aux::HashTuple.new(key: key, value: value)
45
+ attribute = type.attributes[:_tuple]
46
+ yielder << [attribute, tuple, Path::Segment.index(key)]
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def define_injector
53
+ injector_block do |entity, _, _, tuple, *|
54
+ entity[tuple.key] = tuple.value
55
+ end
56
+ end
57
+
58
+ def define_denormalizer
59
+ denormalizer_block do |input, type, context = nil, *|
60
+ input = input.to_h if input.respond_to?(:to_h)
61
+ break input if input.is_a?(Hash)
62
+ message = "Expected to receive hash, #{input.class} received"
63
+ type.mapping_error(message, context: context)
64
+ end
65
+ end
66
+
67
+ def define_normalizer
68
+ normalizer_block do |entity, *|
69
+ intermediate = entity.map do |key, value|
70
+ [key, value]
71
+ end
72
+ Hash[intermediate]
73
+ end
74
+ end
75
+
76
+ INSTANCE = new
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../type'
4
+ require_relative '../../mixin/errors'
5
+ require_relative 'primitive_type/denormalizer'
6
+
7
+ module AMA
8
+ module Entity
9
+ class Mapper
10
+ class Type
11
+ module BuiltIn
12
+ # Predefined type for Set class
13
+ class PrimitiveType < Type
14
+ include Mixin::Errors
15
+
16
+ def initialize(type, method_map)
17
+ super(type)
18
+ this = self
19
+
20
+ factory_block do |*|
21
+ this.compliance_error("#{this} factory should never be called")
22
+ end
23
+ normalizer_block do |entity, *|
24
+ entity
25
+ end
26
+ self.denormalizer = Denormalizer.new(method_map)
27
+ enumerator_block do |*|
28
+ Enumerator.new { |*| }
29
+ end
30
+ injector_block { |*| }
31
+ end
32
+
33
+ # This hash describes which helper methods may be used for which
34
+ # type to extract target primitive. During the run inheritance chain
35
+ # is unwrapped, and first matching entry (topmost one) is used.
36
+ primitives = {
37
+ Symbol => { Object => [], String => %i[to_sym] },
38
+ String => { Object => [], Symbol => %i[to_s] },
39
+ Numeric => { Object => %i[to_i to_f], String => [] },
40
+ Integer => { Object => %i[to_i], String => [] },
41
+ Float => { Object => %i[to_f], String => [] },
42
+ TrueClass => { Object => %i[to_b to_bool], String => [] },
43
+ FalseClass => { Object => %i[to_b to_bool], String => [] },
44
+ NilClass => { Object => [] }
45
+ }
46
+
47
+ # rubocop:disable Lint/UnifiedInteger
48
+ if defined?(Fixnum)
49
+ primitives[Fixnum] = { Object => %i[to_i], String => [] }
50
+ end
51
+ # rubocop:enable Lint/UnifiedInteger
52
+
53
+ ALL = primitives.map do |klass, method_map|
54
+ const_set(klass.to_s.upcase, new(klass, method_map))
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../type'
4
+ require_relative '../../../mixin/errors'
5
+
6
+ module AMA
7
+ module Entity
8
+ class Mapper
9
+ class Type
10
+ module BuiltIn
11
+ class PrimitiveType < Type
12
+ # Standard denormalizer for primitive type
13
+ class Denormalizer
14
+ include Mixin::Errors
15
+
16
+ # @param [Hash{Class, Array<Symbol>}] method_map
17
+ def initialize(method_map)
18
+ @method_map = method_map
19
+ end
20
+
21
+ # @param [Object] source
22
+ # @param [AMA::Entity::Mapper::Type] type
23
+ # @param [AMA::Entity::Mapper::Context] context
24
+ def denormalize(source, type, context)
25
+ return source if type.valid?(source, context)
26
+ find_candidate_methods(source.class).each do |candidate|
27
+ begin
28
+ next unless source.respond_to?(candidate)
29
+ value = source.send(candidate)
30
+ return value if type.valid?(value, context)
31
+ rescue StandardError => e
32
+ message = "Method #{candidate} failed with error when " \
33
+ "denormalizing #{type.type} out of #{source.class}: " \
34
+ "#{e.message}"
35
+ context.logger.warn(message)
36
+ end
37
+ end
38
+ message = "Can't create #{type} instance from #{source.class}"
39
+ mapping_error(message, context: context)
40
+ end
41
+
42
+ private
43
+
44
+ def find_candidate_methods(klass)
45
+ chain = []
46
+ cursor = klass
47
+ until cursor.nil?
48
+ chain.push(cursor)
49
+ cursor = cursor.superclass
50
+ end
51
+ winner = chain.find do |entry|
52
+ @method_map.key?(entry)
53
+ end
54
+ winner.nil? ? [] : @method_map[winner]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end