Completed
Push — master ( 308a2c...1aebdd )
by Fike
01:00
created

RecursiveMapper.map()   A

Complexity

Conditions 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 2
1
# frozen_string_literal: true
2
3
# rubocop:disable Metrics/ClassLength
4
5
require_relative '../mixin/suppression_support'
6
require_relative '../mixin/errors'
7
require_relative '../error'
8
require_relative '../type'
9
require_relative '../type/analyzer'
10
11
module AMA
12
  module Entity
13
    class Mapper
14
      class Engine
15
        # Recursively maps object to one of specified types
16
        class RecursiveMapper
17
          include Mixin::SuppressionSupport
18
          include Mixin::Errors
19
20
          # @param [AMA::Entity::Mapper::Type::Registry] registry
21
          def initialize(registry)
22
            @registry = registry
23
          end
24
25
          # @param [Object] source
26
          # @param [Array<AMA::Entity::Mapper::Type] types
27
          # @param [AMA::Entity::Mapper::Context] context
28
          def map(source, types, context)
29
            map_unsafe(source, types, context)
30
          rescue StandardError => e
31
            message = "Failed to map #{source.class} " \
32
              "to any of provided types (#{types.map(&:to_def).join(', ')}). " \
33
              "Last error: #{e.message} in #{e.backtrace_locations[0]}"
34
            mapping_error(message)
35
          end
36
37
          # @param [Object] source
38
          # @param [AMA::Entity::Mapper::Type] type
39
          # @param [AMA::Entity::Mapper::Context] ctx
40
          # @return [Object]
41
          def map_type(source, type, ctx)
42
            ctx.logger.debug("Mapping #{source.class} to type #{type.to_def}")
43
            source, reassembled = request_reassembly(source, type, ctx)
44
            epithet = reassembled ? 'reassembled' : 'source'
45 View Code Duplication
            if type.attributes.empty?
46
              message = "#{type.to_def} has no attributes, " \
47
                "returning #{epithet} instance"
48
              ctx.logger.debug(message)
49
              return source
50
            end
51
            process_attributes(source, type, ctx)
52
          end
53
54
          private
55
56
          # @param [Object] source
57
          # @param [Array<AMA::Entity::Mapper::Type] types
58
          # @param [AMA::Entity::Mapper::Context] context
59 View Code Duplication
          def map_unsafe(source, types, context)
60
            message = "Mapping #{source.class} into one of: " \
61
              "#{types.map(&:to_def).join(', ')}"
62
            context.logger.debug(message)
63
            successful(types, Mapper::Error, context) do |type|
64
              result = map_type(source, type, context)
65
              context.logger.debug("Validating resulting #{type.to_def}")
66
              type.valid!(result, context)
67
              result
68
            end
69
          end
70
71
          # @param [Object] source
72
          # @param [AMA::Entity::Mapper::Type] type
73
          # @param [AMA::Entity::Mapper::Context] ctx
74
          # @return [Object]
75
          def process_attributes(source, type, ctx)
76
            attributes = map_attributes(source, type, ctx)
77
            if attributes.select(&:first).empty?
78
              message = 'No changes in attributes detected, ' \
79
                "returning #{source.class}"
80
              ctx.logger.debug(message)
81
              return source
82
            end
83
            ctx.logger.debug("Creating new #{type.to_def} instance")
84
            target = type.factory.create(type, source, ctx)
85
            ctx.logger.debug("Installing #{type.to_def} attributes")
86
            install_attributes(target, type, attributes, ctx)
87
          end
88
89
          # Returns array of mapped attribute in format
90
          # [[changed?, attribute, value, attribute_context],..]
91
          # @param [Object] source
92
          # @param [AMA::Entity::Mapper::Type] type
93
          # @param [AMA::Entity::Mapper::Context] ctx
94
          # @return [Array]
95
          def map_attributes(source, type, ctx)
96
            ctx.logger.debug("Mapping #{source.class} attributes")
97
            enumerator = type.enumerator.enumerate(source, type, ctx)
98
            enumerator.map do |attribute, value, segment|
99
              local_ctx = segment.nil? ? ctx : ctx.advance(segment)
100
              mutated = map_attribute(value, attribute, local_ctx)
101
              changed = !mutated.equal?(value)
102
              if changed
103
                ctx.logger.debug("Attribute #{attribute.to_def} has changed")
104
              end
105
              [changed, attribute, mutated, local_ctx]
106
            end
107
          end
108
109
          # @param [Object] source
110
          # @param [AMA::Entity::Mapper::Type::Attribute] attribute
111
          # @param [AMA::Entity::Mapper::Context] context
112 View Code Duplication
          def map_attribute(source, attribute, context)
113
            message = "Extracting attribute #{attribute.to_def} " \
114
              "from #{source.class}"
115
            context.logger.debug(message)
116
            successful(attribute.types, Mapper::Error) do |type|
117
              if source.nil? && attribute.nullable
118
                context.logger.debug('Found legal nil, short-circuiting')
119
                break nil
120
              end
121
              result = map_type(source, type, context)
122
              context.logger.debug("Validating resulting #{attribute.to_def}")
123
              attribute.valid!(result, context)
124
              result
125
            end
126
          end
127
128
          # @param [Object] target
129
          # @param [AMA::Entity::Mapper::Type] type
130
          # @param [Array] attributes
131
          # @param [AMA::Entity::Mapper::Context] ctx
132
          def install_attributes(target, type, attributes, ctx)
133
            ctx.logger.debug("Installing updated attributes on #{type.to_def}")
134
            attributes.each do |_, attribute, value, local_ctx|
135
              type.injector.inject(target, type, attribute, value, local_ctx)
136
            end
137
            target
138
          end
139
140
          # @param [Object] source
141
          # @param [AMA::Entity::Mapper::Type] type
142
          # @param [AMA::Entity::Mapper::Context] context
143
          # @return [Array<Object, TrueClass, FalseClass>]
144
          def request_reassembly(source, type, context)
145
            if type.instance?(source)
146
              msg = "Not reassembling #{source.class}, already of target type"
147
              context.logger.debug(msg)
148
              return [source, false]
149
            end
150
            reassemble(source, type, context)
151
          end
152
153
          # @param [Object] source
154
          # @param [AMA::Entity::Mapper::Type] type
155
          # @param [AMA::Entity::Mapper::Context] context
156
          # @return [Object]
157
          def reassemble(source, type, context)
158
            message = "Reassembling #{source.class} as #{type.type}"
159
            context.logger.debug(message)
160
            source_type = @registry.find(source.class)
161
            source_type ||= Type::Analyzer.analyze(source.class)
162
            normalizer = source_type.normalizer
163
            normalized = normalizer.normalize(source, source_type, context)
164
            [type.denormalizer.denormalize(normalized, type, context), true]
165
          end
166
        end
167
      end
168
    end
169
  end
170
end
171