contracts 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ require 'testable'
2
+
1
3
  =begin rdoc
2
4
  This module contains all the builtin contracts.
3
5
  If you want to use them, first:
@@ -21,6 +23,14 @@ module Contracts
21
23
  def self.valid? val
22
24
  val.is_a? Numeric
23
25
  end
26
+
27
+ def self.testable?
28
+ true
29
+ end
30
+
31
+ def self.test_data
32
+ [-1, 0, 1, 1.5, 50000]
33
+ end
24
34
  end
25
35
 
26
36
  # Check that an argument is a positive number.
@@ -28,6 +38,14 @@ module Contracts
28
38
  def self.valid? val
29
39
  val > 0
30
40
  end
41
+
42
+ def testable?
43
+ true
44
+ end
45
+
46
+ def self.test_data
47
+ (0..5).map { rand(999) + 1 }
48
+ end
31
49
  end
32
50
 
33
51
  # Check that an argument is a negative number.
@@ -35,6 +53,14 @@ module Contracts
35
53
  def self.valid? val
36
54
  val < 0
37
55
  end
56
+
57
+ def testable?
58
+ true
59
+ end
60
+
61
+ def self.test_data
62
+ (0..5).map { (rand(999) + 1) * -1 }
63
+ end
38
64
  end
39
65
 
40
66
  # Passes for any argument.
@@ -83,6 +109,19 @@ module Contracts
83
109
  def to_s
84
110
  @vals[0, @vals.size-1].join(", ") + " or " + @vals[-1].to_s
85
111
  end
112
+
113
+ # this can only be tested IF all the sub-contracts have a test_data method
114
+ def testable?
115
+ @vals.all? do |val|
116
+ Testable.testable?(val)
117
+ end
118
+ end
119
+
120
+ def test_data
121
+ @vals.map { |val|
122
+ Testable.test_data(val)
123
+ }.flatten
124
+ end
86
125
  end
87
126
 
88
127
  # Takes a variable number of contracts.
@@ -104,6 +143,18 @@ module Contracts
104
143
  def to_s
105
144
  @vals[0, @vals.size-1].join(", ") + " xor " + @vals[-1].to_s
106
145
  end
146
+
147
+ def testable?
148
+ @vals.all? do |val|
149
+ Testable.testable? val
150
+ end
151
+ end
152
+
153
+ def test_data
154
+ @vals.map { |val|
155
+ Testable.test_data val
156
+ }.flatten
157
+ end
107
158
  end
108
159
 
109
160
  # Takes a variable number of contracts.
@@ -129,8 +180,8 @@ module Contracts
129
180
  # Takes a variable number of method names as symbols.
130
181
  # The contract passes if the argument responds to all
131
182
  # of those methods.
132
- # Example: <tt>RespondsTo[:password, :credit_card]</tt>
133
- class RespondsTo < CallableClass
183
+ # Example: <tt>RespondTo[:password, :credit_card]</tt>
184
+ class RespondTo < CallableClass
134
185
  def initialize(*meths)
135
186
  @meths = meths
136
187
  end
@@ -222,6 +273,14 @@ module Contracts
222
273
  def to_s
223
274
  "an array of #{@contract}"
224
275
  end
276
+
277
+ def testable?
278
+ Testable.testable? @contract
279
+ end
280
+
281
+ def test_data
282
+ [[], [Testable.test_data(@contract)], [Testable.test_data(@contract), Testable.test_data(@contract)]]
283
+ end
225
284
  end
226
285
 
227
286
  # Used for <tt>*args</tt> (variadic functions). Takes a contract
@@ -237,5 +296,60 @@ module Contracts
237
296
  def to_s
238
297
  "Args[#{@contract}]"
239
298
  end
240
- end
299
+
300
+ def testable?
301
+ Testable.testable? @contract
302
+ end
303
+
304
+ def test_data
305
+ [[], [Testable.test_data(@contract)], [Testable.test_data(@contract), Testable.test_data(@contract)]]
306
+ end
307
+ end
308
+
309
+ class Bool
310
+ def self.valid? val
311
+ val.is_a?(TrueClass) || val.is_a?(FalseClass)
312
+ end
313
+ end
314
+
315
+ class ::Hash
316
+ def testable?
317
+ self.values.all? do |val|
318
+ Testable.testable?(val)
319
+ end
320
+ end
321
+
322
+ def test_data
323
+ keys = self.keys
324
+ _vals = keys.map do |key|
325
+ ret = Testable.test_data(self[key])
326
+ if ret.is_a? Array
327
+ ret
328
+ else
329
+ [ret]
330
+ end
331
+ end
332
+ all_vals = Testable.product(_vals)
333
+ hashes = []
334
+ all_vals.each do |vals|
335
+ hash = {}
336
+ keys.zip(vals).each do |key, val|
337
+ hash[key] = val
338
+ end
339
+ hashes << hash
340
+ end
341
+ hashes
342
+ end
343
+ end
344
+
345
+ class ::String
346
+ def self.testable?
347
+ true
348
+ end
349
+
350
+ def self.test_data
351
+ # send a random string
352
+ ('a'..'z').to_a.shuffle[0, 10].join
353
+ end
354
+ end
241
355
  end
