contracts 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,40 +1,73 @@
1
+ =begin rdoc
2
+ This module contains all the builtin contracts.
3
+ If you want to use them, first:
4
+
5
+ import Contracts
6
+
7
+ And then use these or write your own!
8
+
9
+ A simple example:
10
+
11
+ Contract Num, Num, Num
12
+ def add(a, b)
13
+ a + b
14
+ end
15
+
16
+ The contract is <tt>Contract Num, Num, Num</tt>. That says that the +add+ function takes two numbers and returns a number.
17
+ =end
1
18
  module Contracts
19
+ # Check that an argument is +Numeric+.
2
20
  class Num
3
21
  def self.valid? val
4
22
  val.is_a? Numeric
5
23
  end
6
24
  end
7
25
 
26
+ # Check that an argument is a positive number.
8
27
  class Pos
9
28
  def self.valid? val
10
29
  val > 0
11
30
  end
12
31
  end
13
32
 
33
+ # Check that an argument is a negative number.
14
34
  class Neg
15
35
  def self.valid? val
16
36
  val < 0
17
37
  end
18
38
  end
19
39
 
40
+ # Passes for any argument.
20
41
  class Any
21
42
  def self.valid? val
22
43
  true
23
44
  end
24
45
  end
25
46
 
47
+ # Fails for any argument.
26
48
  class None
27
49
  def self.valid? val
28
50
  false
29
51
  end
30
52
  end
31
53
 
54
+ # Use this when you are writing your own contract classes.
55
+ # Allows your contract to be called with <tt>[]</tt> instead of <tt>.new</tt>:
56
+ #
57
+ # Old: <tt>Or.new(param1, param2)</tt>
58
+ #
59
+ # New: <tt>Or[param1, param2]</tt>
60
+ #
61
+ # Of course, <tt>.new</tt> still works.
32
62
  class CallableClass
33
63
  def self.[](*vals)
34
64
  self.new(*vals)
35
65
  end
36
66
  end
37
67
 
68
+ # Takes a variable number of contracts.
69
+ # The contract passes if any of the contracts pass.
70
+ # Example: <tt>Or[Fixnum, Float]</tt>
38
71
  class Or < CallableClass
39
72
  def initialize(*vals)
40
73
  @vals = vals
@@ -52,6 +85,9 @@ module Contracts
52
85
  end
53
86
  end
54
87
 
88
+ # Takes a variable number of contracts.
89
+ # The contract passes if exactly one of those contracts pass.
90
+ # Example: <tt>Xor[Fixnum, Float]</tt>
55
91
  class Xor < CallableClass
56
92
  def initialize(*vals)
57
93
  @vals = vals
@@ -70,6 +106,9 @@ module Contracts
70
106
  end
71
107
  end
72
108
 
109
+ # Takes a variable number of contracts.
110
+ # The contract passes if all contracts pass.
111
+ # Example: <tt>And[Fixnum, Float]</tt>
73
112
  class And < CallableClass
74
113
  def initialize(*vals)
75
114
  @vals = vals
@@ -87,6 +126,10 @@ module Contracts
87
126
  end
88
127
  end
89
128
 
129
+ # Takes a variable number of method names as symbols.
130
+ # The contract passes if the argument responds to all
131
+ # of those methods.
132
+ # Example: <tt>RespondsTo[:password, :credit_card]</tt>
90
133
  class RespondsTo < CallableClass
91
134
  def initialize(*meths)
92
135
  @meths = meths
@@ -103,6 +146,11 @@ module Contracts
103
146
  end
104
147
  end
105
148
 
149
+ # Takes a variable number of method names as symbols.
150
+ # Given an argument, all of those methods are called
151
+ # on the argument one by one. If they all return true,
152
+ # the contract passes.
153
+ # Example: <tt>Send[:valid?]</tt>
106
154
  class Send < CallableClass
107
155
  def initialize(*meths)
108
156
  @meths = meths
@@ -119,6 +167,8 @@ module Contracts
119
167
  end
120
168
  end
121
169
 
170
+ # Takes a class +A+. If argument.is_a? +A+, the contract passes.
171
+ # Example: <tt>IsA[Numeric]</tt>
122
172
  class IsA < CallableClass
123
173
  def initialize(cls)
124
174
  @cls = cls
@@ -133,6 +183,9 @@ module Contracts
133
183
  end
134
184
  end
135
185
 
186
+ # Takes a variable number of contracts. The contract
187
+ # passes if all of those contracts fail for the given argument.
188
+ # Example: <tt>Not[nil]</tt>
136
189
  class Not < CallableClass
137
190
  def initialize(*vals)
138
191
  @vals = vals
@@ -150,6 +203,10 @@ module Contracts
150
203
  end
151
204
  end
152
205
 
206
+ # Takes a contract. The related argument must be an array.
207
+ # Checks the contract against every element of the array.
208
+ # If it passes for all elements, the contract passes.
209
+ # Example: <tt>ArrayOf[Num]</tt>
153
210
  class ArrayOf < CallableClass
