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,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../type'
|
4
|
+
require_relative '../type/analyzer'
|
5
|
+
|
6
|
+
module AMA
|
7
|
+
module Entity
|
8
|
+
class Mapper
|
9
|
+
class Engine
|
10
|
+
# Helper and self-explanatory engine class
|
11
|
+
class RecursiveNormalizer
|
12
|
+
# @param [AMA::Entity::Mapper::Type::Registry] registry
|
13
|
+
def initialize(registry)
|
14
|
+
@registry = registry
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Object] entity
|
18
|
+
# @param [AMA::Entity::Mapper::Context] ctx
|
19
|
+
# @param [AMA::Entity::Mapper::Type, NilClass] type
|
20
|
+
def normalize(entity, ctx, type = nil)
|
21
|
+
type ||= find_type(entity.class)
|
22
|
+
target = entity
|
23
|
+
ctx.logger.debug("Normalizing #{entity.class} as #{type.type}")
|
24
|
+
if type.virtual
|
25
|
+
message = "Type #{type.type} is virtual, skipping to attributes"
|
26
|
+
ctx.logger.debug(message)
|
27
|
+
else
|
28
|
+
target = type.normalizer.normalize(entity, type, ctx)
|
29
|
+
end
|
30
|
+
target_type = find_type(target.class)
|
31
|
+
process_attributes(target, target_type, ctx)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @param [Object] entity
|
37
|
+
# @param [AMA::Entity::Mapper::Type] type
|
38
|
+
# @param [AMA::Entity::Mapper::Context] ctx
|
39
|
+
def process_attributes(entity, type, ctx)
|
40
|
+
if type.attributes.empty?
|
41
|
+
message = "No attributes found on #{type.type}, returning " \
|
42
|
+
"#{entity.class} as is"
|
43
|
+
ctx.logger.debug(message)
|
44
|
+
return entity
|
45
|
+
end
|
46
|
+
normalize_attributes(entity, type, ctx)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param [Object] entity
|
50
|
+
# @param [AMA::Entity::Mapper::Type] type
|
51
|
+
# @param [AMA::Entity::Mapper::Context] ctx
|
52
|
+
def normalize_attributes(entity, type, ctx)
|
53
|
+
message = "Normalizing attributes of #{entity.class} " \
|
54
|
+
"(as #{type.type})"
|
55
|
+
ctx.logger.debug(message)
|
56
|
+
enumerator = type.enumerator.enumerate(entity, type, ctx)
|
57
|
+
enumerator.each do |attribute, value, segment|
|
58
|
+
local_ctx = ctx.advance(segment)
|
59
|
+
value = normalize(value, local_ctx)
|
60
|
+
type.injector.inject(entity, type, attribute, value, local_ctx)
|
61
|
+
end
|
62
|
+
entity
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [Class, Module] klass
|
66
|
+
# @return [AMA::Entity::Mapper::Type]
|
67
|
+
def find_type(klass)
|
68
|
+
@registry.find(klass) || Type::Analyzer.analyze(klass)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AMA
|
4
|
+
module Entity
|
5
|
+
class Mapper
|
6
|
+
module Error
|
7
|
+
# This error is supposed to be thrown whenever end user provides
|
8
|
+
# malformed input - too many types, not enough types, not a type, etc.
|
9
|
+
class ComplianceError < RuntimeError
|
10
|
+
include Error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../mixin/reflection'
|
4
|
+
require_relative '../../mixin/errors'
|
5
|
+
|
6
|
+
module AMA
|
7
|
+
module Entity
|
8
|
+
class Mapper
|
9
|
+
module Handler
|
10
|
+
module Attribute
|
11
|
+
# Default validator for single attribute
|
12
|
+
class Validator
|
13
|
+
INSTANCE = new
|
14
|
+
|
15
|
+
# @param [Object] value Attribute value
|
16
|
+
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
|
17
|
+
# @param [AMA::Entity::Mapper::Context] _context
|
18
|
+
# @return [Array<String>] Single violation, list of violations
|
19
|
+
def validate(value, attribute, _context)
|
20
|
+
violation = validate_internal(value, attribute)
|
21
|
+
violation.nil? ? [] : [violation]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate_internal(value, attribute)
|
27
|
+
if illegal_nil?(value, attribute)
|
28
|
+
return "Attribute #{attribute} could not be nil"
|
29
|
+
end
|
30
|
+
if invalid_type?(value, attribute)
|
31
|
+
return "Provided value doesn't conform to " \
|
32
|
+
"any of attribute #{attribute} types " \
|
33
|
+
"(#{attribute.types.map(&:to_def).join(', ')})"
|
34
|
+
end
|
35
|
+
return unless illegal_value?(value, attribute)
|
36
|
+
"Provided value doesn't match default value (#{value})" \
|
37
|
+
" or any of allowed values (#{attribute.values})"
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Object] value Attribute value
|
41
|
+
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
|
42
|
+
# @return [TrueClass, FalseClass]
|
43
|
+
def illegal_nil?(value, attribute)
|
44
|
+
return false unless value.nil? && !attribute.nullable
|
45
|
+
attribute.types.none? { |type| type.instance?(value) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [Object] value Attribute value
|
49
|
+
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
|
50
|
+
# @return [TrueClass, FalseClass]
|
51
|
+
def invalid_type?(value, attribute)
|
52
|
+
attribute.types.all? do |type|
|
53
|
+
!type.respond_to?(:instance?) || !type.instance?(value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param [Object] value Attribute value
|
58
|
+
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
|
59
|
+
# @return [TrueClass, FalseClass]
|
60
|
+
def illegal_value?(value, attribute)
|
61
|
+
return false if value == attribute.default
|
62
|
+
return false if attribute.values.empty? || attribute.values.nil?
|
63
|
+
!attribute.values.include?(value)
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
include Mixin::Reflection
|
68
|
+
|
69
|
+
# @param [Validator] validator
|
70
|
+
# @return [Validator]
|
71
|
+
def wrap(validator)
|
72
|
+
handler = handler_factory(validator, INSTANCE)
|
73
|
+
description = "Safety wrapper for #{validator}"
|
74
|
+
wrapper = method_object(:validate, to_s: description, &handler)
|
75
|
+
wrapper.singleton_class.instance_eval do
|
76
|
+
include Mixin::Errors
|
77
|
+
end
|
78
|
+
wrapper
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# @param [Validator] validator
|
84
|
+
# @param [Validator] fallback
|
85
|
+
# @return [Proc]
|
86
|
+
def handler_factory(validator, fallback)
|
87
|
+
lambda do |val, attr, ctx|
|
88
|
+
begin
|
89
|
+
validator.validate(val, attr, ctx) do |v, a, c|
|
90
|
+
fallback.validate(v, a, c)
|
91
|
+
end
|
92
|
+
rescue StandardError => e
|
93
|
+
raise_if_internal(e)
|
94
|
+
message = "Unexpected error from validator #{validator}"
|
95
|
+
signature = '(value, attribute, context)'
|
96
|
+
options = { parent: e, context: ctx, signature: signature }
|
97
|
+
compliance_error(message, **options)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../mixin/reflection'
|
4
|
+
require_relative '../../mixin/errors'
|
5
|
+
|
6
|
+
module AMA
|
7
|
+
module Entity
|
8
|
+
class Mapper
|
9
|
+
module Handler
|
10
|
+
module Entity
|
11
|
+
# Default denormalization processor
|
12
|
+
class Denormalizer
|
13
|
+
include Mixin::Reflection
|
14
|
+
include Mixin::Errors
|
15
|
+
|
16
|
+
INSTANCE = new
|
17
|
+
|
18
|
+
# @param [Hash] source
|
19
|
+
# @param [AMA::Entity::Mapper::Type] type
|
20
|
+
# @param [AMA::Entity::Mapper::Context] context
|
21
|
+
def denormalize(source, type, context = nil)
|
22
|
+
validate_source!(source, type, context)
|
23
|
+
entity = type.factory.create(type, source, context)
|
24
|
+
type.attributes.values.each do |attribute|
|
25
|
+
next if attribute.virtual
|
26
|
+
candidate_names(attribute).each do |name|
|
27
|
+
next unless source.key?(name)
|
28
|
+
value = source[name]
|
29
|
+
break set_object_attribute(entity, attribute.name, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
entity
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# @param [AMA::Entity::Mapper::Type::Attribute] attribute
|
38
|
+
# @return [Array<Symbol, String>]
|
39
|
+
def candidate_names(attribute)
|
40
|
+
[attribute.name, *attribute.aliases].flat_map do |candidate|
|
41
|
+
[candidate, candidate.to_s]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param [Hash] source
|
46
|
+
# @param [AMA::Entity::Mapper::Type] type
|
47
|
+
# @param [AMA::Entity::Mapper::Context] context
|
48
|
+
def validate_source!(source, type, context)
|
49
|
+
return if source.is_a?(Hash)
|
50
|
+
message = "Expected Hash, #{source.class} provided " \
|
51
|
+
"(while denormalizing #{type})"
|
52
|
+
mapping_error(message, context: context)
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
include Mixin::Reflection
|
57
|
+
|
58
|
+
# @param [Denormalizer] implementation
|
59
|
+
# @return [Denormalizer]
|
60
|
+
def wrap(implementation)
|
61
|
+
handler = handler_factory(implementation, INSTANCE)
|
62
|
+
depiction = "Safety wrapper for #{implementation}"
|
63
|
+
wrapper = method_object(:denormalize, to_s: depiction, &handler)
|
64
|
+
wrapper.singleton_class.instance_eval do
|
65
|
+
include Mixin::Errors
|
66
|
+
end
|
67
|
+
wrapper
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# @param [Denormalizer] implementation
|
73
|
+
# @param [Denormalizer] fallback
|
74
|
+
# @return [Denormalizer]
|
75
|
+
def handler_factory(implementation, fallback)
|
76
|
+
lambda do |source, type, ctx|
|
77
|
+
begin
|
78
|
+
implementation.denormalize(source, type, ctx) do |s, t, c|
|
79
|
+
fallback.denormalize(s, t, c)
|
80
|
+
end
|
81
|
+
rescue StandardError => e
|
82
|
+
raise_if_internal(e)
|
83
|
+
message = 'Unexpected error from denormalizer ' \
|
84
|
+
"#{implementation}"
|
85
|
+
signature = '(source, type, context)'
|
86
|
+
options = { parent: e, context: ctx, signature: signature }
|
87
|
+
compliance_error(message, **options)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../mixin/reflection'
|
4
|
+
require_relative '../../path/segment'
|
5
|
+
|
6
|
+
module AMA
|
7
|
+
module Entity
|
8
|
+
class Mapper
|
9
|
+
module Handler
|
10
|
+
module Entity
|
11
|
+
# Default attribute enumerator
|
12
|
+
class Enumerator
|
13
|
+
include Mixin::Reflection
|
14
|
+
|
15
|
+
INSTANCE = new
|
16
|
+
|
17
|
+
# @param [Object] entity
|
18
|
+
# @param [AMA::Entity::Mapper::Type] type
|
19
|
+
# @param [AMA::Entity::Mapper::Context] _context
|
20
|
+
def enumerate(entity, type, _context)
|
21
|
+
::Enumerator.new do |yielder|
|
22
|
+
type.attributes.values.each do |attribute|
|
23
|
+
next if attribute.virtual
|
24
|
+
value = attribute.default
|
25
|
+
if object_variable_exists(entity, attribute.name)
|
26
|
+
value = object_variable(entity, attribute.name)
|
27
|
+
end
|
28
|
+
segment = Path::Segment.attribute(attribute.name)
|
29
|
+
yielder << [attribute, value, segment]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
include Mixin::Reflection
|
36
|
+
|
37
|
+
# @param [Enumerator] implementation
|
38
|
+
# @return [Enumerator]
|
39
|
+
def wrap(implementation)
|
40
|
+
handler = handler_factory(implementation, INSTANCE)
|
41
|
+
description = "Safety wrapper for #{implementation}"
|
42
|
+
wrapper = method_object(:enumerate, to_s: description, &handler)
|
43
|
+
wrapper.singleton_class.instance_eval do
|
44
|
+
include Mixin::Errors
|
45
|
+
end
|
46
|
+
wrapper
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# @param [Enumerator] implementation
|
52
|
+
# @param [Enumerator] fallback
|
53
|
+
# @return [Enumerator]
|
54
|
+
def handler_factory(implementation, fallback)
|
55
|
+
lambda do |entity, type, ctx|
|
56
|
+
begin
|
57
|
+
implementation.enumerate(entity, type, ctx) do |e, t, c|
|
58
|
+
fallback.enumerate(e, t, c)
|
59
|
+
end
|
60
|
+
rescue StandardError => e
|
61
|
+
raise_if_internal(e)
|
62
|
+
message = 'Unexpected error from enumerator ' \
|
63
|
+
"#{implementation}"
|
64
|
+
signature = '(entity, type, context)'
|
65
|
+
options = { parent: e, context: ctx, signature: signature }
|
66
|
+
compliance_error(message, **options)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../mixin/errors'
|
4
|
+
require_relative '../../mixin/reflection'
|
5
|
+
require_relative '../../path/segment'
|
6
|
+
|
7
|
+
module AMA
|
8
|
+
module Entity
|
9
|
+
class Mapper
|
10
|
+
module Handler
|
11
|
+
module Entity
|
12
|
+
# Default entity factory
|
13
|
+
class Factory
|
14
|
+
include Mixin::Errors
|
15
|
+
|
16
|
+
INSTANCE = new
|
17
|
+
|
18
|
+
# @param [AMA::Entity::Mapper::Type] type
|
19
|
+
# @param [Object] _data
|
20
|
+
# @param [AMA::Entity::Mapper::Context] context
|
21
|
+
def create(type, _data, context)
|
22
|
+
create_internal(type)
|
23
|
+
rescue StandardError => e
|
24
|
+
message = "Failed to instantiate #{type} directly from class"
|
25
|
+
if e.is_a?(ArgumentError)
|
26
|
+
message += '. Does it have parameterless #initialize() method?'
|
27
|
+
end
|
28
|
+
mapping_error(message, parent: e, context: context)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @param [AMA::Entity::Mapper::Type] type
|
34
|
+
def create_internal(type)
|
35
|
+
entity = type.type.new
|
36
|
+
type.attributes.values.each do |attribute|
|
37
|
+
next if attribute.default.nil? || attribute.virtual
|
38
|
+
segment = Path::Segment.attribute(attribute.name)
|
39
|
+
value = attribute.default
|
40
|
+
type.injector.inject(entity, type, attribute, value, segment)
|
41
|
+
end
|
42
|
+
entity
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
include Mixin::Reflection
|
47
|
+
|
48
|
+
# @param [Factory] implementation
|
49
|
+
# @return [Factory]
|
50
|
+
def wrap(implementation)
|
51
|
+
handler = handler_factory(implementation, INSTANCE)
|
52
|
+
description = "Safety wrapper for #{implementation}"
|
53
|
+
wrapper = method_object(:create, to_s: description, &handler)
|
54
|
+
wrapper.singleton_class.instance_eval do
|
55
|
+
include Mixin::Errors
|
56
|
+
end
|
57
|
+
wrapper
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# @param [Factory] implementation
|
63
|
+
# @param [Factory] fallback
|
64
|
+
# @return [Factory]
|
65
|
+
def handler_factory(implementation, fallback)
|
66
|
+
lambda do |type, data, ctx|
|
67
|
+
begin
|
68
|
+
implementation.create(type, data, ctx) do |t, d, c|
|
69
|
+
fallback.create(t, d, c)
|
70
|
+
end
|
71
|
+
rescue StandardError => e
|
72
|
+
raise_if_internal(e)
|
73
|
+
message = "Unexpected error from factory #{implementation}"
|
74
|
+
signature = '(type, data, context)'
|
75
|
+
options = { parent: e, context: ctx, signature: signature }
|
76
|
+
compliance_error(message, options)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|