contracts 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/contracts.rb CHANGED
@@ -19,10 +19,10 @@ module Contracts
19
19
 
20
20
  def functype(funcname)
21
21
  contracts = self.class.decorated_methods[funcname]
22
- if contracts.nil? || contracts.empty?
22
+ if contracts.nil?
23
23
  "No contract for #{self.class}.#{funcname}"
24
24
  else
25
- "#{funcname} :: #{contracts[0]}"
25
+ "#{funcname} :: #{contracts}"
26
26
  end
27
27
  end
28
28
  end
@@ -36,20 +36,34 @@ end
36
36
  #
37
37
  # This class also provides useful callbacks and a validation method.
38
38
  class Contract < Decorator
39
- attr_accessor :contracts, :klass, :method
39
+ attr_reader :args_contracts, :ret_contract, :klass, :method
40
40
  # decorator_name :contract
41
41
  def initialize(klass, method, *contracts)
42
42
  if contracts[-1].is_a? Hash
43
43
  # internally we just convert that return value syntax back to an array
44
- contracts = contracts[0, contracts.size - 1] + contracts[-1].keys + contracts[-1].values
44
+ @args_contracts = contracts[0, contracts.size - 1] + contracts[-1].keys
45
+ @ret_contract = contracts[-1].values[0]
46
+ @args_validators = @args_contracts.map do |contract|
47
+ Contract.make_validator(contract)
48
+ end
49
+ @ret_validator = Contract.make_validator(@ret_contract)
45
50
  else
46
51
  fail "It looks like your contract for #{method} doesn't have a return value. A contract should be written as `Contract arg1, arg2 => return_value`."
47
52
  end
48
- @klass, @method, @contracts = klass, method, contracts
53
+ @klass, @method= klass, method
54
+ @has_func_contracts = args_contracts.index do |contract|
55
+ contract.is_a? Contracts::Func
56
+ end
57
+ end
58
+
59
+ def pretty_contract c
60
+ c.is_a?(Class) ? c.name : c.class.name
49
61
  end
50
62
 
51
63
  def to_s
52
- (contracts[0, contracts.size - 1].join(", ") + " => #{contracts[-1]}").gsub("Contracts::", "")
64
+ args = @args_contracts.map { |c| pretty_contract(c) }.join(", ")
65
+ ret = pretty_contract(@ret_contract)
66
+ ("#{args} => #{ret}").gsub("Contracts::", "")
53
67
  end
54
68
 
55
69
  # Given a hash, prints out a failure message.
@@ -73,7 +87,7 @@ class Contract < Decorator
73
87
  Expected: #{expected},
74
88
  Actual: #{data[:arg].inspect}
75
89
  Value guarded in: #{data[:class]}::#{method_name}
76
- With Contract: #{data[:contracts].map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ") }
90
+ With Contract: #{data[:contracts]}
77
91
  At: #{position} }
78
92
  end
79
93
 
@@ -94,10 +108,6 @@ class Contract < Decorator
94
108
  raise failure_msg(data)
95
109
  end
96
110
 
97
- # Callback for when a contract succeeds. Does nothing by default.
98
- def self.success_callback(data)
99
- end
100
-
101
111
  # Used to verify if an argument satisfies a contract.
102
112
  #
103
113
  # Takes: an argument and a contract.
@@ -106,37 +116,56 @@ class Contract < Decorator
106
116
  # whether the contract was valid or not. If it wasn't, metadata
107
117
  # contains some useful information about the failure.
108
118
  def self.valid?(arg, contract)
109
- case contract
110
- when Class
111
- # e.g. Fixnum
112
- validate_class arg, contract
113
- when Proc
119
+ make_validator(contract)[arg]
120
+ end
121
+
122
+ # This is a little weird. For each contract
123
+ # we pre-make a proc to validate it so we
124
+ # don't have to go through this decision tree every time.
125
+ # Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
126
+ def self.make_validator(contract)
127
+ # if is faster than case!
128
+ klass = contract.class
129
+ if klass == Proc
114
130
  # e.g. lambda {true}
