contracts 0.12.0 → 0.16.1

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.
data/lib/contracts.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require "contracts/attrs"
1
2
  require "contracts/builtin_contracts"
2
3
  require "contracts/decorators"
3
4
  require "contracts/errors"
@@ -92,9 +93,9 @@ class Contract < Contracts::Decorator
92
93
  last_contract = args_contracts.last
93
94
  penultimate_contract = args_contracts[-2]
94
95
  @has_options_contract = if @has_proc_contract
95
- penultimate_contract.is_a?(Hash)
96
+ penultimate_contract.is_a?(Hash) || penultimate_contract.is_a?(Contracts::Builtin::KeywordArgs)
96
97
  else
97
- last_contract.is_a?(Hash)
98
+ last_contract.is_a?(Hash) || last_contract.is_a?(Contracts::Builtin::KeywordArgs)
98
99
  end
99
100
  # ===
100
101
 
@@ -115,22 +116,57 @@ class Contract < Contracts::Decorator
115
116
  # This function is used by the default #failure_callback method
116
117
  # and uses the hash passed into the failure_callback method.
117
118
  def self.failure_msg(data)
118
- expected = Contracts::Formatters::Expected.new(data[:contract]).contract
119
- position = Contracts::Support.method_position(data[:method])
119
+ indent_amount = 8
120
120
  method_name = Contracts::Support.method_name(data[:method])
121
121
 
122
+ # Header
122
123
  header = if data[:return_value]
123
124
  "Contract violation for return value:"
124
125
  else
125
126
  "Contract violation for argument #{data[:arg_pos]} of #{data[:total_args]}:"
126
127
  end
127
128
 
