contracts 0.5 → 0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/TUTORIAL.md +42 -1
- data/benchmarks/bench.rb +3 -3
- data/contracts.gemspec +0 -1
- data/lib/contracts.rb +65 -54
- data/lib/contracts/core_ext.rb +15 -0
- data/lib/contracts/decorators.rb +78 -46
- data/lib/contracts/eigenclass.rb +41 -0
- data/lib/contracts/errors.rb +65 -0
- data/lib/contracts/invariants.rb +0 -6
- data/lib/contracts/method_reference.rb +75 -0
- data/lib/contracts/modules.rb +17 -0
- data/lib/contracts/support.rb +16 -0
- data/lib/contracts/version.rb +1 -1
- data/spec/builtin_contracts_spec.rb +3 -5
- data/spec/contracts_spec.rb +193 -5
- data/spec/fixtures/fixtures.rb +244 -113
- data/spec/module_spec.rb +2 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support.rb +6 -0
- metadata +8 -2
@@ -0,0 +1,41 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Eigenclass
|
3
|
+
|
4
|
+
def self.extended(eigenclass)
|
5
|
+
return if eigenclass.respond_to?(:owner_class=)
|
6
|
+
|
7
|
+
class << eigenclass
|
8
|
+
attr_accessor :owner_class
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.lift(base)
|
13
|
+
return NullEigenclass if base.singleton_class?
|
14
|
+
|
15
|
+
eigenclass = base.singleton_class
|
16
|
+
|
17
|
+
unless eigenclass.respond_to?(:owner_class=)
|
18
|
+
eigenclass.extend(Eigenclass)
|
19
|
+
end
|
20
|
+
|
21
|
+
unless eigenclass.respond_to?(:pop_decorators)
|
22
|
+
eigenclass.extend(MethodDecorators)
|
23
|
+
end
|
24
|
+
|
25
|
+
eigenclass.owner_class = base
|
26
|
+
|
27
|
+
eigenclass
|
28
|
+
end
|
29
|
+
|
30
|
+
module NullEigenclass
|
31
|
+
def self.owner_class
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.pop_decorators
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# @private
|
2
|
+
# Base class for Contract errors
|
3
|
+
#
|
4
|
+
# If default failure callback is used it stores failure data
|
5
|
+
class ContractBaseError < ArgumentError
|
6
|
+
attr_reader :data
|
7
|
+
|
8
|
+
def initialize(message, data)
|
9
|
+
super(message)
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
# Used to convert to simple ContractError from other contract errors
|
14
|
+
def to_contract_error
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Default contract error
|
20
|
+
#
|
21
|
+
# If default failure callback is used, users normally see only these contract errors
|
22
|
+
class ContractError < ContractBaseError
|
23
|
+
end
|
24
|
+
|
25
|
+
# @private
|
26
|
+
# Special contract error used internally to detect pattern failure during pattern matching
|
27
|
+
class PatternMatchingError < ContractBaseError
|
28
|
+
# Used to convert to ContractError from PatternMatchingError
|
29
|
+
def to_contract_error
|
30
|
+
ContractError.new(to_s, data)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Base invariant violation error
|
35
|
+
class InvariantError < StandardError
|
36
|
+
def to_contract_error
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Contracts
|
42
|
+
# Error issued when user haven't included Contracts in original class but used Contract definition in singleton class
|
43
|
+
#
|
44
|
+
# Provides useful description for user of the gem and an example of correct usage.
|
45
|
+
class ContractsNotIncluded < TypeError
|
46
|
+
DEFAULT_MESSAGE = %{In order to use contracts in singleton class, please include Contracts module in original class
|
47
|
+
Example:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class Example
|
51
|
+
include Contracts # this line is required
|
52
|
+
class << self
|
53
|
+
# you can use `Contract` definition here now
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```}
|
57
|
+
|
58
|
+
attr_reader :message
|
59
|
+
alias_method :to_s, :message
|
60
|
+
|
61
|
+
def initialize(message=DEFAULT_MESSAGE)
|
62
|
+
@message = message
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/contracts/invariants.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
module Contracts
|
2
|
+
# MethodReference represents original method reference that was
|
3
|
+
# decorated by contracts.ruby. Used for instance methods.
|
4
|
+
class MethodReference
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
# name - name of the method
|
9
|
+
# method - method object
|
10
|
+
def initialize(name, method)
|
11
|
+
@name = name
|
12
|
+
@method = method
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns method_position, delegates to Support.method_position
|
16
|
+
def method_position
|
17
|
+
Support.method_position(@method)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Makes a method re-definition in proper way
|
21
|
+
def make_definition(this, &blk)
|
22
|
+
alias_target(this).send(:define_method, name, &blk)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Makes a method private
|
26
|
+
def make_private(this)
|
27
|
+
alias_target(this).class_eval { private name }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Aliases original method to a special unique name, which is known
|
31
|
+
# only to this class. Usually done right before re-defining the
|
32
|
+
# method.
|
33
|
+
def make_alias(this)
|
34
|
+
_aliased_name = aliased_name
|
35
|
+
original_name = name
|
36
|
+
|
37
|
+
alias_target(this).class_eval do
|
38
|
+
alias_method _aliased_name, original_name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Calls original method on specified `this` argument with
|
43
|
+
# specified arguments `args` and block `&blk`.
|
44
|
+
def send_to(this, *args, &blk)
|
45
|
+
this.send(aliased_name, *args, &blk)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Returns alias target for instance methods, subject to be
|
51
|
+
# overriden in subclasses.
|
52
|
+
def alias_target(this)
|
53
|
+
this
|
54
|
+
end
|
55
|
+
|
56
|
+
def aliased_name
|
57
|
+
@_original_name ||= construct_unique_name
|
58
|
+
end
|
59
|
+
|
60
|
+
def construct_unique_name
|
61
|
+
:"__contracts_ruby_original_#{name}_#{Support.unique_id}"
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
# The same as MethodReference, but used for singleton methods.
|
67
|
+
class SingletonMethodReference < MethodReference
|
68
|
+
private
|
69
|
+
|
70
|
+
# Return alias target for singleton methods.
|
71
|
+
def alias_target(this)
|
72
|
+
this.singleton_class
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Modules
|
3
|
+
def self.included(base)
|
4
|
+
common(base)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.extended(base)
|
8
|
+
common(base)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.common(base)
|
12
|
+
return unless base.instance_of?(Module)
|
13
|
+
base.extend(MethodDecorators)
|
14
|
+
Eigenclass.lift(base)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/contracts/support.rb
CHANGED
@@ -2,6 +2,8 @@ module Contracts
|
|
2
2
|
module Support
|
3
3
|
|
4
4
|
def self.method_position(method)
|
5
|
+
return method.method_position if MethodReference === method
|
6
|
+
|
5
7
|
if RUBY_VERSION =~ /^1\.8/
|
6
8
|
if method.respond_to?(:__file__)
|
7
9
|
method.__file__ + ":" + method.__line__.to_s
|
@@ -18,5 +20,19 @@ module Contracts
|
|
18
20
|
method.is_a?(Proc) ? "Proc" : method.name
|
19
21
|
end
|
20
22
|
|
23
|
+
# Generates unique id, which can be used as a part of identifier
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
# Contracts::Support.unique_id # => "i53u6tiw5hbo"
|
27
|
+
def self.unique_id
|
28
|
+
# Consider using SecureRandom.hex here, and benchmark which one is better
|
29
|
+
(Time.now.to_f * 1000).to_i.to_s(36) + rand(1000000).to_s(36)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.eigenclass_hierarchy_supported?
|
33
|
+
return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
|
34
|
+
RUBY_VERSION.to_f > 1.8
|
35
|
+
end
|
36
|
+
|
21
37
|
end
|
22
38
|
end
|
data/lib/contracts/version.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
include Contracts
|
2
|
-
|
3
1
|
RSpec.describe "Contracts:" do
|
4
2
|
before :all do
|
5
|
-
@o =
|
3
|
+
@o = GenericExample.new
|
6
4
|
end
|
7
5
|
|
8
6
|
describe "Num:" do
|
@@ -205,11 +203,11 @@ RSpec.describe "Contracts:" do
|
|
205
203
|
|
206
204
|
describe '#to_s' do
|
207
205
|
context 'given Symbol => String' do
|
208
|
-
it { expect(HashOf[Symbol, String].to_s).to eq('Hash<Symbol, String>') }
|
206
|
+
it { expect(Contracts::HashOf[Symbol, String].to_s).to eq('Hash<Symbol, String>') }
|
209
207
|
end
|
210
208
|
|
211
209
|
context 'given String => Num' do
|
212
|
-
it { expect(HashOf[String, Num].to_s).to eq('Hash<String, Contracts::Num>') }
|
210
|
+
it { expect(Contracts::HashOf[String, Contracts::Num].to_s).to eq('Hash<String, Contracts::Num>') }
|
213
211
|
end
|
214
212
|
end
|
215
213
|
end
|
data/spec/contracts_spec.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
include Contracts
|
2
|
-
|
3
1
|
RSpec.describe "Contracts:" do
|
4
2
|
before :all do
|
5
|
-
@o =
|
3
|
+
@o = GenericExample.new
|
6
4
|
end
|
7
5
|
|
8
6
|
describe "basic" do
|
@@ -73,6 +71,159 @@ RSpec.describe "Contracts:" do
|
|
73
71
|
end
|
74
72
|
end
|
75
73
|
|
74
|
+
describe "usage in singleton class" do
|
75
|
+
it "should work normally when there is no contract violation" do
|
76
|
+
expect(SingletonClassExample.hoge("hoge")).to eq("superhoge")
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should fail with proper error when there is contract violation" do
|
80
|
+
expect {
|
81
|
+
SingletonClassExample.hoge(3)
|
82
|
+
}.to raise_error(ContractError, /Expected: String/)
|
83
|
+
end
|
84
|
+
|
85
|
+
context "when owner class does not include Contracts" do
|
86
|
+
let(:error) {
|
87
|
+
# NOTE Unable to support this user-friendly error for ruby
|
88
|
+
# 1.8.7 and jruby 1.8, 1.9 it has much less support for
|
89
|
+
# singleton inheritance hierarchy
|
90
|
+
if Contracts::Support.eigenclass_hierarchy_supported?
|
91
|
+
[Contracts::ContractsNotIncluded, Contracts::ContractsNotIncluded::DEFAULT_MESSAGE]
|
92
|
+
else
|
93
|
+
[NoMethodError, /undefined method `Contract'/]
|
94
|
+
end
|
95
|
+
}
|
96
|
+
|
97
|
+
it "fails with descriptive error" do
|
98
|
+
expect {
|
99
|
+
Class.new(GenericExample) do
|
100
|
+
class << self
|
101
|
+
Contract String => String
|
102
|
+
def hoge(name)
|
103
|
+
"super#{name}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
}.to raise_error(*error)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "no contracts feature" do
|
113
|
+
it "disables normal contract checks" do
|
114
|
+
object = NoContractsSimpleExample.new
|
115
|
+
expect { object.some_method(3) }.not_to raise_error
|
116
|
+
end
|
117
|
+
|
118
|
+
it "disables invariants" do
|
119
|
+
object = NoContractsInvariantsExample.new
|
120
|
+
object.day = 7
|
121
|
+
expect { object.next_day }.not_to raise_error
|
122
|
+
end
|
123
|
+
|
124
|
+
it "does not disable pattern matching" do
|
125
|
+
object = NoContractsPatternMatchingExample.new
|
126
|
+
|
127
|
+
expect(object.on_response(200, "hello")).to eq("hello!")
|
128
|
+
expect(object.on_response(404, "Not found")).to eq("error 404: Not found")
|
129
|
+
expect { object.on_response(nil, "junk response") }.to raise_error(ContractError)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "module usage" do
|
134
|
+
context "with instance methods" do
|
135
|
+
it "should check contract" do
|
136
|
+
expect { KlassWithModuleExample.new.plus(3, nil) }.to raise_error(ContractError)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "with singleton methods" do
|
141
|
+
it "should check contract" do
|
142
|
+
expect { ModuleExample.hoge(nil) }.to raise_error(ContractError)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context "with singleton class methods" do
|
147
|
+
it "should check contract" do
|
148
|
+
expect { ModuleExample.eat(:food) }.to raise_error(ContractError)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "singleton methods self in inherited methods" do
|
154
|
+
it "should be a proper self" do
|
155
|
+
expect(SingletonInheritanceExampleSubclass.a_contracted_self).to eq(SingletonInheritanceExampleSubclass)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "anonymous classes" do
|
160
|
+
let(:klass) do
|
161
|
+
Class.new do
|
162
|
+
include Contracts
|
163
|
+
|
164
|
+
Contract String => String
|
165
|
+
def greeting(name)
|
166
|
+
"hello, #{name}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
let(:obj) { klass.new }
|
172
|
+
|
173
|
+
it "does not fail when contract is satisfied" do
|
174
|
+
expect(obj.greeting("world")).to eq("hello, world")
|
175
|
+
end
|
176
|
+
|
177
|
+
it "fails with error when contract is violated" do
|
178
|
+
expect { obj.greeting(3) }.to raise_error(ContractError, /Actual: 3/)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "anonymous modules" do
|
183
|
+
let(:mod) do
|
184
|
+
Module.new do
|
185
|
+
include Contracts
|
186
|
+
include Contracts::Modules
|
187
|
+
|
188
|
+
Contract String => String
|
189
|
+
def greeting(name)
|
190
|
+
"hello, #{name}"
|
191
|
+
end
|
192
|
+
|
193
|
+
Contract String => String
|
194
|
+
def self.greeting(name)
|
195
|
+
"hello, #{name}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
let(:klass) do
|
201
|
+
Class.new.tap { |klass| klass.send(:include, mod) }
|
202
|
+
end
|
203
|
+
|
204
|
+
let(:obj) { klass.new }
|
205
|
+
|
206
|
+
it "does not fail when contract is satisfied" do
|
207
|
+
expect(obj.greeting("world")).to eq("hello, world")
|
208
|
+
end
|
209
|
+
|
210
|
+
it "fails with error when contract is violated" do
|
211
|
+
expect { obj.greeting(3) }.to raise_error(ContractError, /Actual: 3/)
|
212
|
+
end
|
213
|
+
|
214
|
+
context "when called on module itself" do
|
215
|
+
let(:obj) { mod }
|
216
|
+
|
217
|
+
it "does not fail when contract is satisfied" do
|
218
|
+
expect(obj.greeting("world")).to eq("hello, world")
|
219
|
+
end
|
220
|
+
|
221
|
+
it "fails with error when contract is violated" do
|
222
|
+
expect { obj.greeting(3) }.to raise_error(ContractError, /Actual: 3/)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
76
227
|
describe "instance methods" do
|
77
228
|
it "should allow two classes to have the same method with different contracts" do
|
78
229
|
a = A.new
|
@@ -96,11 +247,11 @@ RSpec.describe "Contracts:" do
|
|
96
247
|
|
97
248
|
describe "class methods" do
|
98
249
|
it "should pass for correct input" do
|
99
|
-
expect {
|
250
|
+
expect { GenericExample.a_class_method(2) }.to_not raise_error
|
100
251
|
end
|
101
252
|
|
102
253
|
it "should fail for incorrect input" do
|
103
|
-
expect {
|
254
|
+
expect { GenericExample.a_class_method("bad") }.to raise_error(ContractError)
|
104
255
|
end
|
105
256
|
end
|
106
257
|
|
@@ -180,6 +331,10 @@ RSpec.describe "Contracts:" do
|
|
180
331
|
it "should fail for incorrect input" do
|
181
332
|
expect { @o.do_call(nil) }.to raise_error(ContractError)
|
182
333
|
end
|
334
|
+
|
335
|
+
it "should handle properly lack of block when there are other arguments" do
|
336
|
+
expect { @o.double_with_proc(4) }.to raise_error(ContractError, /Actual: nil/)
|
337
|
+
end
|
183
338
|
end
|
184
339
|
|
185
340
|
describe "varargs" do
|
@@ -192,6 +347,39 @@ RSpec.describe "Contracts:" do
|
|
192
347
|
end
|
193
348
|
end
|
194
349
|
|
350
|
+
describe "varargs with block" do
|
351
|
+
it "should pass for correct input" do
|
352
|
+
expect { @o.with_partial_sums(1, 2, 3) { |partial_sum| 2 * partial_sum + 1 } }.not_to raise_error
|
353
|
+
expect { @o.with_partial_sums_contracted(1, 2, 3) { |partial_sum| 2 * partial_sum + 1 } }.not_to raise_error
|
354
|
+
end
|
355
|
+
|
356
|
+
it "should fail for incorrect input" do
|
357
|
+
expect {
|
358
|
+
@o.with_partial_sums(1, 2, "bad") { |partial_sum| 2 * partial_sum + 1 }
|
359
|
+
}.to raise_error(ContractError, /Actual: "bad"/)
|
360
|
+
|
361
|
+
expect {
|
362
|
+
@o.with_partial_sums(1, 2, 3)
|
363
|
+
}.to raise_error(ContractError, /Actual: nil/)
|
364
|
+
|
365
|
+
expect {
|
366
|
+
@o.with_partial_sums(1, 2, 3, lambda { |x| x })
|
367
|
+
}.to raise_error(ContractError, /Actual: #<Proc/)
|
368
|
+
end
|
369
|
+
|
370
|
+
context "when block has Func contract" do
|
371
|
+
it "should fail for incorrect input" do
|
372
|
+
expect {
|
373
|
+
@o.with_partial_sums_contracted(1, 2, "bad") { |partial_sum| 2 * partial_sum + 1 }
|
374
|
+
}.to raise_error(ContractError, /Actual: "bad"/)
|
375
|
+
|
376
|
+
expect {
|
377
|
+
@o.with_partial_sums_contracted(1, 2, 3)
|
378
|
+
}.to raise_error(ContractError, /Actual: nil/)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
195
383
|
describe "contracts on functions" do
|
196
384
|
it "should pass for a function that passes the contract" do
|
197
385
|
expect { @o.map([1, 2, 3], lambda { |x| x + 1 }) }.to_not raise_error
|