ama-entity-mapper 0.1.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
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