Completed
Push — dev ( ff1c91...ab1075 )
by Fike
01:06
created

Type.resolved!   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
1
# frozen_string_literal: true
2
3
# rubocop:disable Metrics/ClassLength
4
5
require_relative 'mixin/errors'
6
require_relative 'mixin/reflection'
7
require_relative 'mixin/handler_support'
8
require_relative 'context'
9
require_relative 'type/parameter'
10
require_relative 'type/attribute'
11
require_relative 'handler/entity/normalizer'
12
require_relative 'handler/entity/denormalizer'
13
require_relative 'handler/entity/enumerator'
14
require_relative 'handler/entity/injector'
15
require_relative 'handler/entity/factory'
16
require_relative 'handler/entity/validator'
17
18
module AMA
19
  module Entity
20
    class Mapper
21
      # Type wrapper
22
      class Type
23
        include Mixin::Errors
24
        include Mixin::Reflection
25
        include Mixin::HandlerSupport
26
27
        # @!attribute type
28
        #   @return [Class]
29
        attr_accessor :type
30
        # @!attribute parameters
31
        #   @return [Hash{Symbol, AMA::Entity::Mapper::Type::Parameter}]
32
        attr_accessor :parameters
33
        # @!attribute attributes
34
        #   @return [Hash{Symbol, AMA::Entity::Mapper::Type::Attribute}]
35
        attr_accessor :attributes
36
        # @!attribute virtual
37
        #   @return [TrueClass, FalseClass]
38
        attr_accessor :virtual
39
40
        handler_namespace Handler::Entity
41
42
        # @!attribute factory
43
        #   @return [AMA::Entity::Mapper::Handler::Entity::Factory]
44
        handler :factory, :create
45
        # @!attribute normalizer
46
        #   @return [AMA::Entity::Mapper::Handler::Entity::Normalizer]
47
        handler :normalizer, :normalize
48
        # @!attribute denormalizer
49
        #   @return [AMA::Entity::Mapper::Handler::Entity::Denormalizer]
50
        handler :denormalizer, :denormalize
51
        # @!attribute enumerator
52
        #   @return [AMA::Entity::Mapper::Handler::Entity::Enumerator]
53
        handler :enumerator, :enumerate
54
        # @!attribute injector
55
        #   @return [AMA::Entity::Mapper::Handler::Entity::Injector]
56
        handler :injector, :inject
57
        # @!attribute injector
58
        #   @return [AMA::Entity::Mapper::Handler::Entity::Validator]
59
        handler :validator, :validate
60
61
        # @param [Class, Module] klass
62
        def initialize(klass, virtual: false)
63
          @type = validate_type!(klass)
64
          @parameters = {}
65
          @attributes = {}
66
          @virtual = virtual
67
        end
68
69
        # Tells if provided object is an instance of this type.
70
        #
71
        # This doesn't mean all of it's attributes do match requested types.
72
        #
73
        # @param [Object] object
74
        # @return [TrueClass, FalseClass]
75
        def instance?(object)
76
          object.is_a?(@type)
77
        end
78
79
        def instance!(object, context)
80
          return if instance?(object)
81
          message = "Provided object #{object} is not an instance of #{self}"
82
          validation_error(message, context: context)
83
        end
84
85
        # @return [TrueClass, FalseClass]
86
        def resolved?
87
          attributes.values.all?(&:resolved?)
88
        end
89
90
        # Validates that type is fully resolved, otherwise raises an error
91
        # @param [AMA::Entity::Mapper::Context] context
92
        def resolved!(context = Context.new)
93
          attributes.values.each { |attribute| attribute.resolved!(context) }
94
        end
95
96
        # Shortcut for attribute creation.
97
        #
98
        # @param [String, Symbol] name
99
        # @param [Array<AMA::Entity::Mapper::Type>] types
100
        # @param [Hash] options
101
        def attribute!(name, *types, **options)
102
          name = name.to_sym
103
          types = types.map do |type|
104
            next type if type.is_a?(Parameter)
105
            next parameter!(type) if type.is_a?(Symbol)
106
            next self.class.new(type) unless type.is_a?(Type)
107
            type
108
          end
109
          attributes[name] = Attribute.new(self, name, *types, **options)
110
        end
111
112
        # Creates new type parameter
113
        #
