ruby-contract 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/COPYING +56 -0
  2. data/Manifest +85 -0
  3. data/README +32 -0
  4. data/TODO +83 -0
  5. data/doc/classes/Contract.html +599 -0
  6. data/doc/classes/Contract/Check.html +229 -0
  7. data/doc/classes/Contract/Check/All.html +172 -0
  8. data/doc/classes/Contract/Check/Any.html +172 -0
  9. data/doc/classes/Contract/Check/Block.html +172 -0
  10. data/doc/classes/Contract/Check/None.html +173 -0
  11. data/doc/classes/Contract/Check/Quack.html +172 -0
  12. data/doc/classes/Contract/ContractError.html +151 -0
  13. data/doc/classes/Contract/ContractException.html +162 -0
  14. data/doc/classes/Contract/ContractMismatch.html +134 -0
  15. data/doc/classes/Kernel.html +256 -0
  16. data/doc/classes/Method.html +135 -0
  17. data/doc/classes/MethodSignatureMixin.html +208 -0
  18. data/doc/classes/Module.html +526 -0
  19. data/doc/created.rid +1 -0
  20. data/doc/dot/f_0.dot +14 -0
  21. data/doc/dot/f_0.png +0 -0
  22. data/doc/dot/f_1.dot +14 -0
  23. data/doc/dot/f_1.png +0 -0
  24. data/doc/dot/f_2.dot +14 -0
  25. data/doc/dot/f_2.png +0 -0
  26. data/doc/dot/f_3.dot +112 -0
  27. data/doc/dot/f_3.png +0 -0
  28. data/doc/dot/f_4.dot +62 -0
  29. data/doc/dot/f_4.png +0 -0
  30. data/doc/dot/f_5.dot +62 -0
  31. data/doc/dot/f_5.png +0 -0
  32. data/doc/dot/f_6.dot +224 -0
  33. data/doc/dot/f_6.png +0 -0
  34. data/doc/dot/f_6_0.dot +24 -0
  35. data/doc/dot/f_6_0.png +0 -0
  36. data/doc/dot/f_6_1.dot +24 -0
  37. data/doc/dot/f_6_1.png +0 -0
  38. data/doc/dot/f_7.dot +62 -0
  39. data/doc/dot/f_7.png +0 -0
  40. data/doc/files/COPYING.html +168 -0
  41. data/doc/files/README.html +146 -0
  42. data/doc/files/TODO.html +240 -0
  43. data/doc/files/lib/contract/assertions_rb.html +118 -0
  44. data/doc/files/lib/contract/exception_rb.html +125 -0
  45. data/doc/files/lib/contract/integration_rb.html +130 -0
  46. data/doc/files/lib/contract/overrides_rb.html +118 -0
  47. data/doc/files/lib/contract_rb.html +127 -0
  48. data/doc/fr_class_index.html +40 -0
  49. data/doc/fr_file_index.html +34 -0
  50. data/doc/fr_method_index.html +45 -0
  51. data/doc/index.html +24 -0
  52. data/doc/rdoc-style.css +208 -0
  53. data/lib/contract.rb +146 -0
  54. data/lib/contract/assertions.rb +42 -0
  55. data/lib/contract/exception.rb +95 -0
  56. data/lib/contract/integration.rb +664 -0
  57. data/lib/contract/overrides.rb +41 -0
  58. data/setup.rb +1360 -0
  59. data/test/coverage/_-lib-contract-assertions_rb.html +526 -0
  60. data/test/coverage/_-lib-contract-exception_rb.html +632 -0
  61. data/test/coverage/_-lib-contract-integration_rb.html +1450 -0
  62. data/test/coverage/_-lib-contract-overrides_rb.html +524 -0
  63. data/test/coverage/_-lib-contract_rb.html +724 -0
  64. data/test/coverage/__-lib-contract-assertions_rb.html +484 -0
  65. data/test/coverage/__-lib-contract-exception_rb.html +537 -0
  66. data/test/coverage/__-lib-contract-integration_rb.html +946 -0
  67. data/test/coverage/__-lib-contract-overrides_rb.html +483 -0
  68. data/test/coverage/__-lib-contract_rb.html +583 -0
  69. data/test/coverage/index.html +93 -0
  70. data/test/tc_all.rb +8 -0
  71. data/test/tc_contract.rb +109 -0
  72. data/test/tc_exception.rb +43 -0
  73. data/test/tc_integration.rb +357 -0
  74. metadata +136 -0
