ruby-contract 0.1.1

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.
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