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,59 @@
|
|
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
|
+
# Rational type description
|
12
|
+
class RationalType < Type
|
13
|
+
def initialize
|
14
|
+
super(Rational)
|
15
|
+
|
16
|
+
normalizer_block do |entity, *|
|
17
|
+
entity.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
define_denormalizer
|
21
|
+
define_factory
|
22
|
+
|
23
|
+
enumerator_block do |*|
|
24
|
+
::Enumerator.new { |*| }
|
25
|
+
end
|
26
|
+
|
27
|
+
injector_block { |*| }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def define_denormalizer
|
33
|
+
denormalizer_block do |input, _, ctx|
|
34
|
+
break input if input.is_a?(Rational)
|
35
|
+
input = input.to_s if input.is_a?(Symbol)
|
36
|
+
break Rational(input) if input.is_a?(String)
|
37
|
+
singleton_class.send(:include, Mixin::Errors)
|
38
|
+
message = "String input expected (like '2.3'), " \
|
39
|
+
"#{input.class} received: #{input}"
|
40
|
+
mapping_error(message, context: ctx)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def define_factory
|
45
|
+
factory_block do |_, _, ctx|
|
46
|
+
singleton_class.send(:include, Mixin::Errors)
|
47
|
+
message = 'Rational type could not be instantiated directly, ' \
|
48
|
+
'it only supports normalization and denormalization'
|
49
|
+
compliance_error(message, context: ctx)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
INSTANCE = new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require_relative '../../type'
|
6
|
+
require_relative '../../path/segment'
|
7
|
+
require_relative '../../mixin/errors'
|
8
|
+
|
9
|
+
module AMA
|
10
|
+
module Entity
|
11
|
+
class Mapper
|
12
|
+
class Type
|
13
|
+
module BuiltIn
|
14
|
+
# Predefined type for Set class
|
15
|
+
class SetType < Type
|
16
|
+
def initialize
|
17
|
+
super(::Set)
|
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
|
+
Set.new([])
|
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 Set from #{data.class}"
|
45
|
+
type.mapping_error(message, context: context)
|
46
|
+
end
|
47
|
+
Set.new(data)
|
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, *|
|
64
|
+
entity.add(value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
INSTANCE = new
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../mixin/errors'
|
4
|
+
|
5
|
+
module AMA
|
6
|
+
module Entity
|
7
|
+
class Mapper
|
8
|
+
class Type
|
9
|
+
# This class represents parameter type - an unknown-until-runtime type
|
10
|
+
# that belongs to particular other type. For example,
|
11
|
+
# Hash<Symbol, Integer> may be described as Type(Hash) with
|
12
|
+
# parameters _key: Symbol and _value: Integer
|
13
|
+
class Parameter
|
14
|
+
include Mixin::Errors
|
15
|
+
|
16
|
+
# @!attribute type
|
17
|
+
# @return [AMA::Entity::Mapper::Type]
|
18
|
+
attr_reader :owner
|
19
|
+
# @!attribute id
|
20
|
+
# @return [Symbol]
|
21
|
+
attr_reader :id
|
22
|
+
|
23
|
+
# @param [AMA::Entity::Mapper::Type] owner
|
24
|
+
# @param [Symbol] id
|
25
|
+
def initialize(owner, id)
|
26
|
+
@owner = owner
|
27
|
+
@id = id
|
28
|
+
end
|
29
|
+
|
30
|
+
def instance?(_)
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def resolve_parameter(*)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def resolved?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolved!(context = nil)
|
43
|
+
compliance_error("Type #{self} is not resolved", context: context)
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
"Parameter #{owner.type}.#{id}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_def
|
51
|
+
"#{owner.type}.#{id}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def hash
|
55
|
+
@owner.hash ^ @id.hash
|
56
|
+
end
|
57
|
+
|
58
|
+
def eql?(other)
|
59
|
+
return false unless other.is_a?(self.class)
|
60
|
+
@id == other.id && @owner == other.owner
|
61
|
+
end
|
62
|
+
|
63
|
+
def ==(other)
|
64
|
+
eql?(other)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_String_literal: true
|
2
|
+
|
3
|
+
require_relative '../mixin/errors'
|
4
|
+
require_relative 'parameter'
|
5
|
+
require_relative 'builtin/enumerable_type'
|
6
|
+
require_relative 'builtin/array_type'
|
7
|
+
require_relative 'builtin/hash_type'
|
8
|
+
require_relative 'builtin/hash_tuple_type'
|
9
|
+
require_relative 'builtin/set_type'
|
10
|
+
require_relative 'builtin/primitive_type'
|
11
|
+
require_relative 'builtin/rational_type'
|
12
|
+
require_relative 'builtin/datetime_type'
|
13
|
+
|
14
|
+
module AMA
|
15
|
+
module Entity
|
16
|
+
class Mapper
|
17
|
+
class Type
|
18
|
+
# Holds all registered types
|
19
|
+
class Registry
|
20
|
+
include Mixin::Errors
|
21
|
+
|
22
|
+
attr_accessor :types
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@types = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [AMA::Entity::Mapper::Type::Registry]
|
29
|
+
def with_default_types
|
30
|
+
register(BuiltIn::EnumerableType::INSTANCE)
|
31
|
+
register(BuiltIn::ArrayType::INSTANCE)
|
32
|
+
register(BuiltIn::HashType::INSTANCE)
|
33
|
+
register(BuiltIn::SetType::INSTANCE)
|
34
|
+
register(BuiltIn::HashTupleType::INSTANCE)
|
35
|
+
register(BuiltIn::RationalType::INSTANCE)
|
36
|
+
register(BuiltIn::DateTimeType::INSTANCE)
|
37
|
+
BuiltIn::PrimitiveType::ALL.each do |type|
|
38
|
+
register(type)
|
39
|
+
end
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Class, Module] klass
|
44
|
+
def [](klass)
|
45
|
+
@types[klass]
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [AMA::Entity::Mapper::Type] type
|
49
|
+
def register(type)
|
50
|
+
@types[type.type] = type
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Class] klass
|
54
|
+
def key?(klass)
|
55
|
+
@types.key?(klass)
|
56
|
+
end
|
57
|
+
|
58
|
+
alias registered? key?
|
59
|
+
|
60
|
+
# @param [Class, Module] klass
|
61
|
+
# @return [Array<AMA::Entity::Mapper::Type>]
|
62
|
+
def select(klass)
|
63
|
+
types = class_hierarchy(klass).map do |entry|
|
64
|
+
@types[entry]
|
65
|
+
end
|
66
|
+
types.reject(&:nil?)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param [Class, Module] klass
|
70
|
+
# @return [AMA::Entity::Mapper::Type, NilClass]
|
71
|
+
def find(klass)
|
72
|
+
candidates = select(klass)
|
73
|
+
candidates.empty? ? nil : candidates.first
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [Class, Module] klass
|
77
|
+
# @return [AMA::Entity::Mapper::Type]
|
78
|
+
def find!(klass)
|
79
|
+
candidate = find(klass)
|
80
|
+
return candidate if candidate
|
81
|
+
message = "Could not find any registered type for class #{klass}"
|
82
|
+
compliance_error(message)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [Class, Module] klass
|
86
|
+
# @return [TrueClass, FalseClass]
|
87
|
+
def resolvable?(klass)
|
88
|
+
!select(klass).empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# @param [Class, Module] klass
|
94
|
+
def class_hierarchy(klass)
|
95
|
+
ptr = klass
|
96
|
+
chain = []
|
97
|
+
loop do
|
98
|
+
chain.push(*class_with_modules(ptr))
|
99
|
+
break if !ptr.respond_to?(:superclass) || ptr.superclass.nil?
|
100
|
+
ptr = ptr.superclass
|
101
|
+
end
|
102
|
+
chain
|
103
|
+
end
|
104
|
+
|
105
|
+
def class_with_modules(klass)
|
106
|
+
if klass.superclass.nil?
|
107
|
+
parent_modules = []
|
108
|
+
else
|
109
|
+
parent_modules = klass.superclass.included_modules
|
110
|
+
end
|
111
|
+
[klass, *(klass.included_modules - parent_modules)]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../mixin/errors'
|
4
|
+
require_relative '../type'
|
5
|
+
require_relative 'parameter'
|
6
|
+
|
7
|
+
module AMA
|
8
|
+
module Entity
|
9
|
+
class Mapper
|
10
|
+
class Type
|
11
|
+
# This class is responsible for resolution of simple type definitions,
|
12
|
+
# converting definitions like
|
13
|
+
# [Array, T: [NilClass, [Hash, K: Symbol, V: Integer]]]
|
14
|
+
# into real type hierarchy
|
15
|
+
class Resolver
|
16
|
+
include Mixin::Errors
|
17
|
+
|
18
|
+
# @param [Registry] registry
|
19
|
+
def initialize(registry)
|
20
|
+
@registry = registry
|
21
|
+
end
|
22
|
+
|
23
|
+
def resolve(definition)
|
24
|
+
definition = [definition] unless definition.is_a?(Enumerable)
|
25
|
+
resolve_definition(definition)
|
26
|
+
rescue StandardError => parent
|
27
|
+
message = "Definition #{definition} resolution resulted " \
|
28
|
+
"in error: #{parent}"
|
29
|
+
compliance_error(message)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def resolve_definitions(definitions)
|
35
|
+
definitions = [definitions] unless definitions.is_a?(Array)
|
36
|
+
if definitions.size == 2 && definitions.last.is_a?(Hash)
|
37
|
+
definitions = [definitions]
|
38
|
+
end
|
39
|
+
definitions.map do |definition|
|
40
|
+
resolve_definition(definition)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def resolve_definition(definition)
|
45
|
+
definition = [definition] unless definition.is_a?(Array)
|
46
|
+
type = definition.first
|
47
|
+
parameters = definition[1] || {}
|
48
|
+
resolve_type(type, parameters)
|
49
|
+
rescue StandardError => e
|
50
|
+
message = "Unexpected error during definition #{definition} " \
|
51
|
+
"resolution: #{e.message}"
|
52
|
+
compliance_error(message)
|
53
|
+
end
|
54
|
+
|
55
|
+
def resolve_type(type, parameters)
|
56
|
+
type = find_type(type)
|
57
|
+
unless parameters.is_a?(Hash)
|
58
|
+
message = "Type parameters were passed not as hash: #{parameters}"
|
59
|
+
compliance_error(message)
|
60
|
+
end
|
61
|
+
parameters.each do |parameter, replacements|
|
62
|
+
parameter = resolve_type_parameter(type, parameter)
|
63
|
+
replacements = resolve_definitions(replacements)
|
64
|
+
type = type.resolve_parameter(parameter, replacements)
|
65
|
+
end
|
66
|
+
type
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_type(type)
|
70
|
+
return type if type.is_a?(Type)
|
71
|
+
return Type::Any::INSTANCE if [:*, '*'].include?(type)
|
72
|
+
if type.is_a?(Class) || type.is_a?(Module)
|
73
|
+
return @registry[type] || Type::Analyzer.analyze(type)
|
74
|
+
end
|
75
|
+
message = 'Invalid type provided for resolution, expected Type, ' \
|
76
|
+
"Class or Module: #{type}"
|
77
|
+
compliance_error(message)
|
78
|
+
end
|
79
|
+
|
80
|
+
def resolve_type_parameter(type, parameter)
|
81
|
+
unless parameter.is_a?(Parameter)
|
82
|
+
parameter = find_parameter(type, parameter)
|
83
|
+
end
|
84
|
+
return parameter if parameter.owner.type == type.type
|
85
|
+
message = "Parameter #{parameter} belongs to different type " \
|
86
|
+
'rather one it is resolved against'
|
87
|
+
compliance_error(message)
|
88
|
+
end
|
89
|
+
|
90
|
+
def find_parameter(type, parameter)
|
91
|
+
parameter = parameter.to_sym if parameter.respond_to?(:to_sym)
|
92
|
+
unless parameter.is_a?(Symbol)
|
93
|
+
message = "#{parameter} is not a valid parameter identifier " \
|
94
|
+
'(Symbol expected)'
|
95
|
+
compliance_error(message)
|
96
|
+
end
|
97
|
+
return type.parameters[parameter] if type.parameters.key?(parameter)
|
98
|
+
message = "Type #{type} has no requested parameter #{parameter}"
|
99
|
+
compliance_error(message)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AMA
|
4
|
+
module Entity
|
5
|
+
class Mapper
|
6
|
+
class Version
|
7
|
+
MAJOR = 0
|
8
|
+
MINOR = 1
|
9
|
+
PATCH = 0
|
10
|
+
CLASSIFIER = 'beta'.freeze
|
11
|
+
PRERELEASE_NUMBER = 2
|
12
|
+
CHUNKS = [MAJOR, MINOR, PATCH, CLASSIFIER, PRERELEASE_NUMBER].freeze
|
13
|
+
VERSION = CHUNKS.reject(&:nil?).join('.').freeze
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|