contracts 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,9 +1,3 @@
1
- class InvariantError < StandardError
2
- def to_contract_error
3
- self
4
- end
5
- end
6
-
7
1
  module Contracts
8
2
  module Invariants
9
3
  def self.included(base)
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Contracts
2
- VERSION = "0.5"
2
+ VERSION = "0.6"
3
3
  end
@@ -1,8 +1,6 @@
1
- include Contracts
2
-
3
1
  RSpec.describe "Contracts:" do
4
2
  before :all do
5
- @o = Object.new
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
@@ -1,8 +1,6 @@
1
- include Contracts
2
-
3
1
  RSpec.describe "Contracts:" do
4
2
  before :all do
5
- @o = Object.new
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 { Object.a_class_method(2) }.to_not raise_error
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 { Object.a_class_method("bad") }.to raise_error(ContractError)
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