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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/docs/algorithm.md +61 -0
  3. data/docs/basic-usage.md +179 -0
  4. data/docs/dsl.md +90 -0
  5. data/docs/generics.md +55 -0
  6. data/docs/handlers.md +196 -0
  7. data/docs/index.md +23 -0
  8. data/docs/installation.md +27 -0
  9. data/docs/logging.md +18 -0
  10. data/docs/wildcards.md +16 -0
  11. data/lib/ama-entity-mapper.rb +42 -0
  12. data/lib/ama-entity-mapper/aux/null_stream.rb +30 -0
  13. data/lib/ama-entity-mapper/context.rb +61 -0
  14. data/lib/ama-entity-mapper/dsl.rb +21 -0
  15. data/lib/ama-entity-mapper/dsl/class_methods.rb +100 -0
  16. data/lib/ama-entity-mapper/engine.rb +88 -0
  17. data/lib/ama-entity-mapper/engine/recursive_mapper.rb +164 -0
  18. data/lib/ama-entity-mapper/engine/recursive_normalizer.rb +74 -0
  19. data/lib/ama-entity-mapper/error.rb +11 -0
  20. data/lib/ama-entity-mapper/error/compliance_error.rb +15 -0
  21. data/lib/ama-entity-mapper/error/mapping_error.rb +14 -0
  22. data/lib/ama-entity-mapper/error/validation_error.rb +14 -0
  23. data/lib/ama-entity-mapper/handler/attribute/validator.rb +107 -0
  24. data/lib/ama-entity-mapper/handler/entity/denormalizer.rb +97 -0
  25. data/lib/ama-entity-mapper/handler/entity/enumerator.rb +76 -0
  26. data/lib/ama-entity-mapper/handler/entity/factory.rb +86 -0
  27. data/lib/ama-entity-mapper/handler/entity/injector.rb +69 -0
  28. data/lib/ama-entity-mapper/handler/entity/normalizer.rb +68 -0
  29. data/lib/ama-entity-mapper/handler/entity/validator.rb +66 -0
  30. data/lib/ama-entity-mapper/mixin/errors.rb +55 -0
  31. data/lib/ama-entity-mapper/mixin/handler_support.rb +69 -0
  32. data/lib/ama-entity-mapper/mixin/reflection.rb +67 -0
  33. data/lib/ama-entity-mapper/mixin/suppression_support.rb +37 -0
  34. data/lib/ama-entity-mapper/path.rb +91 -0
  35. data/lib/ama-entity-mapper/path/segment.rb +51 -0
  36. data/lib/ama-entity-mapper/type.rb +243 -0
  37. data/lib/ama-entity-mapper/type/analyzer.rb +27 -0
  38. data/lib/ama-entity-mapper/type/any.rb +66 -0
  39. data/lib/ama-entity-mapper/type/attribute.rb +197 -0
  40. data/lib/ama-entity-mapper/type/aux/hash_tuple.rb +35 -0
  41. data/lib/ama-entity-mapper/type/builtin/array_type.rb +28 -0
  42. data/lib/ama-entity-mapper/type/builtin/datetime_type.rb +65 -0
  43. data/lib/ama-entity-mapper/type/builtin/enumerable_type.rb +74 -0
  44. data/lib/ama-entity-mapper/type/builtin/hash_tuple_type.rb +33 -0
  45. data/lib/ama-entity-mapper/type/builtin/hash_type.rb +82 -0
  46. data/lib/ama-entity-mapper/type/builtin/primitive_type.rb +61 -0
  47. data/lib/ama-entity-mapper/type/builtin/primitive_type/denormalizer.rb +62 -0
  48. data/lib/ama-entity-mapper/type/builtin/rational_type.rb +59 -0
  49. data/lib/ama-entity-mapper/type/builtin/set_type.rb +74 -0
  50. data/lib/ama-entity-mapper/type/parameter.rb +70 -0
  51. data/lib/ama-entity-mapper/type/registry.rb +117 -0
  52. data/lib/ama-entity-mapper/type/resolver.rb +105 -0
  53. data/lib/ama-entity-mapper/version.rb +17 -0
  54. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMA
4
+ module Entity
5
+ class Mapper
6
+ # Marker module to allow easy library error processing
7
+ module Error
8
+ end
9
+ end
10
+ end
11
+ 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMA
4
+ module Entity
5
+ class Mapper
6
+ module Error
7
+ # Made to be thrown whenever mapping can't be done
8
+ class MappingError < RuntimeError
9
+ include Error
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMA
4
+ module Entity
5
+ class Mapper
6
+ module Error
7
+ # Made to be thrown if validation fails
8
+ class ValidationError < RuntimeError
9
+ include Error
10
+ end
11
+ end
12
+ end
13
+ end
14
+ 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