11module Contracts
22 module MethodDecorators
33 def self . extended ( klass )
4- return if klass . respond_to? ( :decorated_methods= )
5-
6- class << klass
7- attr_accessor :decorated_methods
8- end
9- end
10-
11- module EigenclassWithOwner
12- def self . lift ( eigenclass )
13- fail Contracts ::ContractsNotIncluded unless with_owner? ( eigenclass )
14-
15- eigenclass
16- end
17-
18- private
19-
20- def self . with_owner? ( eigenclass )
21- eigenclass . respond_to? ( :owner_class ) && eigenclass . owner_class
22- end
4+ Engine . apply ( klass )
235 end
246
25- # first, when you write a contract, the decorate method gets called which
26- # sets the @decorators variable. Then when the next method after the contract
27- # is defined, method_added is called and we look at the @decorators variable
28- # to find the decorator for that method. This is how we associate decorators
29- # with methods.
307 def method_added ( name )
31- common_method_added name , false
8+ MethodHandler . new ( name , false , self ) . handle
329 super
3310 end
3411
3512 def singleton_method_added ( name )
36- common_method_added name , true
13+ MethodHandler . new ( name , true , self ) . handle
3714 super
3815 end
39-
40- def pop_decorators
41- Array ( @decorators ) . tap { @decorators = nil }
42- end
43-
44- def fetch_decorators
45- pop_decorators + Eigenclass . lift ( self ) . pop_decorators
46- end
47-
48- def common_method_added ( name , is_class_method )
49- decorators = fetch_decorators
50- return if decorators . empty?
51-
52- @decorated_methods ||= { :class_methods => { } , :instance_methods => { } }
53-
54- if is_class_method
55- method_reference = SingletonMethodReference . new ( name , method ( name ) )
56- method_type = :class_methods
57- else
58- method_reference = MethodReference . new ( name , instance_method ( name ) )
59- method_type = :instance_methods
60- end
61-
62- @decorated_methods [ method_type ] [ name ] ||= [ ]
63-
64- unless decorators . size == 1
65- fail %{
66- Oops, it looks like method '#{ name } ' has multiple contracts:
67- #{ decorators . map { |x | x [ 1 ] [ 0 ] . inspect } . join ( "\n " ) }
68-
69- Did you accidentally put more than one contract on a single function, like so?
70-
71- Contract String => String
72- Contract Num => String
73- def foo x
74- end
75-
76- If you did NOT, then you have probably discovered a bug in this library.
77- Please file it along with the relevant code at:
78- https://github.com/egonSchiele/contracts.ruby/issues
79- }
80- end
81-
82- pattern_matching = false
83- decorators . each do |klass , args |
84- # a reference to the method gets passed into the contract here. This is good because
85- # we are going to redefine this method with a new name below...so this reference is
86- # now the *only* reference to the old method that exists.
87- # We assume here that the decorator (klass) responds to .new
88- decorator = klass . new ( self , method_reference , *args )
89- new_args_contract = decorator . args_contracts
90- matched = @decorated_methods [ method_type ] [ name ] . select do |contract |
91- contract . args_contracts == new_args_contract
92- end
93- unless matched . empty?
94- fail ContractError . new ( %{
95- It looks like you are trying to use pattern-matching, but
96- multiple definitions for function '#{ name } ' have the same
97- contract for input parameters:
98-
99- #{ ( matched + [ decorator ] ) . map ( &:to_s ) . join ( "\n " ) }
100-
101- Each definition needs to have a different contract for the parameters.
102- } , { } )
103- end
104- @decorated_methods [ method_type ] [ name ] << decorator
105- pattern_matching ||= decorator . pattern_match?
106- end
107-
108- if @decorated_methods [ method_type ] [ name ] . any? { |x | x . method != method_reference }
109- @decorated_methods [ method_type ] [ name ] . each ( &:pattern_match! )
110-
111- pattern_matching = true
112- end
113-
114- method_reference . make_alias ( self )
115-
116- return if ENV [ "NO_CONTRACTS" ] && !pattern_matching
117-
118- # in place of this method, we are going to define our own method. This method
119- # just calls the decorator passing in all args that were to be passed into the method.
120- # The decorator in turn has a reference to the actual method, so it can call it
121- # on its own, after doing it's decorating of course.
122-
123- # Very important: THe line `current = #{self}` in the start is crucial.
124- # Not having it means that any method that used contracts could NOT use `super`
125- # (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27).
126- # Here's why: Suppose you have this code:
127- #
128- # class Foo
129- # Contract String
130- # def to_s
131- # "Foo"
132- # end
133- # end
134- #
135- # class Bar < Foo
136- # Contract String
137- # def to_s
138- # super + "Bar"
139- # end
140- # end
141- #
142- # b = Bar.new
143- # p b.to_s
144- #
145- # `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However,
146- # we have overwritten the function (that's what this next defn is). So it gets a
147- # reference to the function to call by looking at `decorated_methods`.
148- #
149- # Now, this line used to read something like:
150- #
151- # current = self#{is_class_method ? "" : ".class"}
152- #
153- # In that case, `self` would always be `Bar`, regardless of whether you were calling
154- # Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which
155- # means you would always call Bar's to_s...infinite recursion! Instead, you want to
156- # call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
157-
158- current = self
159- method_reference . make_definition ( self ) do |*args , &blk |
160- ancestors = current . ancestors
161- ancestors . shift # first one is just the class itself
162- while current && !current . respond_to? ( :decorated_methods ) || current . decorated_methods . nil?
163- current = ancestors . shift
164- end
165- if !current . respond_to? ( :decorated_methods ) || current . decorated_methods . nil?
166- fail "Couldn't find decorator for method " + self . class . name + ":#{ name } .\n Does this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\n Look at the specs for contracts.ruby as an example of how to write contracts in this case."
167- end
168- methods = current . decorated_methods [ method_type ] [ name ]
169-
170- # this adds support for overloading methods. Here we go through each method and call it with the arguments.
171- # If we get a ContractError, we move to the next function. Otherwise we return the result.
172- # If we run out of functions, we raise the last ContractError.
173- success = false
174- i = 0
175- result = nil
176- expected_error = methods [ 0 ] . failure_exception
177- until success
178- method = methods [ i ]
179- i += 1
180- begin
181- success = true
182- result = method . call_with ( self , *args , &blk )
183- rescue expected_error => error
184- success = false
185- unless methods [ i ]
186- begin
187- ::Contract . failure_callback ( error . data , false )
188- rescue expected_error => final_error
189- raise final_error . to_contract_error
190- end
191- end
192- end
193- end
194- result
195- end
196- end
197-
198- def decorate ( klass , *args )
199- if Support . eigenclass? self
200- return EigenclassWithOwner . lift ( self ) . owner_class . decorate ( klass , *args )
201- end
202-
203- @decorators ||= [ ]
204- @decorators << [ klass , args ]
205- end
20616 end
20717
20818 class Decorator
@@ -220,7 +30,7 @@ def self.inherited(klass)
22030 # inside, `decorate` is called with those params.
22131 MethodDecorators . module_eval <<-ruby_eval , __FILE__ , __LINE__ + 1
22232 def #{ klass } (*args, &blk)
223- decorate(#{ klass } , *args, &blk)
33+ ::Contracts::Engine.fetch_from(self). decorate(#{ klass } , *args, &blk)
22434 end
22535 ruby_eval
22636 end
0 commit comments