data/lib/contracts.rb CHANGED
@@ -1,15 +1,16 @@
1
1
  require 'decorators'
2
2
  require 'builtin_contracts'
3
3
 
4
- class Class
5
- include MethodDecorators
6
- end
4
+ module Contracts
5
+ def self.included(base)
6
+ base.extend MethodDecorators
7
+ end
7
8
 
8
- class Module
9
- include MethodDecorators
9
+ def self.extended(base)
10
+ base.extend MethodDecorators
11
+ end
10
12
  end
11
13
 
12
-
13
14
  # This is the main Contract class. When you write a new contract, you'll
14
15
  # write it as:
15
16
  #
@@ -18,7 +19,7 @@ end
18
19
  # This class also provides useful callbacks and a validation method.
19
20
  class Contract < Decorator
20
21
  attr_accessor :contracts, :klass, :method
21
- decorator_name :contract
22
+ # decorator_name :contract
22
23
  def initialize(klass, method, *contracts)
23
24
  @klass, @method, @contracts = klass, method, contracts
24
25
  end
@@ -185,3 +186,17 @@ Contracts: #{@contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(
185
186
  end
186
187
  end
187
188
  end
189
+
190
+ # convenience function for small scripts.
191
+ # Use as:
192
+ #
193
+ # use_contracts self
194
+ #
195
+ # And then you can use contracts on functions
196
+ # that aren't in any module or class.
197
+ def use_contracts(this)
198
+ this.class.send(:include, Contracts)
199
+ def this.Contract(*args)
200
+ self.class.Contract(*args)
201
+ end
202
+ end
data/lib/decorators.rb CHANGED
@@ -5,27 +5,11 @@ module MethodDecorators
5
5
  end
6
6
  end
7
7
 