115
- validate_proc arg, contract
116
- when Array
131
+ contract
132
+ elsif klass == Array
117
133
  # e.g. [Num, String]
118
134
  # TODO account for these errors too
119
- return mkerror(false, arg, contract) unless arg.is_a?(Array)
120
- arg.zip(contract).each do |_arg, _contract|
121
- res, info = valid?(_arg, _contract)
122
- return mkerror(false, arg, contract) unless res
123
- end
124
- when Hash
135
+ lambda { |arg|
136
+ return false unless arg.is_a?(Array)
137
+ arg.zip(contract).all? do |_arg, _contract|
138
+ Contract.valid?(_arg, _contract)
139
+ end
140
+ }
141
+ elsif klass == Hash
125
142
  # e.g. { :a => Num, :b => String }
126
- return mkerror(false, arg, contract) unless arg.is_a?(Hash)
127
- validate_hash(arg, contract)
128
- when Contracts::Args
129
- valid? arg, contract.contract
130
- when Contracts::Func
131
- arg.is_a?(Method) || arg.is_a?(Proc)
143
+ lambda { |arg|
144
+ return false unless arg.is_a?(Hash)
145
+ contract.keys.all? do |k|
146
+ Contract.valid?(arg[k], contract[k])
147
+ end
148
+ }
149
+ elsif klass == Contracts::Args
150
+ lambda { |arg|
151
+ Contract.valid?(arg, contract.contract)
152
+ }
153
+ elsif klass == Contracts::Func
154
+ lambda { |arg|
155
+ arg.is_a?(Method) || arg.is_a?(Proc)
156
+ }
132
157
  else
158
+ # classes and everything else
159
+ # e.g. Fixnum, Num
133
160
  if contract.respond_to? :valid?
134
- mkerror(contract.valid?(arg), arg, contract)
161
+ lambda { |arg| contract.valid?(arg) }
162
+ elsif klass == Class
163
+ lambda { |arg| contract == arg.class }
135
164
  else
136
- mkerror(arg == contract, arg, contract)
165
+ lambda { |arg| contract == arg }
137
166
  end
138
167
  end
139
- end
168
+ end
140
169
 
141
170
  def [](*args, &blk)
142
171
  call(*args, &blk)
@@ -148,88 +177,40 @@ class Contract < Decorator
148
177
 
149
178
  def call_with(this, *args, &blk)
150
179
  _args = blk ? args + [blk] : args
151
- res = Contract.validate_all(_args, @contracts[0, @contracts.size - 1], @klass, @method)
152
- return if res == false
153
180
 
154
- # contracts on methods
155
-
156
- contracts.each_with_index do |contract, i|
157
- if contract.is_a? Contracts::Func
158
- args[i] = Contract.new(@klass, args[i], *contract.contracts)
181
+ # check contracts on arguments
182
+ # fun fact! This is significantly faster than .zip (3.7 secs vs 4.7 secs). Why??
183
+ last_index = @args_validators.size - 1
184
+ # times is faster than (0..args.size).each
185
+ _args.size.times do |i|
186
+ # this is done to account for extra args (for *args)
187
+ j = i < last_index ? i : last_index
188
+ #unless true #@args_contracts[i].valid?(args[i])
189
+ unless @args_validators[j][_args[i]]
190
+ call_function = Contract.failure_callback({:arg => _args[i], :contract => @args_contracts[j], :class => @klass, :method => @method, :contracts => self})
191
+ return unless call_function
159
192
  end
160
- end
161
-
162
- if @method.respond_to? :bind
163
- # instance method
164
- result = @method.bind(this).call(*args, &blk)
165
- else
166
- # class method
167
- result = @method.call(*args, &blk)
168
- end
169
-
170
- Contract.validate(result, @contracts[-1], @klass, @method, @contracts)
171
- result
172
- end
173
-
174
- private
175
-
176
- def self.mkerror(validates, arg, contract)
177
- if validates
178
- [true, {}]
179
- else
180
- [false, { :arg => arg, :contract => contract }]
181
- end
182
- end
183
-
184
- def self.validate_hash(arg, contract)
185
- contract.keys.each do |k|
186
- result, info = valid?(arg[k], contract[k])
187
- return [result, info] unless result
188
193
  end