128
- %{#{header}
129
- Expected: #{expected},
130
- Actual: #{data[:arg].inspect}
131
- Value guarded in: #{data[:class]}::#{method_name}
132
- With Contract: #{data[:contracts]}
133
- At: #{position} }
129
+ # Expected
130
+ expected_prefix = "Expected: "
131
+ expected_value = Contracts::Support.indent_string(
132
+ Contracts::Formatters::Expected.new(data[:contract]).contract.pretty_inspect,
133
+ expected_prefix.length
134
+ ).strip
135
+ expected_line = expected_prefix + expected_value + ","
136
+
137
+ # Actual
138
+ actual_prefix = "Actual: "
139
+ actual_value = Contracts::Support.indent_string(
140
+ data[:arg].pretty_inspect,
141
+ actual_prefix.length
142
+ ).strip
143
+ actual_line = actual_prefix + actual_value
144
+
145
+ # Value guarded in
146
+ value_prefix = "Value guarded in: "
147
+ value_value = "#{data[:class]}::#{method_name}"
148
+ value_line = value_prefix + value_value
149
+
150
+ # Contract
151
+ contract_prefix = "With Contract: "
152
+ contract_value = data[:contracts].to_s
153
+ contract_line = contract_prefix + contract_value
154
+
155
+ # Position
156
+ position_prefix = "At: "
157
+ position_value = Contracts::Support.method_position(data[:method])
158
+ position_line = position_prefix + position_value
159
+
160
+ header +
161
+ "\n" +
162
+ Contracts::Support.indent_string(
163
+ [expected_line,
164
+ actual_line,
165
+ value_line,
166
+ contract_line,
167
+ position_line].join("\n"),
168
+ indent_amount
169
+ )
134
170
  end
135
171
 
136
172
  # Callback for when a contract fails. By default it raises
@@ -215,9 +251,9 @@ class Contract < Contracts::Decorator
215
251
  # returns true if it appended nil
216
252
  def maybe_append_options! args, blk
217
253
  return false unless @has_options_contract
218
- if @has_proc_contract && args_contracts[-2].is_a?(Hash) && !args[-2].is_a?(Hash)
254
+ if @has_proc_contract && (args_contracts[-2].is_a?(Hash) || args_contracts[-2].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-2].is_a?(Hash)
219
255
  args.insert(-2, {})
220
- elsif args_contracts[-1].is_a?(Hash) && !args[-1].is_a?(Hash)
256
+ elsif (args_contracts[-1].is_a?(Hash) || args_contracts[-1].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-1].is_a?(Hash)
221
257
  args << {}
222
258
  end
223
259
  true
@@ -0,0 +1,24 @@
1
+ module Contracts
2
+ module Attrs
3
+ def attr_reader_with_contract(*names, contract)
4
+ names.each do |name|
5
+ Contract Contracts::None => contract
6
+ attr_reader(name)
7
+ end
8
+ end
9
+
10
+ def attr_writer_with_contract(*names, contract)
11
+ names.each do |name|
12
+ Contract contract => contract
13
+ attr_writer(name)
14
+ end
15
+ end
16
+
17
+ def attr_accessor_with_contract(*names, contract)
18
+ attr_reader_with_contract(*names, contract)
19
+ attr_writer_with_contract(*names, contract)
20
+ end
21
+ end
22
+
23
+ include Attrs
24
+ end
@@ -41,13 +41,27 @@ module Contracts
41
41
  end
42
42
  end
43
43
 
44
- # Check that an argument is a natural number.
44
+ # Check that an argument is an +Integer+.
45
+ class Int
46
+ def self.valid? val
47
+ val && val.is_a?(Integer)
48
+ end
49
+ end
50
+
51
+ # Check that an argument is a natural number (includes zero).
45
52
  class Nat
46
53
  def self.valid? val
47
54
  val && val.is_a?(Integer) && val >= 0
48
55
  end
49
56
  end
50
57
 
58
+ # Check that an argument is a positive natural number (excludes zero).
59
+ class NatPos
60
+ def self.valid? val
61
+ val && val.is_a?(Integer) && val > 0
62
+ end
63
+ end
64
+
51
65
  # Passes for any argument.
52
66
  class Any
53
67
  def self.valid? val
@@ -379,6 +393,26 @@ module Contracts
379
393
  end
380
394
  end
381
395
 
396
+ # Use this to specify the Hash characteristics. This contracts fails
397
+ # if there are any extra keys that don't have contracts on them.
398
+ # Example: <tt>StrictHash[{ a: String, b: Bool }]</tt>
399
+ class StrictHash < CallableClass
400
+ attr_reader :contract_hash
401
+
402
+ def initialize(contract_hash)
403
+ @contract_hash = contract_hash
404
+ end
405
+
406
+ def valid?(arg)
407
+ return false unless arg.is_a?(Hash)
408
+ return false unless arg.keys.sort.eql?(contract_hash.keys.sort)
409
+
410
+ contract_hash.all? do |key, contract|
411
+ contract_hash.key?(key) && Contract.valid?(arg[key], contract)
412
+ end
413
+ end
414
+ end
415
+
382
416
  # Use this for specifying contracts for keyword arguments
383
417
  # Example: <tt>KeywordArgs[ e: Range, f: Optional[Num] ]</tt>
384
418
  class KeywordArgs < CallableClass
@@ -387,6 +421,7 @@ module Contracts
387
421
  end
388
422
 
389
423
  def valid?(hash)
424
+ return false unless hash.is_a?(Hash)
390
425
  return false unless hash.keys - options.keys == []
391
426
  options.all? do |key, contract|
392
427
  Optional._valid?(hash, key, contract)
@@ -406,6 +441,30 @@ module Contracts
406
441
  attr_reader :options
407
442
  end
408
443
 
444
+ # Use this for specifying contracts for class arguments
445
+ # Example: <tt>DescendantOf[ e: Range, f: Optional[Num] ]</tt>
446
+ class DescendantOf < CallableClass
447
+ def initialize(parent_class)
448
+ @parent_class = parent_class
449
+ end
450
+
451
+ def valid?(given_class)
452
+ given_class.is_a?(Class) && given_class.ancestors.include?(parent_class)
453
+ end
454
+
455
+ def to_s
456
+ "DescendantOf[#{parent_class}]"
457
+ end
458
+
459
+ def inspect
460
+ to_s
461
+ end
462
+
463
+ private
464
+
465
+ attr_reader :parent_class
466
+ end
467
+
409
468
  # Use this for specifying optional keyword argument
410
469
  # Example: <tt>Optional[Num]</tt>
411
470
  class Optional < CallableClass
@@ -1,6 +1,10 @@
1
1
  module Contracts
2
2
  module CallWith
3
3
  def call_with(this, *args, &blk)
4
+ call_with_inner(false, this, *args, &blk)
5
+ end
6
+
7
+ def call_with_inner(returns, this, *args, &blk)
4
8
  args << blk if blk
5
9
 
6
10
  # Explicitly append blk=nil if nil != Proc contract violation anticipated
@@ -16,17 +20,21 @@ module Contracts
16
20
  validator = @args_validators[i]
17
21
 
18
22
  unless validator && validator[arg]
19
- return unless Contract.failure_callback(:arg => arg,
20
- :contract => contract,
21
- :class => klass,
22
- :method => method,
23
- :contracts => self,
24
- :arg_pos => i+1,
25
- :total_args => args.size,
26
- :return_value => false)
23
+ data = {:arg => arg,
24
+ :contract => contract,
25
+ :class => klass,
26
+ :method => method,
27
+ :contracts => self,
28
+ :arg_pos => i+1,
29
+ :total_args => args.size,
30
+ :return_value => false}
31
+ return ParamContractError.new("as return value", data) if returns
32
+ return unless Contract.failure_callback(data)
27
33
  end
28
34
 
29
- if contract.is_a?(Contracts::Func)
35
+ if contract.is_a?(Contracts::Func) && blk && !nil_block_appended
36
+ blk = Contract.new(klass, arg, *contract.contracts)
37
+ elsif contract.is_a?(Contracts::Func)
30
38
  args[i] = Contract.new(klass, arg, *contract.contracts)
31
39
  end
32
40
  end
@@ -72,8 +80,11 @@ module Contracts
72
80
  # proc, block, lambda, etc
73
81
  method.call(*args, &blk)
74
82
  else
75
- # original method name referrence
76
- method.send_to(this, *args, &blk)
83
+ # original method name reference
84
+ # Don't reassign blk, else Travis CI shows "stack level too deep".
85
+ target_blk = blk
86
+ target_blk = lambda { |*params| blk.call(*params) } if blk && blk.is_a?(Contract)
87
+ method.send_to(this, *args, &target_blk)
77
88
  end
78
89
 
79
90
  unless @ret_validator[result]
@@ -9,8 +9,6 @@ module Contracts
9
9
  end
10
10
 
11
11
  def self.common(base)
12
- return if base.respond_to?(:Contract)
13
-
14
12
  base.extend(MethodDecorators)
15
13
 
16
14
  base.instance_eval do
@@ -4,6 +4,11 @@ module Contracts
4
4
  Engine.apply(klass)
5
5
  end
6
6
 
7
+ def inherited(subclass)
8
+ Engine.fetch_from(subclass).set_eigenclass_owner
9
+ super
10
+ end
11
+
7
12
  def method_added(name)
8
13
  MethodHandler.new(name, false, self).handle
9
14
  super
@@ -18,6 +18,10 @@ module Contracts
18
18
  return Engine.fetch_from(eigenclass) if Engine.applied?(eigenclass)
19
19
 
20
20
  Target.new(eigenclass).apply(Eigenclass)
21
+ eigenclass.extend(MethodDecorators)
22
+ # FIXME; this should detect what user uses `include Contracts` or
23
+ # `include Contracts;;Core`
24
+ eigenclass.send(:include, Contracts)
21
25
  Engine.fetch_from(owner).set_eigenclass_owner
22
26
  Engine.fetch_from(eigenclass)
23
27
  end
@@ -53,6 +53,8 @@ module Contracts
53
53
 
54
54
  self.class.new(eigenclass).apply(Eigenclass)
55
55
  eigenclass.extend(MethodDecorators)
56
+ # FIXME; this should detect what user uses `include Contracts` or
57
+ # `include Contracts;;Core`
56
58
  eigenclass.send(:include, Contracts)
57
59
  end
58
60
 
@@ -1,3 +1,5 @@
1
+ require "pp"
2
+
1
3
  module Contracts
2
4
  # A namespace for classes related to formatting.
3
5
  module Formatters
@@ -25,13 +27,13 @@ module Contracts
25
27
  @full = true # Complex values output completely, overriding @full
26
28
  hash.inject({}) do |repr, (k, v)|
27
29
  repr.merge(k => InspectWrapper.create(contract(v), @full))
28
- end.inspect
30
+ end
29
31
  end
30
32
 
31
33
  # Formats Array contracts.
32
34
  def array_contract(array)
33
35
  @full = true
34
- array.map { |v| InspectWrapper.create(contract(v), @full) }.inspect
36
+ array.map { |v| InspectWrapper.create(contract(v), @full) }
35
37
  end
36
38
  end
37
39
 
@@ -125,31 +125,23 @@ module Contracts
125
125
  # function. Otherwise we return the result.
126
126
  # If we run out of functions, we raise the last error, but
127
127
  # convert it to_contract_error.
128
- success = false
129
- i = 0
130
- result = nil
128
+
131
129
  expected_error = decorated_methods[0].failure_exception
130
+ last_error = nil
132
131
 
133
- until success
134
- decorated_method = decorated_methods[i]
135
- i += 1
136
- begin
137
- success = true
138
- result = decorated_method.call_with(self, *args, &blk)
139
- rescue expected_error => error
140
- success = false
141
- unless decorated_methods[i]
142
- begin
143
- ::Contract.failure_callback(error.data, false)
144
- rescue expected_error => final_error
145
- raise final_error.to_contract_error
146
- end
147
- end
148
- end
132
+ decorated_methods.each do |decorated_method|
133
+ result = decorated_method.call_with_inner(true, self, *args, &blk)
134
+ return result unless result.is_a?(ParamContractError)
135
+ last_error = result
149
136
  end
150
137
 
151
- # Return the result of successfully called method
152
- result
138
+ begin
139
+ if ::Contract.failure_callback(last_error.data, false)
140
+ decorated_methods.last.call_with_inner(false, self, *args, &blk)
141
+ end
142
+ rescue expected_error => final_error
143
+ raise final_error.to_contract_error
144
+ end
153
145
  end
154
146
  end
155
147
 
@@ -157,7 +149,7 @@ module Contracts
157
149
  return if decorators.size == 1
158
150
 
159
151
  fail %{
160
- Oops, it looks like method '#{name}' has multiple contracts:
152
+ Oops, it looks like method '#{method_name}' has multiple contracts:
161
153
  #{decorators.map { |x| x[1][0].inspect }.join("\n")}
162
154
 
163
155
  Did you accidentally put more than one contract on a single function, like so?
@@ -4,14 +4,10 @@ module Contracts
4
4
  def method_position(method)
5
5
  return method.method_position if method.is_a?(MethodReference)
6
6
 
7
- if RUBY_VERSION =~ /^1\.8/
8
- if method.respond_to?(:__file__)
9
- method.__file__ + ":" + method.__line__.to_s
10
- else
11
- method.inspect
12
- end
7
+ file, line = method.source_location
8
+ if file.nil? || line.nil?
9
+ ""
13
10
  else
14
- file, line = method.source_location
15
11
  file + ":" + line.to_s
16
12
  end
17
13
  end
@@ -34,8 +30,7 @@ module Contracts
34
30
  end
35
31
 
36
32
  def eigenclass_hierarchy_supported?
37
- return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
38
- RUBY_VERSION.to_f > 1.8
33
+ RUBY_PLATFORM != "java" || RUBY_VERSION.to_f >= 2.0
39
34
  end
40
35
 
41
36
  def eigenclass_of(target)
@@ -47,6 +42,13 @@ module Contracts
47
42
  target <= eigenclass_of(Object)
48
43
  end
49
44
 
45
+ def indent_string(string, amount)
46
+ string.gsub(
47
+ /^(?!$)/,
48
+ (string[/^[ \t]/] || " ") * amount
49
+ )
50
+ end
51
+
50
52
  private
51
53
 
52
54
  # Module eigenclass can be detected by its ancestor chain
@@ -1,3 +1,3 @@
1
1
  module Contracts
2
- VERSION = "0.12.0"
2
+ VERSION = "0.16.1"
3
3
  end
@@ -0,0 +1,119 @@
1
+ RSpec.describe "Contracts:" do
2
+ describe "Attrs:" do
3
+ class Person
4
+ include Contracts::Core
5
+ include Contracts::Attrs
6
+ include Contracts::Builtin
7
+
8
+ def initialize(name)
9
+ @name_r = name
10
+ @name_w = name
11
+ @name_rw = name
12
+
13
+ @name_r_2 = name
14
+ @name_w_2 = name
15
+ @name_rw_2 = name
16
+ end
17
+
18
+ attr_reader_with_contract :name_r, :name_r_2, String
19
+ attr_writer_with_contract :name_w, :name_w_2, String
20
+ attr_accessor_with_contract :name_rw, :name_rw_2, String
21
+ end
22
+
23
+ context "attr_reader_with_contract" do
24
+ it "getting valid type" do
25
+ expect(Person.new("bob").name_r)
26
+ .to(eq("bob"))
27
+ end
28
+
29
+ it "getting invalid type" do
30
+ expect { Person.new(1.3).name_r }
31
+ .to(raise_error(ReturnContractError))
32
+ end
33
+
34
+ it "getting valid type for second val" do
35
+ expect(Person.new("bob").name_r_2)
36
+ .to(eq("bob"))
37
+ end
38
+
39
+ it "getting invalid type for second val" do
40
+ expect { Person.new(1.3).name_r_2 }
41
+ .to(raise_error(ReturnContractError))
42
+ end
43
+
44
+ it "setting" do
45
+ expect { Person.new("bob").name_r = "alice" }
46
+ .to(raise_error(NoMethodError))
47
+ end
48
+ end
49
+
50
+ context "attr_writer_with_contract" do
51
+ it "getting" do
52
+ expect { Person.new("bob").name_w }
53
+ .to(raise_error(NoMethodError))
54
+ end
55
+
56
+ it "setting valid type" do
57
+ expect(Person.new("bob").name_w = "alice")
58
+ .to(eq("alice"))
59
+ end
60
+
61
+ it "setting invalid type" do
62
+ expect { Person.new("bob").name_w = 1.2 }
63
+ .to(raise_error(ParamContractError))
64
+ end
65
+
66
+ it "setting valid type for second val" do
67
+ expect(Person.new("bob").name_w_2 = "alice")
68
+ .to(eq("alice"))
69
+ end
70
+
71
+ it "setting invalid type for second val" do
72
+ expect { Person.new("bob").name_w_2 = 1.2 }
73
+ .to(raise_error(ParamContractError))
74
+ end
75
+ end
76
+
77
+ context "attr_accessor_with_contract" do
78
+ it "getting valid type" do
79
+ expect(Person.new("bob").name_rw)
80
+ .to(eq("bob"))
81
+ end
82
+
83
+ it "getting invalid type" do
84
+ expect { Person.new(1.2).name_rw }
85
+ .to(raise_error(ReturnContractError))
86
+ end
87
+
88
+ it "setting valid type" do
89
+ expect(Person.new("bob").name_rw = "alice")
90
+ .to(eq("alice"))
91
+ end
92
+
93
+ it "setting invalid type" do
94
+ expect { Person.new("bob").name_rw = 1.2 }
95
+ .to(raise_error(ParamContractError))
96
+ end
97
+
98
+ it "getting valid type for second val" do
99
+ expect(Person.new("bob").name_rw_2)
100
+ .to(eq("bob"))
101
+ end
102
+
103
+ it "getting invalid type for second val" do
104
+ expect { Person.new(1.2).name_rw_2 }
105
+ .to(raise_error(ReturnContractError))
106
+ end
107
+
108
+ it "setting valid type for second val" do
109
+ expect(Person.new("bob").name_rw_2 = "alice")
110
+ .to(eq("alice"))
111
+ end
112
+
113
+ it "setting invalid type for second val" do
114
+ expect { Person.new("bob").name_rw_2 = 1.2 }
115
+ .to(raise_error(ParamContractError))
116
+ end
117
+ end
118
+ end
119
+ end