154
211
  def initialize(contract)
155
212
  @contract = contract
@@ -166,4 +223,19 @@ module Contracts
166
223
  "an array of #{@contract}"
167
224
  end
168
225
  end
226
+
227
+ # Used for <tt>*args</tt> (variadic functions). Takes a contract
228
+ # and uses it to validate every element passed in
229
+ # through <tt>*args</tt>.
230
+ # Example: <tt>Args[Or[String, Num]]</tt>
231
+ class Args < CallableClass
232
+ attr_reader :contract
233
+ def initialize(contract)
234
+ @contract = contract
235
+ end
236
+
237
+ def to_s
238
+ "Args[#{@contract}]"
239
+ end
240
+ end
169
241
  end
data/lib/contracts.rb CHANGED
@@ -5,7 +5,12 @@ class Class
5
5
  include MethodDecorators
6
6
  end
7
7
 
8
-
8
+ # This is the main Contract class. When you write a new contract, you'll
9
+ # write it as:
10
+ #
11
+ # Contract [contract names]
12
+ #
13
+ # This class also provides useful callbacks and a validation method.
9
14
  class Contract < Decorator
10
15
  attr_accessor :contracts, :klass, :method
11
16
  decorator_name :contract
@@ -13,87 +18,59 @@ class Contract < Decorator
13
18
  @klass, @method, @contracts = klass, method, contracts
14
19
  end
15
20
 
16
- def self.mkerror(validates, arg, contract)
17
- if validates
18
- [true, {}]
19
- else
20
- [false, { :arg => arg, :contract => contract }]
21
- end
22
- end
23
-
21
+ # Given a hash, prints out a failure message.
22
+ # This function is used by the default #failure_callback method
23
+ # and uses the hash passed into the failure_callback method.
24
24
  def self.failure_msg(data)
25
- # TODO __file__ and __line__ won't work in Ruby 1.9.
26
- # It provides a source_location method instead.
27
25
  expected = if data[:contract].to_s == ""
28
26
  data[:contract].inspect
29
27
  else
30
28
  data[:contract].to_s
31
29
  end