8
- def method_missing(name, *args, &blk)
9
- # if the const isn't capitalized, then const_defined will throw an
10
- # error. So first check to make sure it is capitalized.
11
- letter = name.to_s[0]
12
- if letter >= 65 && letter <= 90 && Object.const_defined?(name)
13
- const = Object.const_get(name)
14
- elsif Decorator.decorators.key?(name)
15
- const = Decorator.decorators[name]
16
- else
17
- return super
18
- end
19
-
20
- instance_eval <<-ruby_eval, __FILE__, __LINE__ + 1
21
- def #{name}(*args, &blk)
22
- decorate(#{const.name}, *args, &blk)
23
- end
24
- ruby_eval
25
-
26
- send(name, *args, &blk)
27
- end
28
-
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.
29
13
  def method_added(name)
30
14
  return unless @decorators
31
15
 
@@ -33,13 +17,21 @@ module MethodDecorators
33
17
  @decorators = nil
34
18
  @decorated_methods ||= Hash.new {|h,k| h[k] = []}
35
19
 
20
+ # attr_accessor on the class variable decorated_methods
36
21
  class << self; attr_accessor :decorated_methods; end
37
22
 
38
23
  decorators.each do |klass, args|
24
+ # a reference to the method gets passed into the contract here. This is good because
25
+ # we are going to redefine this method with a new name below...so this reference is
26
+ # now the *only* reference to the old method that exists.
39
27
  decorator = klass.respond_to?(:new) ? klass.new(self, instance_method(name), *args) : klass
40
28
  @decorated_methods[name] << decorator
41
29
  end
42
30
 
31
+ # in place of this method, we are going to define our own method. This method
32
+ # just calls the decorator passing in all args that were to be passed into the method.
33
+ # The decorator in turn has a reference to the actual method, so it can call it
34
+ # on its own, after doing it's decorating of course.
43
35
  class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
44
36
  def #{name}(*args, &blk)
45
37
  ret = nil
@@ -49,6 +41,7 @@ module MethodDecorators
49
41
  ret
50
42
  end
51
43
  ruby_eval
44
+ super
52
45
  end
53
46
 
54
47
  def decorate(klass, *args)
@@ -58,19 +51,18 @@ module MethodDecorators
58
51
  end
59
52
 
60
53
  class Decorator
61
- class << self
62
- attr_accessor :decorators
63
- def decorator_name(name)
64
- Decorator.decorators ||= {}
65
- Decorator.decorators[name] = self
66
- end
67
- end
54
+ # an attr_accessor for a class variable:
55
+ class << self; attr_accessor :decorators; end
68
56
 
69
57
  def self.inherited(klass)
70
58
  name = klass.name.gsub(/^./) {|m| m.downcase}
71
59
 
72
60
  return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
73
61
 
62
+ # the file and line parameters set the text for error messages
63
+ # make a new method that is the name of your decorator.
64
+ # that method accepts random args and a block.
65
+ # inside, `decorate` is called with those params.
74
66
  MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
75
67
  def #{klass}(*args, &blk)
76
68
  decorate(#{klass}, *args, &blk)
data/lib/foo.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  require 'contracts'
2
- include Contracts
2
+ use_contracts(self)
3
3
 
4
- class Object
5
- Contract Num, Num, Num
6
- def add(a, b)
7
- a + b
8
- end
4
+ Contract [Num, String]
5
+ def mult
6
+ return 1, 2
9
7
  end
10
8
 
11
- puts add(1, "foo")
9
+ f = mult
10
+ p f
data/lib/test.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'contracts'
2
+ require 'testable'
2
3
  include Contracts
3
4
 
4
5
  class Object
6
+
5
7
  Contract Num, Num
6
8
  def double(x)
7
9
  x * 2
@@ -35,6 +37,24 @@ class Object
35
37
  def person(data)
36
38
  p data
37
39
  end
40
+
41
+ Contract Num, Num
42
+ def test(x)
43
+ x + 2
44
+ end
38
45
  end
39
46
 
40
- person({:name => "Adit", :age => 25, :bar => 45})
47
+ # this doesn't work
48
+ # p Object.send(:sum, 1, 2)
49
+
50
+ # but this does:
51
+ # p send(:sum, 1, 2)
52
+ #
53
+ # why???
54
+
55
+ Testable.check_all
56
+ # Testable.check(method(:double))
57
+
58
+ # p Object.test(5)
59
+ #
60
+ Object.Hello
data/lib/testable.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Contracts
2
+ class Testable
3
+ # Given an array-of-arrays of arguments,
4
+ # gives you the product of those arguments so that
5
+ # each possible combination is tried.
6
+ # Example: <tt>[[1, 2], [3, 4]]</tt> would give you:
7
+ #
8
+ # [[1, 3], [1, 4], [2, 3], [2, 4]]
9
+ def self.product(arrays)
10
+ arrays.inject { |acc, x|
11
+ acc.product(x)
12
+ }.flatten(arrays.size - 2)
13
+ end
14
+
15
+ # Given a contract, tells if you it's testable
16
+ def self.testable?(contract)
17
+ if contract.respond_to?(:testable?)
18
+ contract.testable?
19
+ else
20
+ contract.respond_to?(:new) && contract.method(:new).arity == 0
21
+ end
22
+ end
23
+
24
+ # Given a contract, returns the test data associated with that contract
25
+ def self.test_data(contract)
26
+ if contract.respond_to?(:testable?)
27
+ contract.test_data
28
+ else
29
+ contract.new
30
+ end
31
+ end
32
+
33
+ # TODO Should work on whatever class it was invoked on, no?
34
+ def self.check_all
35
+ o = Object.new
36
+ Object.decorated_methods.each do |name, contracts|
37
+ check(o.method(name))
38
+ end
39
+ end
40
+
41
+ def self.check(meth)
42
+ contracts = meth.owner.decorated_methods[meth.name.to_sym][0].contracts
43
+ arg_contracts = contracts[0, contracts.size - 1]
44
+ return_val = contracts[-1]
45
+ checkable = arg_contracts.all? do |arg_contract|
46
+ Testable.testable?(arg_contract)
47
+ end
48
+
49
+ if checkable
50
+ print "Checking #{meth.name}..."
51
+ _test_data = arg_contracts.map do |arg_contract|
52
+ data = Testable.test_data(arg_contract)
53
+ data.is_a?(Array) ? data : [data]
54
+ end
55
+ test_data = Testable.product _test_data
56
+ test_data.each do |args|
57
+ if args.is_a? Hash
58
+ # because *hash destroys the hash
59
+ res = meth.call(args)
60
+ else
61
+ res = meth.call(*args)
62
+ end
63
+ Contract.valid?(res, return_val)
64
+ end
65
+ puts "#{test_data.size} tests run."
66
+ end
67
+ end
68
+ end
69
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contracts
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 6
10
- version: 0.0.6
9
+ - 7
10
+ version: 0.0.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - Aditya Bhargava
@@ -33,6 +33,7 @@ files:
33
33
  - lib/decorators.rb
34
34
  - lib/foo.rb
35
35
  - lib/test.rb
36
+ - lib/testable.rb
36
37
  has_rdoc: true
37
38
  homepage: http://github.com/egonSchiele/contracts.ruby
38
39
  licenses: []