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
|
# 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
matched = decorated_methods.select do |contract|
contract.args_contracts == new_args_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
|