@@ -0,0 +1,146 @@
1
+ # Contains the main implementation of the Contract class.
2
+ # :main:Contract
3
+
4
+ require 'test/unit/testcase'
5
+ require 'test/unit/testresult'
6
+ require 'test/unit/testsuite'
7
+
8
+ require 'contract/exception'
9
+ require 'contract/overrides'
10
+ require 'contract/assertions'
11
+ require 'contract/integration'
12
+
13
+ # Represents a contract between Objects as a collection of test cases.
14
+ # Objects are said to fulfill a contract if all test cases suceed. This
15
+ # is useful for ensuring that Objects your code is getting behave in a
16
+ # way that you expect them to behave so you can fail early or execute
17
+ # different logic for Objects with different interfaces.
18
+ #
19
+ # The tests of the test suite will be run on a copy of the tested Object
20
+ # so you can safely test its behavior without having to fear data loss.
21
+ # By default Contracts obtain deep copies of Objects by serializing and
22
+ # unserializing them with Ruby's +Marshal+ functionality. This will work
23
+ # in most cases but can fail for Objects containing unserializable parts
24
+ # like Procs, Files or Sockets. In those cases it is currently of your
25
+ # responsibility to provide a fitting implementation by overwriting the
26
+ # Contract.deep_copy method. In the future the contract library might
27
+ # provide different implementations of it via Ruby's mixin mechanism.
28
+ class Contract < Test::Unit::TestCase
29
+ id = %q$Id: contract.rb 120 2005-02-11 20:39:14Z flgr $
30
+ current_version = id.split(" ")[2]
31
+ unless defined?(Version)
32
+ # The Version of the contract library you are using as String of the
33
+ # 1.2.3 form where the digits stand for release, major and minor
34
+ # version respectively.
35
+ Version = "0.1.1"
36
+ end
37
+
38
+ # Returns true if the given object fulfills this contract.
39
+ # This is useful for implementing dispatching mechanisms where you
40
+ # want to hit different code branches based on whether an Object has
41
+ # one or another interface.
42
+ def self.fulfilled_by?(object)
43
+ self.test(object).nil?
44
+ end
45
+
46
+ class << self
47
+ # You can use contracts in +case+ ... +when+ statements or in
48
+ # Module#signature checks. For example:
49
+ # case obj
50
+ # when Numeric then obj + 1
51
+ # when ListContract then obj + [1]
52
+ # end
53
+ alias :=== :fulfilled_by?
54
+ end
55
+
56
+ # Enforces that object implements this contract. If it does not an
57
+ # Exception will be raised. This is useful for example useful when you
58
+ # need to ensure that the arguments given to a method fulfill a given
59
+ # contract.
60
+ #
61
+ # Note that using Module#enforce is a higher-level way of checking
62
+ # arguments and return values for the conformance of a given type. You
63
+ # might however still want to use Contract.enforce directly when you
64
+ # need more flexibility.
65
+ def self.enforce(object)
66
+ reason = self.test(object)
67
+ raise reason if reason
68
+ end
69
+
70
+ # Tests whether the given Object fulfils this contract.
71
+ #
72
+ # Note: This will return the first reason for the Object not fulfilling
73
+ # the contract or +nil+ in case it fulfills it.
74
+ def self.test(object, return_all = false)
75
+ reasons = []
76
+
77
+ result = Test::Unit::TestResult.new
78
+ result.add_listener(Test::Unit::TestResult::FAULT) do |fault|
79
+ reason = Contract.fault_to_exception(fault, object, self)
80
+ return reason unless return_all
81
+ reasons << reason
82
+ end
83
+
84
+ self.suite.run(result, deep_copy(object))
85
+
86
+ return reasons unless result.passed?
87
+ end
88
+
89
+ # Same as Contract.test, but will return all reasons for the Object not
90
+ # fulfilling the contract in an Array or nil in case of fulfillment.
91
+ # (as an Array of Exceptions) or +nil+ in the case it does fulfill it.
92
+ def self.test_all(object)
93
+ test(object, true)
94
+ end
95
+
96
+ # This method is used internally for getting a copy of Objects that
97
+ # the contract is checked against. By default it uses Ruby's +Marshal+
98
+ # functionality for obtaining a copy, but this can fail if the Object
99
+ # contains unserializable parts like Procs, Files or Sockets. It is
100
+ # currently your responsibility to provide a fitting implementation
101
+ # of this by overwriting the method in case the default implementation
102
+ # does not work for you. In the future the contract library might offer
103
+ # different implementations for this via Ruby's mixin mechanism.
104
+ def self.deep_copy(object)
105
+ Marshal.load(Marshal.dump(object))
106
+ end
107
+
108
+ # Fulfilling this Contract (via Module#fulfills) implies that the Object
109
+ # is automatically compatible with the specified mixins which will then
110
+ # be included automatically. For example the Enumerable relationship
111
+ # could be expressed like this:
112
+ #
113
+ # class EnumerableContract < Contract
114
+ # provides :each
115
+ # implies Enumerable
116
+ # end
117
+ def self.implies(*mixins)
118
+ mixins.each do |mixin|
119
+ if not mixin.is_a?(Module) then
120
+ raise(TypeError, "wrong argument type #{mixin.class} for " +
121
+ "#{mixin.inspect} (expected Module)")
122
+ end
123
+ end
124
+
125
+ @implications ||= Array.new
126
+ @implications += mixins
127
+ end
128
+
129
+ class << self
130
+ # Returns all implications of a given contract that were stated via
131
+ # calling Contract.implies.
132
+ attr_reader :implications
133
+ end
134
+
135
+ def self.implications() # :nodoc:
136
+ @implications ||= Array.new
137
+
138
+ ancestors[1 .. -1].inject(@implications) do |result, ancestor|
139
+ if ancestor.respond_to?(:implications) then
140
+ ancestor.implications + result
141
+ else
142
+ result
143
+ end
144
+ end.uniq
145
+ end
146
+ end
@@ -0,0 +1,42 @@
1
+ # This file provides a few new assertions and class methods for expressing
2
+ # contracts in a comfortable way that does not require manually writing test
3
+ # methods.
4
+ #
5
+ # See Contract.provides.
6
+
7
+
8
+ class Contract < Test::Unit::TestCase
9
+ # Tests that the tested Object provides the specified methods with the
10
+ # specified behavior.
11
+ #
12
+ # If a block is supplied it will be evaluated in the context of the
13
+ # contract so <code>@object</code> will refer to the object being tested.
14
+ #
15
+ # This can be used like this:
16
+ # class ListContract < Contract
17
+ # provides :size do
18
+ # assert(@object.size >= 0, "#size should never be negative.")
19
+ # end
20
+ #
21
+ # provides :include?
22
+ #
23
+ # provides :each do
24
+ # count = 0
25
+ # @object.each do |item|
26
+ # assert(@object.include?(item),
27
+ # "#each should only yield items that the list includes.")
28
+ # count += 1
29
+ # end
30
+ # assert_equal(@object.size, count,
31
+ # "#each should yield #size items.")
32
+ # end
33
+ # end
34
+ def self.provides(*symbols, &block) # :yields:
35
+ symbols.each do |symbol|
36
+ define_method("test_provides_#{symbol}".intern) do
37
+ assert_respond_to(@object, symbol)
38
+ instance_eval(&block) if block
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,95 @@
1
+ # This file contains things needed to map Test::Unit status objects to
2
+ # Exceptions, Exception related infrastructure and similar things.
3
+ #
4
+ # See Contract::ContractException, Contract::ContractMismatch and
5
+ # Contract::ContractError.
6
+
7
+
8
+ class Contract < Test::Unit::TestCase
9
+ # Exceptions raised by Contract contain some useful meta-information.
10
+ # This module is mixed into Exceptions that provide such information.
11
+ module ContractException
12
+ def ce_initialize(original_message, backtrace, test_object,
13
+ test_method, test_contract, *more) # :nodoc:
14
+ @original_message = original_message
15
+ message = original_message.dup
16
+ detail = " for #{test_object.inspect} in " +
17
+ "#{test_contract.inspect}"
18
+ message[/(\s*)[.!?]?\s*\Z/, 1] = detail
19
+ Exception.instance_method(:initialize).bind(self).call(message)
20
+
21
+ set_backtrace(backtrace)
22
+ @test_object = test_object
23
+ @test_method = test_method
24
+ @test_contract = test_contract
25
+ end
26
+ private :ce_initialize
27
+
28
+ # What object was tested when this Exception was raised?
29
+ attr_reader :test_object
30
+ # What method implemented that test?
31
+ attr_reader :test_method
32
+ # What contract was that method part of?
33
+ attr_reader :test_contract
34
+ # The original, unfiltered exception message.
35
+ attr_reader :original_message
36
+ end
37
+
38
+ # Represents a failed test in a contract. This means that an object
39
+ # simply does not fulfill one of the parts of a contract. Subclass of
40
+ # TypeError.
41
+ class ContractMismatch < TypeError
42
+ def initialize(*args) # :nodoc:
43
+ ce_initialize(*args)
44
+ end
45
+
46
+ include ContractException
47
+ end
48
+
49
+ # Represents an unexpected failure while processing a contract test.
50
+ # This is more critical than ContractMismatch and usually means that
51
+ # something is wrong with the test itself.
52
+ class ContractError < StandardError
53
+ def initialize(*args) # :nodoc:
54
+ @type = args.pop
55
+ ce_initialize(*args)
56
+ end
57
+
58
+ # The type of the original Exception that triggered the unexpected
59
+ # failure.
60
+ attr_reader :type
61
+
62
+ include ContractException
63
+ end
64
+
65
+
66
+ # Maps a Test::Unit::Failure instance to an actual Exception with the
67
+ # specified meta data.
68
+ def self.failure_to_exception(failure, object, contract) # :nodoc:
69
+ ContractMismatch.new(failure.message, failure.location, object,
70
+ extract_method_name(failure.test_name), contract)
71
+ end
72
+
73
+ # Maps a Test::Unit::Error instance to an actual Exception with the
74
+ # specified meta data.
75
+ def self.error_to_exception(error, object, contract) # :nodoc:
76
+ original = error.exception
77
+ ContractError.new(original.message, original.backtrace, object,
78
+ extract_method_name(error.test_name), contract, original.class)
79
+ end
80
+
81
+ # Maps a Test::Unit fault (either a Failure or Error) to an actual
82
+ # exception with the specified meta data.
83
+ def self.fault_to_exception(fault, *args) # :nodoc:
84
+ if fault.is_a?(Test::Unit::Failure) then
85
+ failure_to_exception(fault, *args)
86
+ else
87
+ error_to_exception(fault, *args)
88
+ end
89
+ end
90
+
91
+ # Extracts the method name from a Test::Unit test_name style String.
92
+ def self.extract_method_name(test_name) # :nodoc:
93
+ test_name[/\A(.+?)\(.+?\)\Z/, 1]
94
+ end
95
+ end
@@ -0,0 +1,664 @@
1
+ # This file contains code for integrating the contract library with built-in
2
+ # classes and methods.
3
+ #
4
+ # See Contract::Check, Module#signature and Module#fulfills.
5
+
6
+
7
+ class Contract < Test::Unit::TestCase
8
+ # Implements checks that can for example be used in Module#signature
9
+ # specifications. They are implemented simply by overriding the === case
10
+ # equality operator. They can also be nested like this:
11
+ # # Matches something that is an Enumerable and that responds to
12
+ # # either :to_ary or :to_a.
13
+ # signature :x, Contract::Check::All[
14
+ # Enumerable,
15
+ # Contract::Check::Any[
16
+ # Contract::Check::Quack[:to_a],
17
+ # Contract::Check::Quack[:to_ary]
18
+ # ]
19
+ # ]
20
+ module Check
21
+ # An abstract Base class for Contract::Check classes.
22
+ # Contains logic for instantation.
23
+ class Base # :nodoc:
24
+ class << self
25
+ alias :[] :new
26
+ end
27
+
28
+ def initialize(*args, &block)
29
+ @args, @block = args, block
30
+ end
31
+ end
32
+
33
+ # Checks that the specified block matches.
34
+ # Example:
35
+ # signature :x, Contract::Check.block { |arg| arg > 0 }
36
+ class Block < Base
37
+ def ===(other)
38
+ @block.call(other)
39
+ end
40
+ end
41
+ # Short-cut for creating a Contract::Check::Block.
42
+ def self.block(&block) # :yields: arg
43
+ Block.new(&block)
44
+ end
45
+
46
+ # Checks that all the specified methods are answered.
47
+ # Example:
48
+ # signature :x, Contract::Check::Quack[:to_sym]
49
+ class Quack < Base
50
+ def ===(other)
51
+ @args.all? { |arg| other.respond_to?(arg) }
52
+ end
53
+ end
54
+
55
+ # Checks that all the specified conditions match.
56
+ # Example:
57
+ # signature :x, Contract::Check::All[Array, Enumerable]
58
+ class All < Base
59
+ def ===(other)
60
+ @args.all? { |arg| arg === other }
61
+ end
62
+ end
63
+ # Alias for Contract::Check::All
64
+ And = All unless defined?(And)
65
+
66
+ # Checks that at least one of the specified conditions match.
67
+ # Example:
68
+ # signature :x, Contract::Check::Any[String, Symbol]
69
+ class Any < Base
70
+ def ===(other)
71
+ @args.any? { |arg| arg === other }
72
+ end
73
+ end
74
+ # Alias for Contract::Check::Any
75
+ Or = Any unless defined?(Or)
76
+
77
+ # Checks that none of the specified conditions match.
78
+ # Example:
79
+ # signature :x, Contract::Check::None[Numeric, Symbol]
80
+ # signature :x, Contract::Check::Not[Comparable]
81
+ class None < Base
82
+ def ===(other)
83
+ not @args.any? { |arg| arg === other }
84
+ end
85
+ end
86
+ # Alias for Contract::Check::None
87
+ Not = None unless defined?(Not)
88
+ end
89
+
90
+ class << self
91
+ # Whether signatures should be checked. By default signatures are checked
92
+ # only when the application is run in $DEBUG mode. (By specifying the -d
93
+ # switch on the invocation of Ruby.)
94
+ #
95
+ # Note: If you want to change this you need to do so before doing any
96
+ # Module#signature calls or it will not be applied. It's probably best
97
+ # set right after requiring the contract library.
98
+ attr_accessor :check_signatures
99
+ alias :check_signatures? :check_signatures
100
+
101
+ # Whether fulfills should be checked. This is enabled by default.
102
+ #
103
+ # Note: If you want to change this you need to do so before doing any
104
+ # Module#fulfills calls or it will not be applied. It's probably best
105
+ # set right after requiring the contract library.
106
+ attr_accessor :check_fulfills
107
+ alias :check_fulfills? :check_fulfills
108
+
109
+ # All adaption routes.
110
+ attr_accessor :adaptions # :nodoc:
111
+ end
112
+ self.check_signatures = $DEBUG if self.check_signatures.nil?
113
+ self.check_fulfills = true if self.check_fulfills.nil?
114
+ if self.adaptions.nil? then
115
+ self.adaptions = Hash.new { |hash, key| hash[key] = Array.new }
116
+ end
117
+
118
+ # Tries to adapt the specified object to the specified type.
119
+ # Returns the old object if no suitable adaption route was found or if
120
+ # it already is of the specified type.
121
+ #
122
+ # This will only use adaptions where the :to part is equal to the specified
123
+ # type. No multi-step conversion will be performed.
124
+ def self.adapt(object, type)
125
+ return object if type === object
126
+
127
+ @adaptions[type].each do |adaption|
128
+ if adaption[:from] === object and
129
+ (adaption[:if].nil? or adaption[:if] === object)
130
+ then
131
+ result = adaption[:via].call(object)
132
+ return result if type === result
133
+ end
134
+ end
135
+
136
+ return object
137
+ end
138
+ end
139
+
140
+
141
+ class Module
142
+ # Checks that the arguments and return value of a method match the specified
143
+ # signature. Checks are only actually done when Contract.check_signatures is
144
+ # set to true or if the <code>:no_adaption</code> option is +false+. The
145
+ # method will return +true+ in case it actually inserted the signature check
146
+ # logic and +nil+ in case it didn't.
147
+ #
148
+ # You will usually specify one type specifier (<code>:any</code> which will
149
+ # allow anything to appear at that position of the argument list or something
150
+ # that implements the === case equality operator -- samples are Contracts,
151
+ # Ranges, Classes, Modules, Regexps, Contract::Checks and so on) per argument.
152
+ # You can also use objects that implement the +call+ method as type specifiers
153
+ # which includes Methods and Procs.
154
+ #
155
+ # If you don't use the <code>:repeated</code> or <code>:allow_trailing</code>
156
+ # options the method will take exactly as many arguments as there are type
157
+ # specifiers which means that <code>signature :a_method</code> enforces
158
+ # +a_method+ having exactly zero arguments.
159
+ #
160
+ # The checks are done by wrapping the type checks around the method.
161
+ # ArgumentError exceptions will be raised in case the signature contract is
162
+ # not adhered to by your caller.
163
+ #
164
+ # An ArgumentError exception will be raised in case the methods natural
165
+ # argument list size and the signature you specified via Module.signature are
166
+ # incompatible. (Note that they don't have to be completely equivalent, you
167
+ # can still have a method taking zero or more arguments and apply a signature
168
+ # that limits the actual argument count to three arguments.)
169
+ #
170
+ # This method can take quite a few options. Here's a complete list:
171
+ #
172
+ # <code>:return</code>::
173
+ # A return type that the method must comply to. Note that this check (if
174
+ # failed) will actually raise a StandardError instead of an ArgumentError
175
+ # because the failure likely lies in the method itself and not in what the
176
+ # caller did.
177
+ #
178
+ # <code>:block</code>::
179
+ # +true+ or +false+ -- whether the method must take a block or not. So
180
+ # specifying <code>:block => false</code> enforces that the method is not
181
+ # allowed to have a block supplied.
182
+ #
183
+ # <code>:allow_trailing</code>::
184
+ # +true+ or +false+ -- whether the argument list may contain trailing,
185
+ # unchecked arguments.
186
+ #
187
+ # <code>:optional</code>::
188
+ # An Array specifying optional arguments. These arguments are assumed to
189
+ # be after regular arguments, but *before* repeated ones. They will be
190
+ # checked if they are present, but don't actually have to be present.
191
+ #
192
+ # This could for example be useful for <code>File.open(name, mode)</code>
193
+ # where mode is optional, but has to be either an Integer or String.
194
+ #
195
+ # Note that all optional arguments will have to be specified if you want
196
+ # to use optional and repeated arguments.
197
+ #
198
+ # Specifying an empty Array is like not supplying the option at all.
199
+ #
200
+ # <code>:repeated</code>::
201
+ # An Array that specifies arguments of a method that will be repeated over
202
+ # and over again at the end of the argument list.
203
+ #
204
+ # A good sample of this are Array#values_at which takes zero or or more
205
+ # Numeric arguments and Enumerable#zip which takes zero or more other
206
+ # Enumerable arguments.
207
+ #
208
+ # Note that the Array that was associated with the <code>:repeated</code>
209
+ # option must not be empty or an ArgumentError exception will be raised.
210
+ # If there's just one repeated type you can omit the Array and directly
211
+ # specify the type identifier.
212
+ #
213
+ # The <code>:repeated</code> option overrides the
214
+ # <code>:allow_trailing</code> option. Combining them is thus quite
215
+ # meaningless.
216
+ #
217
+ # <code>:no_adaption</code>::
218
+ # +true+ or +false+ -- whether no type adaption should be performed.
219
+ #
220
+ # Usage:
221
+ # signature(:to_s) # no arguments
222
+ # signature(:+, :any) # one argument, type unchecked
223
+ # signature(:+, Fixnum) # one argument, type Fixnum
224
+ # signature(:+, NumericContract)
225
+ # signature(:+, 1 .. 10)
226
+ # signature(:sqrt, lambda { |arg| arg > 0 })
227
+ #
228
+ # signature(:each, :block => true) # has to have block
229
+ # signature(:to_i, :block => false) # not allowed to have block
230
+ # signature(:to_i, :result => Fixnum) # return value must be Fixnum
231
+ # signature(:zip, :allow_trailing => true) # unchecked trailing args
232
+ # signature(:zip, :repeated => [Enumerable]) # repeated trailing args
233
+ # signature(:zip, :repeated => Enumerable)
234
+ # # foo(3, 6, 4, 7) works; foo(5), foo(3, 2) etc. don't
235
+ # signature(:foo, :repeated => [1..4, 5..9])
236
+ # signature(:foo, :optional => [Numeric, String]) # two optional args
237
+ def signature(method, *args)
238
+ options = {}
239
+ signature = args.dup
240
+ options.update(signature.pop) if signature.last.is_a?(Hash)
241
+ method = method.to_sym
242
+
243
+ return if not Contract.check_signatures? and options[:no_adaption]
244
+
245
+ old_method = instance_method(method)
246
+ remove_method(method) if instance_methods(false).include?(method.to_s)
247
+
248
+ arity = old_method.arity
249
+ if arity != signature.size and
250
+ (arity >= 0 or signature.size < ~arity) then
251
+ raise(ArgumentError, "signature isn't compatible with arity")
252
+ end
253
+
254
+ # Normalizes specifiers to Objects that respond to === so that the run-time
255
+ # checks only have to deal with that case. Also checks that a specifier is
256
+ # actually valid.
257
+ convert_specifier = lambda do |item|
258
+ # Procs, Methods etc.
259
+ if item.respond_to?(:call) then
260
+ Contract::Check.block { |arg| item.call(arg) }
261
+ # Already okay
262
+ elsif item.respond_to?(:===) or item == :any then
263
+ item
264
+ # Unknown specifier
265
+ else
266
+ raise(ArgumentError, "unsupported argument specifier #{item.inspect}")
267
+ end
268
+ end
269
+
270
+ signature.map!(&convert_specifier)
271
+
272
+ if options.include?(:optional) then
273
+ options[:optional] = Array(options[:optional])
274
+ options[:optional].map!(&convert_specifier)
275
+ options.delete(:optional) if options[:optional].empty?
276
+ end
277
+
278
+ if options.include?(:repeated) then
279
+ options[:repeated] = Array(options[:repeated])
280
+ if options[:repeated].size == 0 then
281
+ raise(ArgumentError, "repeated arguments may not be an empty Array")
282
+ else
283
+ options[:repeated].map!(&convert_specifier)
284
+ end
285
+ end
286
+
287
+ if options.include?(:return) then
288
+ options[:return] = convert_specifier.call(options[:return])
289
+ end
290
+
291
+ # We need to keep around references to our arguments because we will
292
+ # need to access them via ObjectSpace._id2ref so that they do not
293
+ # get garbage collected.
294
+ @signatures ||= Hash.new { |hash, key| hash[key] = Array.new }
295
+ @signatures[method] << [signature, options, old_method]
296
+
297
+ adapted = Proc.new do |obj, type, assign_to|
298
+ if options[:no_adaption] then
299
+ obj
300
+ elsif assign_to then
301
+ %{(#{assign_to} = Contract.adapt(#{obj}, #{type}))}
302
+ else
303
+ %{Contract.adapt(#{obj}, #{type})}
304
+ end
305
+ end
306
+
307
+ # We have to use class_eval so that signatures can be specified for
308
+ # methods taking blocks in Ruby 1.8. (This will be obsolete in 1.9)
309
+ # We also make the checks as efficient as we can.
310
+ code = %{
311
+ def #{method}(*args, &block)
312
+ old_args = args.dup
313
+
314
+ #{if options.include?(:block) then
315
+ if options[:block] then
316
+ %{raise(ArgumentError, "no block given") unless block}
317
+ else
318
+ %{raise(ArgumentError, "block given") if block}
319
+ end
320
+ end
321
+ }
322
+
323
+ #{if not (options[:allow_trailing] or
324
+ options.include?(:repeated) or options.include?(:optional))
325
+ then
326
+ msg = "wrong number of arguments (\#{args.size} for " +
327
+ "#{signature.size})"
328
+ %{if args.size != #{signature.size} then
329
+ raise(ArgumentError, "#{msg}")
330
+ end
331
+ }
332
+ elsif options.include?(:optional) and
333
+ not options.include?(:allow_trailing) and
334
+ not options.include?(:repeated)
335
+ then
336
+ min = signature.size
337
+ max = signature.size + options[:optional].size
338
+ msg = "wrong number of arguments (\#{args.size} for " +
339
+ "#{min} upto #{max})"
340
+ %{unless args.size.between?(#{min}, #{max})
341
+ raise(ArgumentError, "#{msg}")
342
+ end
343
+ }
344
+ elsif signature.size > 0 then
345
+ msg = "wrong number of arguments (\#{args.size} for " +
346
+ "at least #{signature.size}"
347
+ %{if args.size < #{signature.size} then
348
+ raise(ArgumentError, "#{msg}")
349
+ end
350
+ }
351
+ end
352
+ }
353
+
354
+ #{index = 0
355
+ signature.map do |part|
356
+ next if part == :any
357
+ index += 1
358
+ msg = "argument #{index} (\#{arg.inspect}) does not match " +
359
+ "#{part.inspect}"
360
+ %{type = ObjectSpace._id2ref(#{part.object_id})
361
+ arg = args.shift
362
+ unless type === #{adapted[%{arg}, %{type}, %{old_args[#{index - 1}]}]}
363
+ raise(ArgumentError, "#{msg}")
364
+ end
365
+ }
366
+ end
367
+ }
368
+
369
+ #{%{catch(:args_exhausted) do} if options.include?(:optional)}
370
+ #{if optional = options[:optional] then
371
+ index = 0
372
+ optional.map do |part|
373
+ next if part == :any
374
+ index += 1
375
+ msg = "argument #{index + signature.size} " +
376
+ "(\#{arg.inspect}) does not match #{part.inspect}"
377
+ oa_index = index + signature.size - 1
378
+
379
+ %{throw(:args_exhausted) if args.empty?
380
+ type = ObjectSpace._id2ref(#{part.object_id})
381
+ arg = args.shift
382
+ unless type === #{adapted[%{arg}, %{type}, %{old_args[#{oa_index}]}]}
383
+ raise(ArgumentError, "#{msg}")
384
+ end
385
+ }
386
+ end
387
+ end
388
+ }
389
+
390
+ #{if repeated = options[:repeated] then
391
+ arg_off = 1 + signature.size
392
+ arg_off += options[:optional].size if options.include?(:optional)
393
+ msg = "argument \#{idx + #{arg_off}} " +
394
+ "(\#{arg.inspect}) does not match \#{part.inspect}"
395
+ %{parts = ObjectSpace._id2ref(#{repeated.object_id})
396
+ args.each_with_index do |arg, idx|
397
+ part = parts[idx % #{repeated.size}]
398
+ if part != :any and
399
+ not part === (#{adapted[%{arg}, %{part}, %{old_args[idx]}]})
400
+ then
401
+ raise(ArgumentError, "#{msg}")
402
+ end
403
+ end
404
+ }
405
+ end
406
+ }
407
+ #{%{end} if options.include?(:optional)}
408
+
409
+ result = ObjectSpace._id2ref(#{old_method.object_id}).bind(self).
410
+ call(*old_args, &block)
411
+ #{if rt = options[:return] and rt != :any then
412
+ msg = "return value (\#{result.inspect}) does not match #{rt.inspect}"
413
+ %{type = ObjectSpace._id2ref(#{rt.object_id})
414
+ unless type === #{adapted[%{result}, %{type}]}
415
+ raise(StandardError, "#{msg}")
416
+ end
417
+ }
418
+ end
419
+ }
420
+ end
421
+ }
422
+ class_eval code, "(signature check for #{old_method.inspect[/: (.+?)>\Z/, 1]})"
423
+
424
+ return true
425
+ end
426
+
427
+ # Specifies that this Module/Class fulfills one or more contracts. The contracts
428
+ # will automatically be verified after an instance has been successfully created.
429
+ # This only actually does the checks when Contract.check_fulfills is enabled.
430
+ # The method will return +true+ in case it actually inserted the check logic and
431
+ # +nil+ in case it didn't.
432
+ #
433
+ # Note that this works by overriding the #initialize method which means that you
434
+ # should either add the fulfills statements after your initialize method or call
435
+ # the previously defined initialize method from your new one.
436
+ def fulfills(*contracts)
437
+ return unless Contract.check_fulfills?
438
+
439
+ contracts.each do |contract|
440
+ contract.implications.each do |implication|
441
+ include implication
442
+ end
443
+ end
444
+
445
+ old_method = instance_method(:initialize)
446
+ remove_method(:initialize) if instance_methods(false).include?("initialize")
447
+
448
+ # Keep visible references around so that the GC will not eat these up.
449
+ @fulfills ||= Array.new
450
+ @fulfills << [contracts, old_method]
451
+
452
+ # Have to use class_eval because define_method does not allow methods to take
453
+ # blocks. This can be cleaned up when Ruby 1.9 has become current.
454
+ class_eval %{
455
+ def initialize(*args, &block)
456
+ ObjectSpace._id2ref(#{old_method.object_id}).bind(self).call(*args, &block)
457
+ ObjectSpace._id2ref(#{contracts.object_id}).each do |contract|
458
+ contract.enforce self
459
+ end
460
+ end
461
+ }, "(post initialization contract check for #{self.inspect})"
462
+
463
+ return true
464
+ end
465
+ end
466
+
467
+
468
+ module Kernel
469
+ # Adds an adaption route from the specified type to the specified type.
470
+ # Basic usage looks like this:
471
+ # adaption :from => StringIO, :to => String, :via => :read
472
+ #
473
+ # This method takes various options. Here's a complete list:
474
+ #
475
+ # <code>:from</code>::
476
+ # The type that can be converted from. Defaults to +self+ meaning you
477
+ # can safely omit it in Class, Module or Contract context.
478
+ #
479
+ # <code>:to</code>::
480
+ # The type that can be converted to. Defaults to +self+ meaning you
481
+ # can safely omit it in Class, Module or Contract context.
482
+ #
483
+ # Note that you need to specify either <code>:from</code> or
484
+ # <code>:to</code>.
485
+ #
486
+ # <code>:via</code>::
487
+ # How the <code>:from</code> type will be converted to the
488
+ # <code>:to</code> type. If this is a Symbol the conversion will be
489
+ # done by invoking the method identified by that Symbol on the
490
+ # source object. Otherwise this should be something that responds to
491
+ # the +call+ method (for example Methods and Procs) which will get
492
+ # the source object as its argument and which should return the
493
+ # target object.
494
+ #
495
+ # <code>:if</code>::
496
+ # The conversion can only be performed if this condition is met.
497
+ # This can either be something that implements the === case
498
+ # equivalence operator or something that implements the +call+
499
+ # method. So Methods, Procs, Modules, Classes and Contracts all
500
+ # make sense in this context. You can also specify a Symbol in
501
+ # which case the conversion can only be performed if the source
502
+ # object responds to the method identified by that Symbol.
503
+ #
504
+ # Note that the <code>:if</code> option will default to the same
505
+ # value as the <code>:via</code> option if the <code>:via</code>
506
+ # option is a Symbol.
507
+ #
508
+ # If you invoke this method with a block it will be used instead of
509
+ # the <code>:via</code> option.
510
+ #
511
+ # See Contract.adapt for how conversion look-ups are performed.
512
+ def adaption(options = {}, &block) # :yield: source_object
513
+ options = {
514
+ :from => self,
515
+ :to => self
516
+ }.merge(options)
517
+
518
+ if block then
519
+ if options.include?(:via) then
520
+ raise(ArgumentError, "Can't use both block and :via")
521
+ else
522
+ options[:via] = block
523
+ end
524
+ end
525
+
526
+ if options[:via].respond_to?(:to_sym) then
527
+ options[:via] = options[:via].to_sym
528
+ end
529
+
530
+ options[:if] ||= options[:via] if options[:via].is_a?(Symbol)
531
+
532
+ if options[:via].is_a?(Symbol) then
533
+ symbol = options[:via]
534
+ options[:via] = lambda { |obj| obj.send(symbol) }
535
+ end
536
+
537
+ if options[:if].respond_to?(:to_sym) then
538
+ options[:if] = options[:if].to_sym
539
+ end
540
+
541
+ if options[:if].is_a?(Symbol) then
542
+ options[:if] = Contract::Check::Quack[options[:if]]
543
+ elsif options[:if].respond_to?(:call) then
544
+ callable = options[:if]
545
+ options[:if] = Contract::Check.block { |obj| callable.call(obj) }
546
+ end
547
+
548
+ if options[:from] == self and options[:to] == self then
549
+ raise(ArgumentError, "Need to specify either :from or :to")
550
+ elsif options[:from] == options[:to] then
551
+ raise(ArgumentError, "Self-adaption: :from and :to both are " +
552
+ options[:to].inspect)
553
+ end
554
+
555
+ unless options[:via]
556
+ raise(ArgumentError, "Need to specify how to adapt (use :via or block)")
557
+ end
558
+
559
+ Contract.adaptions[options[:to]] << options
560
+ end
561
+
562
+ # Built-in adaption routes that Ruby already uses in its C code.
563
+ adaption :to => Symbol, :via => :to_sym
564
+ adaption :to => String, :via => :to_str
565
+ adaption :to => Array, :via => :to_ary
566
+ adaption :to => Integer, :via => :to_int
567
+ end
568
+
569
+
570
+ # Modifies Method and UnboundMethod so that signatures set by
571
+ # Module.signatures can be retrieved.
572
+ #
573
+ # Note that this can only work when the method origin and definition name
574
+ # are known which is the reason for ruby-contract currently overloading
575
+ # all methods that return a method.
576
+ #
577
+ # This could be greatly simplified it http://www.rcrchive.net/rcr/show/292
578
+ # were to be accepted. You can help the development of ruby-contract by
579
+ # voting for that RCR.
580
+ module MethodSignatureMixin
581
+ attr_reader :origin # :nodoc:
582
+ attr_reader :name # :nodoc:
583
+
584
+ def initialize(origin = nil, name = nil) # :nodoc:
585
+ @origin, @name = origin, name
586
+ @signature = nil
587
+ @has_signature = false
588
+ signatures = origin.instance_variable_get(:@signatures)
589
+ @signature = if signatures and signatures.include?(name) then
590
+ @has_signature = true
591
+ signatures[name].last[0, 2]
592
+ elsif self.arity >= 0 then
593
+ [[:any] * self.arity, {}]
594
+ else
595
+ [[:any] * ~self.arity, { :allow_trailing => true }]
596
+ end
597
+ end
598
+
599
+ # Returns the signature of this method in the form of
600
+ # <code>[fixed types, options]</code>. If no signature was specified via
601
+ # Module#signature it will still return something useful.
602
+ #
603
+ # This information can be useful in meta programming.
604
+ def signature() @signature end
605
+
606
+ # Returns whether a signatue for this method was defined via
607
+ # Module#signature.
608
+ def has_signature?() @has_signature end
609
+ end
610
+
611
+ class Method; include MethodSignatureMixin; end # :nodoc:
612
+ class UnboundMethod; include MethodSignatureMixin; end # :nodoc:
613
+
614
+ # Wrap all places where (Unbound)Methods can be created so that they
615
+ # carry around the origin and name meta data.
616
+ orig_instance_method = Module.instance_method(:instance_method)
617
+ class UnboundMethod # :nodoc:
618
+ alias :old_bind :bind # :nodoc:
619
+ end
620
+ { UnboundMethod => [:forward, [:bind, :clone, :dup]],
621
+ Method => [:forward, [:unbind, :clone, :dup]],
622
+ Object => [:create_obj, [:method]],
623
+ Module => [:create_mod, [:instance_method]]
624
+ }.each do |mod, (type, methods)|
625
+ methods.each do |method|
626
+ old_method = orig_instance_method.old_bind(mod).call(method)
627
+
628
+ case type
629
+ when :forward then
630
+ mod.send(:define_method, method) do |*args|
631
+ result = old_method.old_bind(self).call(*args)
632
+ result.send(:initialize, @origin, @name)
633
+ result
634
+ end
635
+
636
+ when :create_mod then
637
+ mod.send(:define_method, method) do |name|
638
+ result = old_method.old_bind(self).call(name)
639
+ result.send(:initialize, self, name)
640
+ result
641
+ end
642
+
643
+ when :create_obj then
644
+ mod.send(:define_method, method) do |name|
645
+ result = old_method.old_bind(self).call(name)
646
+ meta_origin = result.inspect["."]
647
+
648
+ origin = if meta_origin then
649
+ class << self; self; end
650
+ else
651
+ origin_str = result.inspect[/[( ](.+?)\)?#/, 1]
652
+ self.class.ancestors.find do |mod|
653
+ mod.inspect == origin_str
654
+ end
655
+ end
656
+
657
+ result.send(:initialize, origin, name)
658
+ result
659
+ end
660
+ end
661
+
662
+ mod.send(:public, method)
663
+ end
664
+ end