114
        # @param [Symbol] id
115
        # @return [Parameter]
116
        def parameter!(id)
117
          id = id.to_sym
118
          return @parameters[id] if @parameters.key?(id)
119
          @parameters[id] = Parameter.new(self, id)
120
        end
121
122
        # Resolves single parameter type. Substitution may be either another
123
        # parameter or array of types.
124
        #
125
        # @param [Parameter] parameter
126
        # @param [Parameter, Array<Type>] substitution
127
        def resolve_parameter(parameter, substitution)
128
          parameter = validate_parameter!(parameter)
129
          substitution = validate_substitution!(substitution)
130
          clone.tap do |clone|
131
            intermediate = attributes.map do |key, value|
132
              [key, value.resolve_parameter(parameter, substitution)]
133
            end
134
            clone.attributes = Hash[intermediate]
135
            intermediate = clone.parameters.map do |key, value|
136
              [key, value == parameter ? substitution : value]
137
            end
138
            clone.parameters = Hash[intermediate]
139
          end
140
        end
141
142
        # rubocop:disable Metrics/LineLength
143
144
        # @param [Hash<AMA::Entity::Mapper::Type, AMA::Entity::Mapper::Type>] parameters
145
        # @return [AMA::Entity::Mapper::Type]
146
        def resolve(parameters)
147
          parameters.reduce(self) do |carrier, tuple|
148
            carrier.resolve_parameter(*tuple)
149
          end
150
        end
151
152
        # rubocop:enable Metrics/LineLength
153
154
        def violations(object, context)
155
          validator.validate(object, self, context)
156
        end
157
158
        def valid?(object, context)
159
          violations(object, context).empty?
160
        end
161
162
        def valid!(object, context)
163
          violations = self.violations(object, context)
164
          return if violations.empty?
165
          message = "#{object} has failed type #{to_def} validation: " \
166
            "#{violations.join(', ')}"
167
          validation_error(message, context: context)
168
        end
169
170
        def hash
171
          @type.hash ^ @attributes.hash
172
        end
173
174
        def eql?(other)
175
          return false unless other.is_a?(self.class)
176
          @type == other.type && @attributes == other.attributes
177
        end
178
179
        def ==(other)
180
          eql?(other)
181
        end
182
183
        def to_s
184
          message = "Type #{@type}"
185
          unless @parameters.empty?
186
            message += " (parameters: #{@parameters.keys})"
187
          end
188
          message
189
        end
190
191
        def to_def
192
          return @type.to_s if parameters.empty?
193
          params = parameters.map do |key, value|
194
            value = [value] unless value.is_a?(Enumerable)
195
            value = value.map(&:to_def)
196
            value = value.size > 1 ? "[#{value.join(', ')}]" : value.first
197
            "#{key}:#{value}"
198
          end
199
          "#{@type}<#{params.join(', ')}>"
200
        end
201
202
        private
203
204
        def validate_type!(type)
205
          return type if type.is_a?(Class) || type.is_a?(Module)
206
          message = 'Expected Type to be instantiated with ' \
207
              "Class/Module instance, got #{type}"
208
          compliance_error(message)
209
        end
210
211
        def validate_parameter!(parameter)
212
          return parameter if parameter.is_a?(Parameter)
213
          message = "Non-parameter type #{parameter} " \
214
              'supplied for resolution'
215
          compliance_error(message)
216
        end
217
218
        def validate_substitution!(substitution)
219
          return substitution if substitution.is_a?(Parameter)
220
          substitution = [substitution] if substitution.is_a?(self.class)
221
          if substitution.is_a?(Enumerable)
222
            return validate_substitutions!(substitution)
223
          end
224
          message = 'Provided substitution is neither another Parameter ' \
225
              'or Array of Types: ' \
226
              "#{substitution} (#{substitution.class})"
227
          compliance_error(message)
228
        end
229
230
        def validate_substitutions!(substitutions)
231
          if substitutions.empty?
232
            compliance_error('Empty list of substitutions passed')
233
          end
234
          invalid = substitutions.reject do |substitution|
235
            substitution.is_a?(Type)
236
          end
237
          return substitutions if invalid.empty?
238
          compliance_error("Invalid substitutions supplied: #{invalid}")
239
        end
240
      end
241
    end
242
  end
243
end
244