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.
- checksums.yaml +7 -0
- data/docs/algorithm.md +61 -0
- data/docs/basic-usage.md +179 -0
- data/docs/dsl.md +90 -0
- data/docs/generics.md +55 -0
- data/docs/handlers.md +196 -0
- data/docs/index.md +23 -0
- data/docs/installation.md +27 -0
- data/docs/logging.md +18 -0
- data/docs/wildcards.md +16 -0
- data/lib/ama-entity-mapper.rb +42 -0
- data/lib/ama-entity-mapper/aux/null_stream.rb +30 -0
- data/lib/ama-entity-mapper/context.rb +61 -0
- data/lib/ama-entity-mapper/dsl.rb +21 -0
- data/lib/ama-entity-mapper/dsl/class_methods.rb +100 -0
- data/lib/ama-entity-mapper/engine.rb +88 -0
- data/lib/ama-entity-mapper/engine/recursive_mapper.rb +164 -0
- data/lib/ama-entity-mapper/engine/recursive_normalizer.rb +74 -0
- data/lib/ama-entity-mapper/error.rb +11 -0
- data/lib/ama-entity-mapper/error/compliance_error.rb +15 -0
- data/lib/ama-entity-mapper/error/mapping_error.rb +14 -0
- data/lib/ama-entity-mapper/error/validation_error.rb +14 -0
- data/lib/ama-entity-mapper/handler/attribute/validator.rb +107 -0
- data/lib/ama-entity-mapper/handler/entity/denormalizer.rb +97 -0
- data/lib/ama-entity-mapper/handler/entity/enumerator.rb +76 -0
- data/lib/ama-entity-mapper/handler/entity/factory.rb +86 -0
- data/lib/ama-entity-mapper/handler/entity/injector.rb +69 -0
- data/lib/ama-entity-mapper/handler/entity/normalizer.rb +68 -0
- data/lib/ama-entity-mapper/handler/entity/validator.rb +66 -0
- data/lib/ama-entity-mapper/mixin/errors.rb +55 -0
- data/lib/ama-entity-mapper/mixin/handler_support.rb +69 -0
- data/lib/ama-entity-mapper/mixin/reflection.rb +67 -0
- data/lib/ama-entity-mapper/mixin/suppression_support.rb +37 -0
- data/lib/ama-entity-mapper/path.rb +91 -0
- data/lib/ama-entity-mapper/path/segment.rb +51 -0
- data/lib/ama-entity-mapper/type.rb +243 -0
- data/lib/ama-entity-mapper/type/analyzer.rb +27 -0
- data/lib/ama-entity-mapper/type/any.rb +66 -0
- data/lib/ama-entity-mapper/type/attribute.rb +197 -0
- data/lib/ama-entity-mapper/type/aux/hash_tuple.rb +35 -0
- data/lib/ama-entity-mapper/type/builtin/array_type.rb +28 -0
- data/lib/ama-entity-mapper/type/builtin/datetime_type.rb +65 -0
- data/lib/ama-entity-mapper/type/builtin/enumerable_type.rb +74 -0
- data/lib/ama-entity-mapper/type/builtin/hash_tuple_type.rb +33 -0
- data/lib/ama-entity-mapper/type/builtin/hash_type.rb +82 -0
- data/lib/ama-entity-mapper/type/builtin/primitive_type.rb +61 -0
- data/lib/ama-entity-mapper/type/builtin/primitive_type/denormalizer.rb +62 -0
- data/lib/ama-entity-mapper/type/builtin/rational_type.rb +59 -0
- data/lib/ama-entity-mapper/type/builtin/set_type.rb +74 -0
- data/lib/ama-entity-mapper/type/parameter.rb +70 -0
- data/lib/ama-entity-mapper/type/registry.rb +117 -0
- data/lib/ama-entity-mapper/type/resolver.rb +105 -0
- data/lib/ama-entity-mapper/version.rb +17 -0
- 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
|