189
- end
190
-
191
- def self.validate_proc(arg, contract)
192
- mkerror(contract[arg], arg, contract)
193
- end
194
194
 
195
- def self.validate_class(arg, contract)
196
- valid = if contract.respond_to? :valid?
197
- contract.valid? arg
198
- else
199
- contract == arg.class
200
- end
201
- mkerror(valid, arg, contract)
202
- end
203
-
204
- def self.validate_all(params, contracts, klass, method)
205
- # we assume that any mismatch in # of params/contracts
206
- # has been checked befoer this point.
207
- args_index = contracts.index do |contract|
208
- contract.is_a? Contracts::Args
209
- end
210
- if args_index
211
- # there is a *args at this index.
212
- # Now we need to see how many arguments this contract
213
- # accounts for and just duplicate the contract for all
214
- # of those args.
215
- args_contract = contracts[args_index]
216
- while contracts.size < params.size
217
- contracts.insert(args_index, args_contract.dup)
195
+ if @has_func_contracts
196
+ # contracts on methods
197
+ contracts.each_with_index do |contract, i|
198
+ if contract.is_a? Contracts::Func
199
+ args[i] = Contract.new(@klass, args[i], *contract.contracts)
200
+ end
218
201
  end
219
202
  end
220
203
 
221
- params.zip(contracts).each do |param, contract|
222
- result = validate(param, contract, klass, method, contracts)
223
- return result if result == false
224
- end
225
- end
226
-
227
- def self.validate(arg, contract, klass, method, contracts)
228
- result, _ = valid?(arg, contract)
229
- if result
230
- success_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
204
+ result = if @method.respond_to? :bind
205
+ # instance method
206
+ @method.bind(this).call(*args, &blk)
231
207
  else
232
- failure_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
208
+ # class method
209
+ @method.call(*args, &blk)
233
210
  end
211
+ unless @ret_validator[result]
212
+ Contract.failure_callback({:arg => result, :contract => @ret_contract, :class => @klass, :method => @method, :contracts => self})
213
+ end
214
+ result
234
215
  end
235
216
  end
data/lib/decorators.rb CHANGED
@@ -1,7 +1,14 @@
1
1
  module MethodDecorators
2
2
  def self.extended(klass)
3
- class << klass
4
- attr_accessor :decorated_methods
3
+ klass.class_eval do
4
+ @@__decorated_methods ||= {}
5
+ def self.__decorated_methods
6
+ @@__decorated_methods
7
+ end
8
+
9
+ def self.__decorated_methods_set(k, v)
10
+ @@__decorated_methods[k] = v
11
+ end
5
12
  end
6
13
  end
7
14
 
@@ -15,6 +22,7 @@ module MethodDecorators
15
22
  super
16
23
  end
17
24
 
25
+ # For Ruby 1.9
18
26
  def singleton_method_added name
19
27
  common_method_added name, true
20
28
  super
@@ -25,10 +33,6 @@ module MethodDecorators
25
33
 
26
34
  decorators = @decorators.dup
27
35
  @decorators = nil
28
- @decorated_methods ||= Hash.new {|h,k| h[k] = []}
29
-
30
- # attr_accessor on the class variable decorated_methods
31
- class << self; attr_accessor :decorated_methods; end
32
36
 
33
37
  decorators.each do |klass, args|
34
38
  # a reference to the method gets passed into the contract here. This is good because
@@ -43,26 +47,19 @@ module MethodDecorators
43
47
  else
44
48
  decorator = klass
45
49
  end
46
- @decorated_methods[name] << decorator
50
+ __decorated_methods_set(name, decorator)
47
51
  end
48
52
 
49
53
  # in place of this method, we are going to define our own method. This method
50
54
  # just calls the decorator passing in all args that were to be passed into the method.
51
55
  # The decorator in turn has a reference to the actual method, so it can call it
