contracts 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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