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