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.
- 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
|