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