contracts 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +16 -1
- data/README.md +12 -4
- data/TUTORIAL.md +67 -18
- data/benchmarks/bench.rb +20 -20
- data/benchmarks/invariants.rb +28 -18
- data/benchmarks/io.rb +62 -0
- data/benchmarks/wrap_test.rb +11 -13
- data/contracts.gemspec +1 -1
- data/lib/contracts.rb +167 -109
- data/lib/contracts/builtin_contracts.rb +132 -84
- data/lib/contracts/decorators.rb +41 -53
- data/lib/contracts/eigenclass.rb +3 -7
- data/lib/contracts/errors.rb +1 -1
- data/lib/contracts/formatters.rb +134 -0
- data/lib/contracts/invariants.rb +7 -8
- data/lib/contracts/method_reference.rb +33 -9
- data/lib/contracts/support.rb +9 -3
- data/lib/contracts/testable.rb +6 -6
- data/lib/contracts/version.rb +1 -1
- data/script/rubocop.rb +5 -0
- data/spec/builtin_contracts_spec.rb +57 -9
- data/spec/contracts_spec.rb +182 -53
- data/spec/fixtures/fixtures.rb +130 -9
- data/spec/invariants_spec.rb +0 -2
- data/spec/module_spec.rb +2 -2
- data/spec/ruby_version_specific/contracts_spec_1.9.rb +18 -0
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +22 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +55 -0
- data/spec/spec_helper.rb +9 -2
- data/spec/support.rb +4 -0
- data/spec/support_spec.rb +21 -0
- metadata +11 -3
- data/lib/contracts/core_ext.rb +0 -15
data/lib/contracts/decorators.rb
CHANGED
@@ -10,9 +10,7 @@ module Contracts
|
|
10
10
|
|
11
11
|
module EigenclassWithOwner
|
12
12
|
def self.lift(eigenclass)
|
13
|
-
unless with_owner?(eigenclass)
|
14
|
-
raise Contracts::ContractsNotIncluded
|
15
|
-
end
|
13
|
+
fail Contracts::ContractsNotIncluded unless with_owner?(eigenclass)
|
16
14
|
|
17
15
|
eigenclass
|
18
16
|
end
|
@@ -51,18 +49,14 @@ module Contracts
|
|
51
49
|
decorators = fetch_decorators
|
52
50
|
return if decorators.empty?
|
53
51
|
|
54
|
-
@decorated_methods ||= {:class_methods => {}, :instance_methods => {}}
|
52
|
+
@decorated_methods ||= { :class_methods => {}, :instance_methods => {} }
|
55
53
|
|
56
54
|
if is_class_method
|
57
55
|
method_reference = SingletonMethodReference.new(name, method(name))
|
58
56
|
method_type = :class_methods
|
59
|
-
# private_methods is an array of strings on 1.8 and an array of symbols on 1.9
|
60
|
-
is_private = self.private_methods.include?(name) || self.private_methods.include?(name.to_s)
|
61
57
|
else
|
62
58
|
method_reference = MethodReference.new(name, instance_method(name))
|
63
59
|
method_type = :instance_methods
|
64
|
-
# private_instance_methods is an array of strings on 1.8 and an array of symbols on 1.9
|
65
|
-
is_private = self.private_instance_methods.include?(name) || self.private_instance_methods.include?(name.to_s)
|
66
60
|
end
|
67
61
|
|
68
62
|
@decorated_methods[method_type][name] ||= []
|
@@ -79,9 +73,7 @@ module Contracts
|
|
79
73
|
end
|
80
74
|
|
81
75
|
if @decorated_methods[method_type][name].any? { |x| x.method != method_reference }
|
82
|
-
@decorated_methods[method_type][name].each
|
83
|
-
decorator.pattern_match!
|
84
|
-
end
|
76
|
+
@decorated_methods[method_type][name].each(&:pattern_match!)
|
85
77
|
|
86
78
|
pattern_matching = true
|
87
79
|
end
|
@@ -95,42 +87,40 @@ module Contracts
|
|
95
87
|
# The decorator in turn has a reference to the actual method, so it can call it
|
96
88
|
# on its own, after doing it's decorating of course.
|
97
89
|
|
98
|
-
=
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
end
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
|
133
|
-
=end
|
90
|
+
# Very important: THe line `current = #{self}` in the start is crucial.
|
91
|
+
# Not having it means that any method that used contracts could NOT use `super`
|
92
|
+
# (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27).
|
93
|
+
# Here's why: Suppose you have this code:
|
94
|
+
#
|
95
|
+
# class Foo
|
96
|
+
# Contract nil => String
|
97
|
+
# def to_s
|
98
|
+
# "Foo"
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# class Bar < Foo
|
103
|
+
# Contract nil => String
|
104
|
+
# def to_s
|
105
|
+
# super + "Bar"
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# b = Bar.new
|
110
|
+
# p b.to_s
|
111
|
+
#
|
112
|
+
# `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However,
|
113
|
+
# we have overwritten the function (that's what this next defn is). So it gets a
|
114
|
+
# reference to the function to call by looking at `decorated_methods`.
|
115
|
+
#
|
116
|
+
# Now, this line used to read something like:
|
117
|
+
#
|
118
|
+
# current = self#{is_class_method ? "" : ".class"}
|
119
|
+
#
|
120
|
+
# In that case, `self` would always be `Bar`, regardless of whether you were calling
|
121
|
+
# Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which
|
122
|
+
# means you would always call Bar's to_s...infinite recursion! Instead, you want to
|
123
|
+
# call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
|
134
124
|
|
135
125
|
current = self
|
136
126
|
method_reference.make_definition(self) do |*args, &blk|
|
@@ -140,7 +130,7 @@ Here's why: Suppose you have this code:
|
|
140
130
|
current = ancestors.shift
|
141
131
|
end
|
142
132
|
if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
|
143
|
-
|
133
|
+
fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case."
|
144
134
|
end
|
145
135
|
methods = current.decorated_methods[method_type][name]
|
146
136
|
|
@@ -151,7 +141,7 @@ Here's why: Suppose you have this code:
|
|
151
141
|
i = 0
|
152
142
|
result = nil
|
153
143
|
expected_error = methods[0].failure_exception
|
154
|
-
|
144
|
+
until success
|
155
145
|
method = methods[i]
|
156
146
|
i += 1
|
157
147
|
begin
|
@@ -170,12 +160,10 @@ Here's why: Suppose you have this code:
|
|
170
160
|
end
|
171
161
|
result
|
172
162
|
end
|
173
|
-
|
174
|
-
method_reference.make_private(self) if is_private
|
175
163
|
end
|
176
164
|
|
177
165
|
def decorate(klass, *args)
|
178
|
-
if
|
166
|
+
if Support.eigenclass? self
|
179
167
|
return EigenclassWithOwner.lift(self).owner_class.decorate(klass, *args)
|
180
168
|
end
|
181
169
|
|
@@ -189,7 +177,7 @@ Here's why: Suppose you have this code:
|
|
189
177
|
class << self; attr_accessor :decorators; end
|
190
178
|
|
191
179
|
def self.inherited(klass)
|
192
|
-
name = klass.name.gsub(/^./) {|m| m.downcase}
|
180
|
+
name = klass.name.gsub(/^./) { |m| m.downcase }
|
193
181
|
|
194
182
|
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
195
183
|
|
data/lib/contracts/eigenclass.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
module Contracts
|
2
2
|
module Eigenclass
|
3
|
-
|
4
3
|
def self.extended(eigenclass)
|
5
4
|
return if eigenclass.respond_to?(:owner_class=)
|
6
5
|
|
@@ -10,13 +9,11 @@ module Contracts
|
|
10
9
|
end
|
11
10
|
|
12
11
|
def self.lift(base)
|
13
|
-
return NullEigenclass if
|
12
|
+
return NullEigenclass if Support.eigenclass? base
|
14
13
|
|
15
|
-
eigenclass = base
|
14
|
+
eigenclass = Support.eigenclass_of base
|
16
15
|
|
17
|
-
unless eigenclass.respond_to?(:owner_class=)
|
18
|
-
eigenclass.extend(Eigenclass)
|
19
|
-
end
|
16
|
+
eigenclass.extend(Eigenclass) unless eigenclass.respond_to?(:owner_class=)
|
20
17
|
|
21
18
|
unless eigenclass.respond_to?(:pop_decorators)
|
22
19
|
eigenclass.extend(MethodDecorators)
|
@@ -37,6 +34,5 @@ module Contracts
|
|
37
34
|
[]
|
38
35
|
end
|
39
36
|
end
|
40
|
-
|
41
37
|
end
|
42
38
|
end
|
data/lib/contracts/errors.rb
CHANGED
@@ -0,0 +1,134 @@
|
|
1
|
+
module Contracts
|
2
|
+
# A namespace for classes related to formatting.
|
3
|
+
module Formatters
|
4
|
+
# Used to format contracts for the `Expected:` field of error output.
|
5
|
+
class Expected
|
6
|
+
# @param full [Boolean] if false only unique `to_s` values will be output,
|
7
|
+
# non unique values become empty string.
|
8
|
+
def initialize(contract, full = true)
|
9
|
+
@contract, @full = contract, full
|
10
|
+
end
|
11
|
+
|
12
|
+
# Formats any type of Contract.
|
13
|
+
def contract(contract = @contract)
|
14
|
+
if contract.is_a?(Hash)
|
15
|
+
hash_contract(contract)
|
16
|
+
elsif contract.is_a?(Array)
|
17
|
+
array_contract(contract)
|
18
|
+
else
|
19
|
+
InspectWrapper.create(contract, @full)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Formats Hash contracts.
|
24
|
+
def hash_contract(hash)
|
25
|
+
@full = true # Complex values output completely, overriding @full
|
26
|
+
hash.inject({}) do |repr, (k, v)|
|
27
|
+
repr.merge(k => InspectWrapper.create(contract(v), @full))
|
28
|
+
end.inspect
|
29
|
+
end
|
30
|
+
|
31
|
+
# Formats Array contracts.
|
32
|
+
def array_contract(array)
|
33
|
+
@full = true
|
34
|
+
array.map { |v| InspectWrapper.create(contract(v), @full) }.inspect
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# A wrapper class to produce correct inspect behaviour for different
|
39
|
+
# contract values - constants, Class contracts, instance contracts etc.
|
40
|
+
module InspectWrapper
|
41
|
+
# InspectWrapper is a factory, will never be an instance
|
42
|
+
# @return [ClassInspectWrapper, ObjectInspectWrapper]
|
43
|
+
def self.create(value, full = true)
|
44
|
+
if value.class == Class
|
45
|
+
ClassInspectWrapper
|
46
|
+
else
|
47
|
+
ObjectInspectWrapper
|
48
|
+
end.new(value, full)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param full [Boolean] if false only unique `to_s` values will be output,
|
52
|
+
# non unique values become empty string.
|
53
|
+
def initialize(value, full)
|
54
|
+
@value, @full = value, full
|
55
|
+
end
|
56
|
+
|
57
|
+
# Inspect different types of contract values.
|
58
|
+
# Contracts module prefix will be removed from classes.
|
59
|
+
# Custom to_s messages will be wrapped in round brackets to differentiate
|
60
|
+
# from standard Strings.
|
61
|
+
# Primitive values e.g. 42, true, nil will be left alone.
|
62
|
+
def inspect
|
63
|
+
return "" unless full?
|
64
|
+
return @value.inspect if empty_val?
|
65
|
+
return @value.to_s if plain?
|
66
|
+
return delim(@value.to_s) if useful_to_s?
|
67
|
+
useful_inspect
|
68
|
+
end
|
69
|
+
|
70
|
+
def delim(value)
|
71
|
+
@full ? "(#{value})" : "#{value}"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Eliminates eronious quotes in output that plain inspect includes.
|
75
|
+
def to_s
|
76
|
+
inspect
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def empty_val?
|
82
|
+
@value.nil? || @value == ""
|
83
|
+
end
|
84
|
+
|
85
|
+
def full?
|
86
|
+
@full ||
|
87
|
+
@value.is_a?(Hash) || @value.is_a?(Array) ||
|
88
|
+
(!plain? && useful_to_s?)
|
89
|
+
end
|
90
|
+
|
91
|
+
def plain?
|
92
|
+
# Not a type of contract that can have a custom to_s defined
|
93
|
+
!@value.is_a?(CallableClass) && @value.class != Class
|
94
|
+
end
|
95
|
+
|
96
|
+
def useful_to_s?
|
97
|
+
# Useless to_s value or no custom to_s behavious defined
|
98
|
+
!empty_to_s? && custom_to_s?
|
99
|
+
end
|
100
|
+
|
101
|
+
def empty_to_s?
|
102
|
+
@value.to_s.empty?
|
103
|
+
end
|
104
|
+
|
105
|
+
def strip_prefix(val)
|
106
|
+
val.gsub(/^Contracts::/, "")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class ClassInspectWrapper
|
111
|
+
include InspectWrapper
|
112
|
+
|
113
|
+
def custom_to_s?
|
114
|
+
@value.to_s != @value.name
|
115
|
+
end
|
116
|
+
|
117
|
+
def useful_inspect
|
118
|
+
strip_prefix(empty_to_s? ? @value.name : @value.inspect)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class ObjectInspectWrapper
|
123
|
+
include InspectWrapper
|
124
|
+
|
125
|
+
def custom_to_s?
|
126
|
+
!@value.to_s.match(/#\<\w+:.+\>/)
|
127
|
+
end
|
128
|
+
|
129
|
+
def useful_inspect
|
130
|
+
strip_prefix(empty_to_s? ? @value.class.name : @value.inspect)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/contracts/invariants.rb
CHANGED
@@ -23,7 +23,7 @@ module Contracts
|
|
23
23
|
end
|
24
24
|
|
25
25
|
module InvariantExtension
|
26
|
-
def
|
26
|
+
def invariant(name, &condition)
|
27
27
|
return if ENV["NO_CONTRACTS"]
|
28
28
|
|
29
29
|
invariants << Invariant.new(self, name, &condition)
|
@@ -53,17 +53,16 @@ module Contracts
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def self.failure_callback(data)
|
56
|
-
|
56
|
+
fail InvariantError, failure_msg(data)
|
57
57
|
end
|
58
58
|
|
59
59
|
def self.failure_msg(data)
|
60
|
-
%{Invariant violation:
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
60
|
+
%{Invariant violation:
|
61
|
+
Expected: #{data[:expected]}
|
62
|
+
Actual: #{data[:actual]}
|
63
|
+
Value guarded in: #{data[:target].class}::#{Support.method_name(data[:method])}
|
64
|
+
At: #{Support.method_position(data[:method])}}
|
65
65
|
end
|
66
66
|
end
|
67
|
-
|
68
67
|
end
|
69
68
|
end
|
@@ -2,7 +2,6 @@ module Contracts
|
|
2
2
|
# MethodReference represents original method reference that was
|
3
3
|
# decorated by contracts.ruby. Used for instance methods.
|
4
4
|
class MethodReference
|
5
|
-
|
6
5
|
attr_reader :name
|
7
6
|
|
8
7
|
# name - name of the method
|
@@ -19,13 +18,11 @@ module Contracts
|
|
19
18
|
|
20
19
|
# Makes a method re-definition in proper way
|
21
20
|
def make_definition(this, &blk)
|
21
|
+
is_private = private?(this)
|
22
|
+
is_protected = protected?(this)
|
22
23
|
alias_target(this).send(:define_method, name, &blk)
|
23
|
-
|
24
|
-
|
25
|
-
# Makes a method private
|
26
|
-
def make_private(this)
|
27
|
-
original_name = name
|
28
|
-
alias_target(this).class_eval { private original_name }
|
24
|
+
make_private(this) if is_private
|
25
|
+
make_protected(this) if is_protected
|
29
26
|
end
|
30
27
|
|
31
28
|
# Aliases original method to a special unique name, which is known
|
@@ -48,6 +45,26 @@ module Contracts
|
|
48
45
|
|
49
46
|
private
|
50
47
|
|
48
|
+
# Makes a method private
|
49
|
+
def make_private(this)
|
50
|
+
original_name = name
|
51
|
+
alias_target(this).class_eval { private original_name }
|
52
|
+
end
|
53
|
+
|
54
|
+
def private?(this)
|
55
|
+
this.private_instance_methods.map(&:to_sym).include?(name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def protected?(this)
|
59
|
+
this.protected_instance_methods.map(&:to_sym).include?(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Makes a method protected
|
63
|
+
def make_protected(this)
|
64
|
+
original_name = name
|
65
|
+
alias_target(this).class_eval { protected original_name }
|
66
|
+
end
|
67
|
+
|
51
68
|
# Returns alias target for instance methods, subject to be
|
52
69
|
# overriden in subclasses.
|
53
70
|
def alias_target(this)
|
@@ -61,16 +78,23 @@ module Contracts
|
|
61
78
|
def construct_unique_name
|
62
79
|
:"__contracts_ruby_original_#{name}_#{Support.unique_id}"
|
63
80
|
end
|
64
|
-
|
65
81
|
end
|
66
82
|
|
67
83
|
# The same as MethodReference, but used for singleton methods.
|
68
84
|
class SingletonMethodReference < MethodReference
|
69
85
|
private
|
70
86
|
|
87
|
+
def private?(this)
|
88
|
+
this.private_methods.map(&:to_sym).include?(name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def protected?(this)
|
92
|
+
this.protected_methods.map(&:to_sym).include?(name)
|
93
|
+
end
|
94
|
+
|
71
95
|
# Return alias target for singleton methods.
|
72
96
|
def alias_target(this)
|
73
|
-
this
|
97
|
+
Support.eigenclass_of this
|
74
98
|
end
|
75
99
|
end
|
76
100
|
end
|
data/lib/contracts/support.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
module Contracts
|
2
2
|
module Support
|
3
|
-
|
4
3
|
def self.method_position(method)
|
5
|
-
return method.method_position if MethodReference
|
4
|
+
return method.method_position if method.is_a?(MethodReference)
|
6
5
|
|
7
6
|
if RUBY_VERSION =~ /^1\.8/
|
8
7
|
if method.respond_to?(:__file__)
|
@@ -26,7 +25,7 @@ module Contracts
|
|
26
25
|
# Contracts::Support.unique_id # => "i53u6tiw5hbo"
|
27
26
|
def self.unique_id
|
28
27
|
# Consider using SecureRandom.hex here, and benchmark which one is better
|
29
|
-
(Time.now.to_f * 1000).to_i.to_s(36) + rand(
|
28
|
+
(Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36)
|
30
29
|
end
|
31
30
|
|
32
31
|
def self.eigenclass_hierarchy_supported?
|
@@ -34,5 +33,12 @@ module Contracts
|
|
34
33
|
RUBY_VERSION.to_f > 1.8
|
35
34
|
end
|
36
35
|
|
36
|
+
def self.eigenclass_of(target)
|
37
|
+
class << target; self; end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.eigenclass?(target)
|
41
|
+
target <= eigenclass_of(Object)
|
42
|
+
end
|
37
43
|
end
|
38
44
|
end
|