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,69 @@
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 attribute injector
12
+ class Injector
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::Type::Attribute] attribute
20
+ # @param [Object] value
21
+ # @param [AMA::Entity::Mapper::Context] _context
22
+ def inject(entity, _type, attribute, value, _context = nil)
23
+ return entity if attribute.virtual
24
+ set_object_attribute(entity, attribute.name, value)
25
+ entity
26
+ end
27
+
28
+ class << self
29
+ include Mixin::Reflection
30
+
31
+ # @param [Injector] implementation
32
+ # @return [Injector]
33
+ def wrap(implementation)
34
+ handler = handler_factory(implementation, INSTANCE)
35
+ description = "Safety wrapper for #{implementation}"
36
+ wrapper = method_object(:inject, to_s: description, &handler)
37
+ wrapper.singleton_class.instance_eval do
38
+ include Mixin::Errors
39
+ end
40
+ wrapper
41
+ end
42
+
43
+ private
44
+
45
+ # @param [Injector] impl
46
+ # @param [Injector] fallback
47
+ # @return [Injector]
48
+ def handler_factory(impl, fallback)
49
+ lambda do |entity, type, attr, val, ctx|
50
+ begin
51
+ impl.inject(entity, type, attr, val, ctx) do |e, t, a, v, c|
52
+ fallback.inject(e, t, a, v, c)
53
+ end
54
+ rescue StandardError => e
55
+ raise_if_internal(e)
56
+ message = "Unexpected error from injector #{impl}"
57
+ signature = '(entity, type, attr, val, ctx)'
58
+ options = { parent: e, context: ctx, signature: signature }
59
+ compliance_error(message, options)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,68 @@
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 normalization handler
12
+ class Normalizer
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 normalize(entity, type, _context = nil)
21
+ type.attributes.values.each_with_object({}) do |attribute, data|
22
+ next if attribute.virtual || attribute.sensitive
23
+ data[attribute.name] = object_variable(entity, attribute.name)
24
+ end
25
+ end
26
+
27
+ class << self
28
+ include Mixin::Reflection
29
+
30
+ # @param [Normalizer] implementation
31
+ # @return [Normalizer]
32
+ def wrap(implementation)
33
+ handler = handler_factory(implementation, INSTANCE)
34
+ description = "Safety wrapper for #{implementation}"
35
+ wrapper = method_object(:normalize, to_s: description, &handler)
36
+ wrapper.singleton_class.instance_eval do
37
+ include Mixin::Errors
38
+ end
39
+ wrapper
40
+ end
41
+
42
+ private
43
+
44
+ # @param [Normalizer] impl
45
+ # @param [Normalizer] fallback
46
+ # @return [Proc]
47
+ def handler_factory(impl, fallback)
48
+ lambda do |entity, type, ctx|
49
+ begin
50
+ impl.normalize(entity, type, ctx) do |e, t, c|
51
+ fallback.normalize(e, t, c)
52
+ end
53
+ rescue StandardError => e
54
+ raise_if_internal(e)
55
+ message = "Unexpected error from normalizer #{impl}"
56
+ signature = '(entity, type, context)'
57
+ options = { parent: e, context: ctx, signature: signature }
58
+ compliance_error(message, **options)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,66 @@
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 entity validator
12
+ class Validator
13
+ INSTANCE = new
14
+
15
+ # @param [Object] entity
16
+ # @param [Mapper::Type] type
17
+ # @param [Mapper::Context] _context
18
+ # @return [Array<Array<Attribute, String, Segment>] List of
19
+ # violations, combined with attribute and segment
20
+ def validate(entity, type, _context)
21
+ return [] if type.instance?(entity)
22
+ ["Provided object is not an instance of #{type.type}"]
23
+ end
24
+
25
+ class << self
26
+ include Mixin::Reflection
27
+
28
+ # @param [Validator] validator
29
+ # @return [Validator]
30
+ def wrap(validator)
31
+ handler = handler_factory(validator, INSTANCE)
32
+ description = "Safety wrapper for #{validator}"
33
+ wrapper = method_object(:validate, to_s: description, &handler)
34
+ wrapper.singleton_class.instance_eval do
35
+ include Mixin::Errors
36
+ end
37
+ wrapper
38
+ end
39
+
40
+ private
41
+
42
+ # @param [Validator] validator
43
+ # @param [Validator] fallback
44
+ # @return [Proc]
45
+ def handler_factory(validator, fallback)
46
+ lambda do |entity, type, ctx|
47
+ begin
48
+ validator.validate(entity, type, ctx) do |e, t, c|
49
+ fallback.validate(e, t, c)
50
+ end
51
+ rescue StandardError => e
52
+ raise_if_internal(e)
53
+ message = "Unexpected error from validator #{validator}"
54
+ signature = '(entity, type, context)'
55
+ options = { parent: e, context: ctx, signature: signature }
56
+ compliance_error(message, **options)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+ require_relative '../error/mapping_error'
5
+ require_relative '../error/compliance_error'
6
+ require_relative '../error/validation_error'
7
+
8
+ module AMA
9
+ module Entity
10
+ class Mapper
11
+ module Mixin
12
+ # Simple mixin that provides shortcuts for raising common errors
13
+ module Errors
14
+ error_types = %i[Mapping Compliance Validation]
15
+ error_namespace = ::AMA::Entity::Mapper::Error
16
+
17
+ # @!method mapping_error(message, **options)
18
+ # @param [String] message
19
+
20
+ # @!method compliance_error(message, **options)
21
+ # @param [String] message
22
+
23
+ # @!method validation_error(message, **options)
24
+ # @param [String] message
25
+ error_types.each do |type|
26
+ method = "#{type.to_s.downcase}_error"
27
+ error_class = error_namespace.const_get("#{type}Error")
28
+ define_method method do |message, **options|
29
+ parent_error = options[:parent]
30
+ unless parent_error.nil?
31
+ if options[:signature] && parent_error.is_a?(ArgumentError)
32
+ message += '.' if /\w$/ =~ message
33
+ message += ' Does called method have signature ' \
34
+ "#{options[:signature]}?"
35
+ end
36
+ message += '.' if /\w$/ =~ message
37
+ message += " Parent error: #{parent_error.message}"
38
+ end
39
+ if options[:context]
40
+ message += " (path: #{options[:context].path})"
41
+ end
42
+ raise error_class, message
43
+ end
44
+ end
45
+
46
+ # Raises error again if this is an internal error
47
+ # @param [Exception] e
48
+ def raise_if_internal(e)
49
+ raise e if e.is_a?(Mapper::Error)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'reflection'
4
+
5
+ module AMA
6
+ module Entity
7
+ class Mapper
8
+ module Mixin
9
+ # This module provides Type and Attribute classes with shortcut
10
+ # handler :name, :method method to register handlers
11
+ module HandlerSupport
12
+ class << self
13
+ def included(klass)
14
+ declare_namespace_method(klass)
15
+ declare_handler_method(klass)
16
+ end
17
+
18
+ def declare_namespace_method(klass)
19
+ klass.define_singleton_method(:handler_namespace) do |namespace|
20
+ @handler_namespace = namespace
21
+ end
22
+ end
23
+
24
+ def declare_handler_method(klass)
25
+ processor = self
26
+ klass.define_singleton_method(:handler) do |key, method|
27
+ handler_name = key.capitalize
28
+ handler_class = @handler_namespace.const_get(handler_name)
29
+ processor.declare_handler_getter(klass, key, handler_class)
30
+ processor.declare_handler_setter(klass, key, handler_class)
31
+ processor.declare_handler_block_setter(klass, key, method)
32
+ end
33
+ end
34
+
35
+ def declare_handler_getter(klass, handler_key, handler_class)
36
+ instance = handler_class::INSTANCE
37
+ klass.instance_eval do
38
+ define_method(handler_key) do
39
+ instance_variable_get("@#{handler_key}") || instance
40
+ end
41
+ end
42
+ end
43
+
44
+ def declare_handler_setter(klass, handler_key, handler_class)
45
+ klass.instance_eval do
46
+ define_method("#{handler_key}=") do |handler|
47
+ unless handler.class == handler_class
48
+ handler = handler_class.wrap(handler)
49
+ end
50
+ instance_variable_set("@#{handler_key}", handler)
51
+ self
52
+ end
53
+ end
54
+ end
55
+
56
+ def declare_handler_block_setter(klass, handler_key, method)
57
+ klass.instance_eval do
58
+ include Mixin::Reflection
59
+ define_method("#{handler_key}_block") do |&block|
60
+ send("#{handler_key}=", method_object(method, &block))
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ module AMA
6
+ module Entity
7
+ class Mapper
8
+ module Mixin
9
+ # Collection of common methods twiddling with object internals
10
+ module Reflection
11
+ include Errors
12
+
13
+ # @param [Object] object
14
+ # @param [String, Symbol] name
15
+ # @param [Object] value
16
+ def set_object_attribute(object, name, value)
17
+ method = "#{name}="
18
+ return object.send(method, value) if object.respond_to?(method)
19
+ object.instance_variable_set("@#{name}", value)
20
+ rescue StandardError => e
21
+ message = "Failed to set attribute #{name} on #{object.class}, " \
22
+ "this is most likely due to `#{method}` method not following " \
23
+ 'accessor conventions'
24
+ mapping_error(message, parent: e)
25
+ end
26
+
27
+ # @param [Object] object
28
+ def object_variables(object)
29
+ intermediate = object.instance_variables.map do |variable|
30
+ [variable[1..-1].to_sym, object.instance_variable_get(variable)]
31
+ end
32
+ Hash[intermediate]
33
+ end
34
+
35
+ # @param [Object] object
36
+ # @param [String, Symbol] name
37
+ def object_variable(object, name)
38
+ name = "@#{name}" unless name[0] == '@'
39
+ object.instance_variable_get(name)
40
+ end
41
+
42
+ def object_variable_exists(object, name)
43
+ object.instance_variables.include?("@#{name}".to_sym)
44
+ end
45
+
46
+ def install_object_method(object, name, handler)
47
+ compliance_error('Handler not provided') unless handler
48
+ object.define_singleton_method(name, &handler)
49
+ object
50
+ end
51
+
52
+ def method_object(method, to_s: nil, &handler)
53
+ object = install_object_method(Object.new, method, handler)
54
+ unless to_s
55
+ to_s = "Wrapper object for proc #{handler} " \
56
+ "(installed as method :#{method})"
57
+ end
58
+ object.define_singleton_method(:to_s) do
59
+ to_s
60
+ end
61
+ object
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ module AMA
6
+ module Entity
7
+ class Mapper
8
+ module Mixin
9
+ # Special module with method for playing with error suppression
10
+ module SuppressionSupport
11
+ include Errors
12
+
13
+ # Enumerates elements in enumerator, applying block to each one,
14
+ # returning result or suppressing specified error. If no element
15
+ # has succeeded, raises last error.
16
+ #
17
+ # @param [Enumerator] enumerator
18
+ # @param [Class<? extends StandardError>] error
19
+ # @param [AMA::Entity::Mapper::Context] ctx
20
+ def successful(enumerator, error = StandardError, ctx = nil)
21
+ suppressed = []
22
+ enumerator.each do |*args|
23
+ begin
24
+ return yield(*args)
25
+ rescue error => e
26
+ ctx.logger.debug("#{e.class} raised: #{e.message}") if ctx
27
+ suppressed.push(e)
28
+ end
29
+ end
30
+ compliance_error('Empty enumerator passed') if suppressed.empty?
31
+ raise suppressed.last
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'path/segment'
4
+
5
+ module AMA
6
+ module Entity
7
+ class Mapper
8
+ # Wrapper for simple array. Helps to understand where exactly processing
9
+ # is taking place.
10
+ class Path
11
+ attr_reader :segments
12
+
13
+ def initialize(stack = [])
14
+ @segments = stack
15
+ end
16
+
17
+ def empty?
18
+ @segments.empty?
19
+ end
20
+
21
+ # @param [String, Symbol, Integer] name
22
+ # @return [AMA::Entity::Mapper::Path]
23
+ def index(name)
24
+ push(Segment.index(name))
25
+ end
26
+
27
+ # @param [String, Symbol] name
28
+ # @return [AMA::Entity::Mapper::Path]
29
+ def attribute(name)
30
+ push(Segment.attribute(name))
31
+ end
32
+
33
+ # @param [Array<AMA::Entity::Mapper::Path::Segment>] segments
34
+ # @return [AMA::Entity::Mapper::Path]
35
+ def push(*segments)
36
+ segments = segments.map do |segment|
37
+ next segment if segment.is_a?(Segment)
38
+ Segment.attribute(segment)
39
+ end
40
+ self.class.new(@segments + segments)
41
+ end
42
+
43
+ # @return [AMA::Entity::Mapper::Path]
44
+ def pop
45
+ self.class.new(@segments[0..-2])
46
+ end
47
+
48
+ # @return [AMA::Entity::Mapper::Path::Segment]
49
+ def current
50
+ @segments.last
51
+ end
52
+
53
+ def each
54
+ @segments.each do |item|
55
+ yield(item)
56
+ end
57
+ end
58
+
59
+ def reduce(carrier)
60
+ @segments.reduce(carrier) do |inner_carrier, item|
61
+ yield(inner_carrier, item)
62
+ end
63
+ end
64
+
65
+ # @param [AMA::Entity::Mapper::Path] path
66
+ # @return [AMA::Entity::Mapper::Path]
67
+ def merge(path)
68
+ push(*path.segments)
69
+ end
70
+
71
+ def size
72
+ @segments.size
73
+ end
74
+
75
+ def segments
76
+ @segments.clone
77
+ end
78
+
79
+ # @return [Array<AMA::Entity::Mapper::Path::Segment>]
80
+ def to_a
81
+ @segments.clone
82
+ end
83
+
84
+ # @return [String]
85
+ def to_s
86
+ "$#{@segments.join}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end