contracts 0.5 → 0.6
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.
- 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
|