30
+
31
+ if RUBY_VERSION =~ /^1\.8/
32
+ position = data[:method].__file__ + ":" + data[:method].__line__.to_s
33
+ else
34
+ file, line = data[:method].source_location
35
+ position = file + ":" + line.to_s
36
+ end
37
+
32
38
  %{Contract violation:
33
39
  Expected: #{expected},
34
40
  Actual: #{data[:arg].inspect}
35
41
  Value guarded in: #{data[:class]}::#{data[:method].name}
36
42
  With Contract: #{data[:contracts].map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ") }
37
- At: #{data[:method].__file__}:#{data[:method].__line__} }
43
+ At: #{position} }
38
44
  end
45
+
46
+ # Callback for when a contract fails. By default it raises
47
+ # an error and prints detailed info about the contract that
48
+ # failed. You can also monkeypatch this callback to do whatever
49
+ # you want...log the error, send you an email, print an error
50
+ # message, etc.
51
+ #
52
+ # Example of monkeypatching:
53
+ #
54
+ # Contract.failure_callback(data)
55
+ # puts "You had an error!"
56
+ # puts failure_msg(data)
57
+ # exit
58
+ # end
39
59
  def self.failure_callback(data)
40
60
  raise failure_msg(data)
41
61
  end
42
62
 
63
+ # Callback for when a contract succeeds. Does nothing by default.
43
64
  def self.success_callback(data)
44
65
  end
45
66
 
46
- def self.validate_hash(arg, contract)
47
- arg.keys.each do |k|
48
- result, info = validate(arg[k], contract[k])
49
- return [result, info] unless result
50
- end
51
- end
52
-
53
- def self.validate_proc(arg, contract)
54
- mkerror(contract[arg], arg, contract)
55
- end
56
-
57
- def self.validate_class(arg, contract)
58
- valid = if contract.respond_to? :valid?
59
- contract.valid? arg
60
- else
61
- contract == arg.class
62
- end
63
- mkerror(valid, arg, contract)
64
- end
65
-
66
- def self.validate_all(args, contracts, klass, method)
67
- if args.size > contracts.size - 1
68
- # *args
69
- if contracts[-2].is_a? Args
70
- while contracts.size < args.size + 1
71
- contracts.insert(-2, contracts[-2].dup)
72
- end
73
- else
74
- raise %{The number of arguments doesn't match the number of contracts.
75
- Did you forget to write a contract for the return value of the function?
76
- Or if you want a variable number of arguments using *args, use the Args contract.
77
- Args: #{args.inspect}
78
- Contracts: #{contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ")}}
79
- end
80
- end
81
-
82
- args.zip(contracts).each do |arg, contract|
83
- validate(arg, contract, klass, method, contracts)
84
- end
85
- end
86
-
87
- def self.validate(arg, contract, klass, method, contracts)
88
- result, _ = valid?(arg, contract)
89
- if result
90
- success_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
91
- else
92
- failure_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
93
- end
94
- end
95
-
96
- # arg to method -> contract it should satisfy -> (Boolean, metadata)
67
+ # Used to verify if an argument satisfies a contract.
68
+ #
69
+ # Takes: an argument and a contract.
70
+ #
71
+ # Returns: a tuple: [Boolean, metadata]. The boolean indicates
72
+ # whether the contract was valid or not. If it wasn't, metadata
73
+ # contains some useful information about the failure.
97
74
  def self.valid?(arg, contract)
98
75
  case contract
99
76
  when Class
@@ -111,7 +88,7 @@ Contracts: #{contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join("
111
88
  # e.g. { :a => Num, :b => String }
112
89
  return mkerror(false, arg, contract) unless arg.is_a?(Hash)
113
90
  validate_hash(arg, contract)
114
- when Args
91
+ when Contracts::Args
115
92
  valid? arg, contract.contract
116
93
  else
117
94
  if contract.respond_to? :valid?
@@ -123,22 +100,78 @@ Contracts: #{contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join("
123
100
  end
124
101
 
125
102
  def call(this, *args, &blk)
126
- Contract.validate_all(args, @contracts, @klass, @method)
103
+ _args = blk ? args + [blk] : args
104
+ if _args.size != @contracts.size - 1
105
+ # so it's not *args
106
+ if !@contracts[-2].is_a? Contracts::Args
107
+ raise %{The number of arguments doesn't match the number of contracts.
108
+ Did you forget to write a contract for the return value of the function?
109
+ Or if you want a variable number of arguments using *args, use the Args contract.
110
+ Args: #{args.inspect}
111
+ Contracts: #{@contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ")}}
112
+ end
113
+ end
114
+ Contract.validate_all(_args, @contracts[0, @contracts.size - 1], @klass, @method)
115
+
127
116
  result = @method.bind(this).call(*args, &blk)
117
+
128
118
  if args.size == @contracts.size - 1
129
119
  Contract.validate(result, @contracts[-1], @klass, @method, @contracts)
130
120
  end
131
121
  result
132
122
  end
133
- end
134
123
 
135
- class Args < Contracts::CallableClass
136
- attr_reader :contract
137
- def initialize(contract)
138
- @contract = contract
124
+ private
125
+
126
+ def self.mkerror(validates, arg, contract)
127
+ if validates
128
+ [true, {}]
129
+ else
130
+ [false, { :arg => arg, :contract => contract }]
131
+ end
139
132
  end
140
133
 
141
- def to_s
142
- "Args[#{@contract}]"
134
+ def self.validate_hash(arg, contract)
135
+ arg.keys.each do |k|
136
+ result, info = validate(arg[k], contract[k])
137
+ return [result, info] unless result
138
+ end
139
+ end
140
+
141
+ def self.validate_proc(arg, contract)
142
+ mkerror(contract[arg], arg, contract)
143
+ end
144
+
145
+ def self.validate_class(arg, contract)
146
+ valid = if contract.respond_to? :valid?
147
+ contract.valid? arg
148
+ else
149
+ contract == arg.class
150
+ end
151
+ mkerror(valid, arg, contract)
152
+ end
153
+
154
+ def self.validate_all(args, contracts, klass, method)
155
+ # we assume that any mismatch in # of args/contracts
156
+ # has been checked befoer this point.
157
+ if args.size != contracts.size
158
+ # assumed: contracts[-1].is_a? Args
159
+ while contracts.size < args.size
160
+ contracts << contracts[-1].dup
161
+ end
162
+ end
163
+
164
+ args.zip(contracts).each do |arg, contract|
165
+ validate(arg, contract, klass, method, contracts)
166
+ end
167
+ end
168
+
169
+ def self.validate(arg, contract, klass, method, contracts)
170
+ result, _ = valid?(arg, contract)
171
+ if result
172
+ success_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
173
+ else
174
+ failure_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
175
+ end
143
176
  end
144
177
  end
data/lib/test.rb CHANGED
@@ -24,8 +24,6 @@ class Object
24
24
  func.call
25
25
  end
26
26
 
27
- # thinks there are too many args, throws error
28
- # sidenote: there should be a check to make sure the # of args and contracts match up.
29
27
  Contract Args[Num], Num
30
28
  def sum(*vals)
31
29
  vals.inject(0) do |acc, v|
@@ -38,4 +36,4 @@ run {
38
36
  puts "hi!"
39
37
  }
40
38
 
41
- puts add(1, 2)
39
+ puts sum(1, 2, 3, 4)
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: 27
4
+ hash: 25
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 2
10
- version: 0.0.2
9
+ - 3
10
+ version: 0.0.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Aditya Bhargava