RecursiveMapper.map_unsafe()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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