-
Notifications
You must be signed in to change notification settings - Fork 82
/
Copy pathmethod_handler.rb
199 lines (156 loc) · 5.95 KB
/
method_handler.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# frozen_string_literal: true
module Contracts
# Handles class and instance methods addition
# Represents single such method
class MethodHandler
METHOD_REFERENCE_FACTORY = {
:class_methods => SingletonMethodReference,
:instance_methods => MethodReference,
}
RAW_METHOD_STRATEGY = {
:class_methods => lambda { |target, name| target.method(name) },
:instance_methods => lambda { |target, name| target.instance_method(name) },
}
# Creates new instance of MethodHandler
#
# @param [Symbol] method_name
# @param [Bool] is_class_method
# @param [Class] target - class that method got added to
def initialize(method_name, is_class_method, target)
@method_name = method_name
@is_class_method = is_class_method
@target = target
end
# Handles method addition
def handle
return unless engine?
return if decorators.empty?
validate_decorators!
validate_pattern_matching!
engine.add_method_decorator(method_type, method_name, decorator)
mark_pattern_matching_decorators
method_reference.make_alias(target)
redefine_method
end
private
attr_reader :method_name, :is_class_method, :target
def engine?
Engine.applied?(target)
end
def engine
Engine.fetch_from(target)
end
def decorators
@_decorators ||= engine.all_decorators
end
def method_type
@_method_type ||= is_class_method ? :class_methods : :instance_methods
end
# _method_type is required for assigning it to local variable with
# the same name. See: #redefine_method
alias_method :_method_type, :method_type
def method_reference
@_method_reference ||= METHOD_REFERENCE_FACTORY[method_type].new(method_name, raw_method)
end
def raw_method
RAW_METHOD_STRATEGY[method_type].call(target, method_name)
end
def ignore_decorators?
ENV["NO_CONTRACTS"] && !pattern_matching?
end
def decorated_methods
@_decorated_methods ||= engine.decorated_methods_for(method_type, method_name)
end
def pattern_matching?
return @_pattern_matching if defined?(@_pattern_matching)
@_pattern_matching = decorated_methods.any? { |x| x.method != method_reference }
end
def mark_pattern_matching_decorators
return unless pattern_matching?
decorated_methods.each(&:pattern_match!)
end
def decorator
@_decorator ||= decorator_class.new(target, method_reference, *decorator_args)
end
def decorator_class
decorators.first[0]
end
def decorator_args
decorators.first[1]
end
def redefine_method
return if ignore_decorators?
# Those are required for instance_eval to be able to refer them
name = method_name
method_type = _method_type
current_engine = engine
# We are gonna redefine original method here
method_reference.make_definition(target) do |*args, **kargs, &blk|
engine = current_engine.nearest_decorated_ancestor
# If we weren't able to find any ancestor that has decorated methods
# FIXME : this looks like untested code (commenting it out doesn't make specs red)
unless engine
fail "Couldn't find decorator for method #{self.class.name}:#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case."
end
# Fetch decorated methods out of the contracts engine
decorated_methods = engine.decorated_methods_for(method_type, name)
# This adds support for overloading methods. Here we go
# through each method and call it with the arguments.
# If we get a failure_exception, we move to the next
# function. Otherwise we return the result.
# If we run out of functions, we raise the last error, but
# convert it to_contract_error.
expected_error = decorated_methods[0].failure_exception
last_error = nil
decorated_methods.each do |decorated_method|
result = decorated_method.call_with_inner(true, self, *args, **kargs, &blk)
return result unless result.is_a?(ParamContractError)
last_error = result
end
begin
if ::Contract.failure_callback(last_error&.data, use_pattern_matching: false)
decorated_methods.last.call_with_inner(false, self, *args, **kargs, &blk)
end
# rubocop:disable Naming/RescuedExceptionsVariableName
rescue expected_error => final_error
raise final_error.to_contract_error
# rubocop:enable Naming/RescuedExceptionsVariableName
end
end
end
def validate_decorators!
return if decorators.size == 1
fail %{
Oops, it looks like method '#{method_name}' has multiple contracts:
#{decorators.map { |x| x[1][0].inspect }.join("\n")}
Did you accidentally put more than one contract on a single function, like so?
Contract String => String
Contract Num => String
def foo x
end
If you did NOT, then you have probably discovered a bug in this library.
Please file it along with the relevant code at:
https://github.com/egonSchiele/contracts.ruby/issues
}
end
def validate_pattern_matching!
new_args_contract = decorator.args_contracts
new_kargs_contract = decorator.kargs_contract
matched = decorated_methods.select do |contract|
contract.args_contracts == new_args_contract &&
contract.kargs_contract == new_kargs_contract
end
return if matched.empty?
fail ContractError.new(
%{
It looks like you are trying to use pattern-matching, but
multiple definitions for function '#{method_name}' have the same
contract for input parameters:
#{(matched + [decorator]).map(&:to_s).join("\n")}
Each definition needs to have a different contract for the parameters.
},
{},
)
end
end
end