52
56
  # on its own, after doing it's decorating of course.
53
- class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
57
+ class_eval %{
54
58
  def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
55
- ret = nil
56
59
  this = self#{is_class_method ? "" : ".class"}
57
- unless this.respond_to?(:decorated_methods) && !this.decorated_methods.nil?
58
- raise "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."
59
- end
60
- this.decorated_methods[#{name.inspect}].each do |decorator|
61
- ret = decorator.call_with(self, *args, &blk)
62
- end
63
- ret
64
- end
65
- ruby_eval
60
+ return this.__decorated_methods[#{name.inspect}].call_with(self, *args, &blk)
61
+ end
62
+ }, __FILE__, __LINE__ + 1
66
63
  end
67
64
 
68
65
  def decorate(klass, *args)
@@ -0,0 +1,102 @@
1
+ module MethodDecorators
2
+ def self.extended(klass)
3
+ klass.class_eval do
4
+ @@__decorated_methods ||= {}
5
+ def self.__decorated_methods
6
+ @@__decorated_methods
7
+ end
8
+
9
+ def self.__decorated_methods_set(k, v)
10
+ @@__decorated_methods[k] = v
11
+ end
12
+ end
13
+ end
14
+
15
+ # first, when you write a contract, the decorate method gets called which
16
+ # sets the @decorators variable. Then when the next method after the contract
17
+ # is defined, method_added is called and we look at the @decorators variable
18
+ # to find the decorator for that method. This is how we associate decorators
19
+ # with methods.
20
+ def method_added(name)
21
+ common_method_added name, false
22
+ super
23
+ end
24
+
25
+ # For Ruby 1.9
26
+ def singleton_method_added name
27
+ common_method_added name, true
28
+ super
29
+ end
30
+
31
+ def common_method_added name, is_class_method
32
+ return unless @decorators
33
+
34
+ decorators = @decorators.dup
35
+ @decorators = nil
36
+
37
+ decorators.each do |klass, args|
38
+ # a reference to the method gets passed into the contract here. This is good because
39
+ # we are going to redefine this method with a new name below...so this reference is
40
+ # now the *only* reference to the old method that exists.
41
+ if klass.respond_to? :new
42
+ if is_class_method
43
+ decorator = klass.new(*args)
44
+ else
45
+ decorator = klass.new(*args)
46
+ end
47
+ else
48
+ decorator = klass
49
+ end
50
+ __decorated_methods_set(name, decorator)
51
+ end
52
+
53
+ # in place of this method, we are going to define our own method. This method
54
+ # just calls the decorator passing in all args that were to be passed into the method.
55
+ # The decorator in turn has a reference to the actual method, so it can call it
56
+ # on its own, after doing it's decorating of course.
57
+ alias_method :"original_#{name}", name
58
+ class_eval %{
59
+ def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
60
+ this = self#{is_class_method ? "" : ".class"}
61
+ #this.__decorated_methods[#{name.inspect}].check_args(*args, &blk)
62
+ raise unless args[0].is_a?(Numeric)
63
+ raise unless args[1].is_a?(Numeric)
64
+
65
+ res = self.send :"original_#{name}", *args, &blk
66
+ raise unless res.is_a?(Numeric)
67
+ #this.__decorated_methods[#{name.inspect}].check_res(res)
68
+ res
69
+ end
70
+ }, __FILE__, __LINE__ + 1
71
+ end
72
+
73
+ def decorate(klass, *args)
74
+ @decorators ||= []
75
+ @decorators << [klass, args]
76
+ end
77
+ end
78
+
79
+ class Decorator
80
+ # an attr_accessor for a class variable:
81
+ class << self; attr_accessor :decorators; end
82
+
83
+ def self.inherited(klass)
84
+ name = klass.name.gsub(/^./) {|m| m.downcase}
85
+
86
+ return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
87
+
88
+ # the file and line parameters set the text for error messages
89
+ # make a new method that is the name of your decorator.
90
+ # that method accepts random args and a block.
91
+ # inside, `decorate` is called with those params.
92
+ MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
93
+ def #{klass}(*args, &blk)
94
+ decorate(#{klass}, *args, &blk)
95
+ end
96
+ ruby_eval
97
+ end
98
+
99
+ def initialize(klass, method)
100
+ @method = method
101
+ end
102
+ end
@@ -0,0 +1,97 @@
1
+ module MethodDecorators
2
+ def self.extended(klass)
3
+ class << klass
4
+ attr_accessor :decorated_methods
5
+ end
6
+ end
7
+
8
+ # first, when you write a contract, the decorate method gets called which
9
+ # sets the @decorators variable. Then when the next method after the contract
10
+ # is defined, method_added is called and we look at the @decorators variable
11
+ # to find the decorator for that method. This is how we associate decorators
12
+ # with methods.
13
+ def method_added(name)
14
+ common_method_added name, false
15
+ super
16
+ end
17
+
18
+ def singleton_method_added name
19
+ common_method_added name, true
20
+ super
21
+ end
22
+
23
+ def common_method_added name, is_class_method
24
+ return unless @decorators
25
+
26
+ decorators = @decorators.dup
27
+ @decorators = nil
28
+ @decorated_methods ||= Hash.new {|h,k| h[k] = []}
29
+
30
+ # attr_accessor on the class variable decorated_methods
31
+ class << self; attr_accessor :decorated_methods; end
32
+
33
+ decorators.each do |klass, args|
34
+ # a reference to the method gets passed into the contract here. This is good because
35
+ # we are going to redefine this method with a new name below...so this reference is
36
+ # now the *only* reference to the old method that exists.
37
+ if klass.respond_to? :new
38
+ if is_class_method
39
+ decorator = klass.new(self, method(name), *args)
40
+ else
41
+ decorator = klass.new(self, instance_method(name), *args)
42
+ end
43
+ else
44
+ decorator = klass
45
+ end
46
+ @decorated_methods[name] << decorator
47
+ end
48
+
49
+ # in place of this method, we are going to define our own method. This method
50
+ # just calls the decorator passing in all args that were to be passed into the method.
51
+ # The decorator in turn has a reference to the actual method, so it can call it
52
+ # on its own, after doing it's decorating of course.
53
+ class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
54
+ def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
55
+ ret = nil
56
+ this = self#{is_class_method ? "" : ".class"}
57
+ unless this.respond_to?(:decorated_methods) && !this.decorated_methods.nil?
58
+ raise "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."
59
+ end
60
+ this.decorated_methods[#{name.inspect}].each do |decorator|
61
+ ret = decorator.call_with(self, *args, &blk)
62
+ end
63
+ ret
64
+ end
65
+ ruby_eval
66
+ end
67
+
68
+ def decorate(klass, *args)
69
+ @decorators ||= []
70
+ @decorators << [klass, args]
71
+ end
72
+ end
73
+
74
+ class Decorator
75
+ # an attr_accessor for a class variable:
76
+ class << self; attr_accessor :decorators; end
77
+
78
+ def self.inherited(klass)
79
+ name = klass.name.gsub(/^./) {|m| m.downcase}
80
+
81
+ return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
82
+
83
+ # the file and line parameters set the text for error messages
84
+ # make a new method that is the name of your decorator.
85
+ # that method accepts random args and a block.
86
+ # inside, `decorate` is called with those params.
87
+ MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
88
+ def #{klass}(*args, &blk)
89
+ decorate(#{klass}, *args, &blk)
90
+ end
91
+ ruby_eval
92
+ end
93
+
94
+ def initialize(klass, method)
95
+ @method = method
96
+ end
97
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: contracts
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.3
5
+ version: 0.1.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Aditya Bhargava
@@ -25,6 +25,8 @@ files:
25
25
  - lib/builtin_contracts.rb
26
26
  - lib/contracts.rb
27
27
  - lib/decorators.rb
28
+ - lib/decorators_aliased.rb
29
+ - lib/decorators_old.rb
28
30
  - lib/testable.rb
29
31
  homepage: http://github.com/egonSchiele/contracts.ruby
30
32
  licenses: []