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