contracts 0.13.0 → 0.17
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 +5 -5
- data/.github/workflows/code_style_checks.yaml +36 -0
- data/.github/workflows/tests.yaml +41 -0
- data/CHANGELOG.markdown +54 -7
- data/Gemfile +9 -5
- data/LICENSE +23 -0
- data/README.md +14 -6
- data/Rakefile +5 -6
- data/TUTORIAL.md +28 -1
- data/contracts.gemspec +9 -1
- data/dependabot.yml +20 -0
- data/features/basics/pretty-print.feature +241 -0
- data/features/support/env.rb +2 -0
- data/lib/contracts.rb +64 -19
- data/lib/contracts/attrs.rb +26 -0
- data/lib/contracts/builtin_contracts.rb +85 -7
- data/lib/contracts/call_with.rb +50 -28
- data/lib/contracts/core.rb +3 -3
- data/lib/contracts/decorators.rb +6 -2
- data/lib/contracts/engine.rb +2 -0
- data/lib/contracts/engine/base.rb +4 -3
- data/lib/contracts/engine/eigenclass.rb +3 -2
- data/lib/contracts/engine/target.rb +2 -0
- data/lib/contracts/errors.rb +3 -0
- data/lib/contracts/formatters.rb +17 -11
- data/lib/contracts/invariants.rb +8 -4
- data/lib/contracts/method_handler.rb +30 -28
- data/lib/contracts/method_reference.rb +4 -2
- data/lib/contracts/support.rb +14 -10
- data/lib/contracts/validators.rb +6 -2
- data/lib/contracts/version.rb +3 -1
- data/spec/attrs_spec.rb +119 -0
- data/spec/builtin_contracts_spec.rb +155 -97
- data/spec/contracts_spec.rb +54 -12
- data/spec/fixtures/fixtures.rb +49 -2
- data/spec/methods_spec.rb +54 -0
- data/spec/override_validators_spec.rb +3 -3
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +17 -2
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +1 -1
- data/spec/validators_spec.rb +1 -1
- metadata +22 -10
- data/script/cucumber +0 -5
data/features/support/env.rb
CHANGED
data/lib/contracts.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "contracts/attrs"
|
1
4
|
require "contracts/builtin_contracts"
|
2
5
|
require "contracts/decorators"
|
3
6
|
require "contracts/errors"
|
@@ -50,7 +53,9 @@ class Contract < Contracts::Decorator
|
|
50
53
|
end
|
51
54
|
|
52
55
|
attr_reader :args_contracts, :ret_contract, :klass, :method
|
56
|
+
|
53
57
|
def initialize(klass, method, *contracts)
|
58
|
+
super(klass, method)
|
54
59
|
unless contracts.last.is_a?(Hash)
|
55
60
|
unless contracts.one?
|
56
61
|
fail %{
|
@@ -92,17 +97,17 @@ class Contract < Contracts::Decorator
|
|
92
97
|
last_contract = args_contracts.last
|
93
98
|
penultimate_contract = args_contracts[-2]
|
94
99
|
@has_options_contract = if @has_proc_contract
|
95
|
-
penultimate_contract.is_a?(
|
100
|
+
penultimate_contract.is_a?(Contracts::Builtin::KeywordArgs)
|
96
101
|
else
|
97
|
-
last_contract.is_a?(
|
102
|
+
last_contract.is_a?(Contracts::Builtin::KeywordArgs)
|
98
103
|
end
|
99
104
|
# ===
|
100
105
|
|
101
106
|
@klass, @method = klass, method
|
102
107
|
end
|
103
108
|
|
104
|
-
def pretty_contract
|
105
|
-
|
109
|
+
def pretty_contract contract
|
110
|
+
contract.is_a?(Class) ? contract.name : contract.class.name
|
106
111
|
end
|
107
112
|
|
108
113
|
def to_s
|
@@ -115,22 +120,60 @@ class Contract < Contracts::Decorator
|
|
115
120
|
# This function is used by the default #failure_callback method
|
116
121
|
# and uses the hash passed into the failure_callback method.
|
117
122
|
def self.failure_msg(data)
|
118
|
-
|
119
|
-
position = Contracts::Support.method_position(data[:method])
|
123
|
+
indent_amount = 8
|
120
124
|
method_name = Contracts::Support.method_name(data[:method])
|
121
125
|
|
126
|
+
# Header
|
122
127
|
header = if data[:return_value]
|
123
128
|
"Contract violation for return value:"
|
124
129
|
else
|
125
130
|
"Contract violation for argument #{data[:arg_pos]} of #{data[:total_args]}:"
|
126
131
|
end
|
127
132
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
133
|
+
# Expected
|
134
|
+
expected_prefix = "Expected: "
|
135
|
+
expected_value = Contracts::Support.indent_string(
|
136
|
+
Contracts::Formatters::Expected.new(data[:contract]).contract.pretty_inspect,
|
137
|
+
expected_prefix.length,
|
138
|
+
).strip
|
139
|
+
expected_line = "#{expected_prefix}#{expected_value},"
|
140
|
+
|
141
|
+
# Actual
|
142
|
+
actual_prefix = "Actual: "
|
143
|
+
actual_value = Contracts::Support.indent_string(
|
144
|
+
data[:arg].pretty_inspect,
|
145
|
+
actual_prefix.length,
|
146
|
+
).strip
|
147
|
+
actual_line = actual_prefix + actual_value
|
148
|
+
|
149
|
+
# Value guarded in
|
150
|
+
value_prefix = "Value guarded in: "
|
151
|
+
value_value = "#{data[:class]}::#{method_name}"
|
152
|
+
value_line = value_prefix + value_value
|
153
|
+
|
154
|
+
# Contract
|
155
|
+
contract_prefix = "With Contract: "
|
156
|
+
contract_value = data[:contracts].to_s
|
157
|
+
contract_line = contract_prefix + contract_value
|
158
|
+
|
159
|
+
# Position
|
160
|
+
position_prefix = "At: "
|
161
|
+
position_value = Contracts::Support.method_position(data[:method])
|
162
|
+
position_line = position_prefix + position_value
|
163
|
+
|
164
|
+
[
|
165
|
+
header,
|
166
|
+
Contracts::Support.indent_string(
|
167
|
+
[
|
168
|
+
expected_line,
|
169
|
+
actual_line,
|
170
|
+
value_line,
|
171
|
+
contract_line,
|
172
|
+
position_line,
|
173
|
+
].join("\n"),
|
174
|
+
indent_amount,
|
175
|
+
),
|
176
|
+
].join("\n")
|
134
177
|
end
|
135
178
|
|
136
179
|
# Callback for when a contract fails. By default it raises
|
@@ -146,7 +189,7 @@ class Contract < Contracts::Decorator
|
|
146
189
|
# puts failure_msg(data)
|
147
190
|
# exit
|
148
191
|
# end
|
149
|
-
def self.failure_callback(data, use_pattern_matching
|
192
|
+
def self.failure_callback(data, use_pattern_matching: true)
|
150
193
|
if data[:contracts].pattern_match? && use_pattern_matching
|
151
194
|
return DEFAULT_FAILURE_CALLBACK.call(data)
|
152
195
|
end
|
@@ -206,19 +249,21 @@ class Contract < Contracts::Decorator
|
|
206
249
|
# returns true if it appended nil
|
207
250
|
def maybe_append_block! args, blk
|
208
251
|
return false unless @has_proc_contract && !blk &&
|
209
|
-
|
252
|
+
(@args_contract_index || args.size < args_contracts.size)
|
253
|
+
|
210
254
|
args << nil
|
211
255
|
true
|
212
256
|
end
|
213
257
|
|
214
258
|
# Same thing for when we have named params but didn't pass any in.
|
215
259
|
# returns true if it appended nil
|
216
|
-
def maybe_append_options! args, blk
|
260
|
+
def maybe_append_options! args, kargs, blk
|
217
261
|
return false unless @has_options_contract
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
262
|
+
|
263
|
+
if @has_proc_contract && args_contracts[-2].is_a?(Contracts::Builtin::KeywordArgs)
|
264
|
+
args.insert(-2, kargs)
|
265
|
+
elsif args_contracts[-1].is_a?(Contracts::Builtin::KeywordArgs)
|
266
|
+
args << kargs
|
222
267
|
end
|
223
268
|
true
|
224
269
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Contracts
|
4
|
+
module Attrs
|
5
|
+
def attr_reader_with_contract(*names, contract)
|
6
|
+
names.each do |name|
|
7
|
+
Contract Contracts::None => contract
|
8
|
+
attr_reader(name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def attr_writer_with_contract(*names, contract)
|
13
|
+
names.each do |name|
|
14
|
+
Contract contract => contract
|
15
|
+
attr_writer(name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def attr_accessor_with_contract(*names, contract)
|
20
|
+
attr_reader_with_contract(*names, contract)
|
21
|
+
attr_writer_with_contract(*names, contract)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
include Attrs
|
26
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "contracts/formatters"
|
2
4
|
require "set"
|
3
5
|
|
@@ -30,35 +32,35 @@ module Contracts
|
|
30
32
|
# Check that an argument is a positive number.
|
31
33
|
class Pos
|
32
34
|
def self.valid? val
|
33
|
-
val
|
35
|
+
val.is_a?(Numeric) && val.positive?
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
37
39
|
# Check that an argument is a negative number.
|
38
40
|
class Neg
|
39
41
|
def self.valid? val
|
40
|
-
val
|
42
|
+
val.is_a?(Numeric) && val.negative?
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
44
46
|
# Check that an argument is an +Integer+.
|
45
47
|
class Int
|
46
48
|
def self.valid? val
|
47
|
-
val
|
49
|
+
val.is_a?(Integer)
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
53
|
# Check that an argument is a natural number (includes zero).
|
52
54
|
class Nat
|
53
55
|
def self.valid? val
|
54
|
-
val
|
56
|
+
val.is_a?(Integer) && val >= 0
|
55
57
|
end
|
56
58
|
end
|
57
59
|
|
58
60
|
# Check that an argument is a positive natural number (excludes zero).
|
59
61
|
class NatPos
|
60
62
|
def self.valid? val
|
61
|
-
val
|
63
|
+
val.is_a?(Integer) && val.positive?
|
62
64
|
end
|
63
65
|
end
|
64
66
|
|
@@ -96,6 +98,7 @@ module Contracts
|
|
96
98
|
# Example: <tt>Or[Fixnum, Float]</tt>
|
97
99
|
class Or < CallableClass
|
98
100
|
def initialize(*vals)
|
101
|
+
super()
|
99
102
|
@vals = vals
|
100
103
|
end
|
101
104
|
|
@@ -107,9 +110,11 @@ module Contracts
|
|
107
110
|
end
|
108
111
|
|
109
112
|
def to_s
|
113
|
+
# rubocop:disable Style/StringConcatenation
|
110
114
|
@vals[0, @vals.size-1].map do |x|
|
111
115
|
InspectWrapper.create(x)
|
112
116
|
end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s
|
117
|
+
# rubocop:enable Style/StringConcatenation
|
113
118
|
end
|
114
119
|
end
|
115
120
|
|
@@ -118,6 +123,7 @@ module Contracts
|
|
118
123
|
# Example: <tt>Xor[Fixnum, Float]</tt>
|
119
124
|
class Xor < CallableClass
|
120
125
|
def initialize(*vals)
|
126
|
+
super()
|
121
127
|
@vals = vals
|
122
128
|
end
|
123
129
|
|
@@ -130,9 +136,11 @@ module Contracts
|
|
130
136
|
end
|
131
137
|
|
132
138
|
def to_s
|
139
|
+
# rubocop:disable Style/StringConcatenation
|
133
140
|
@vals[0, @vals.size-1].map do |x|
|
134
141
|
InspectWrapper.create(x)
|
135
142
|
end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s
|
143
|
+
# rubocop:enable Style/StringConcatenation
|
136
144
|
end
|
137
145
|
end
|
138
146
|
|
@@ -141,6 +149,7 @@ module Contracts
|
|
141
149
|
# Example: <tt>And[Fixnum, Float]</tt>
|
142
150
|
class And < CallableClass
|
143
151
|
def initialize(*vals)
|
152
|
+
super()
|
144
153
|
@vals = vals
|
145
154
|
end
|
146
155
|
|
@@ -152,9 +161,11 @@ module Contracts
|
|
152
161
|
end
|
153
162
|
|
154
163
|
def to_s
|
164
|
+
# rubocop:disable Style/StringConcatenation
|
155
165
|
@vals[0, @vals.size-1].map do |x|
|
156
166
|
InspectWrapper.create(x)
|
157
167
|
end.join(", ") + " and " + InspectWrapper.create(@vals[-1]).to_s
|
168
|
+
# rubocop:enable Style/StringConcatenation
|
158
169
|
end
|
159
170
|
end
|
160
171
|
|
@@ -164,6 +175,7 @@ module Contracts
|
|
164
175
|
# Example: <tt>RespondTo[:password, :credit_card]</tt>
|
165
176
|
class RespondTo < CallableClass
|
166
177
|
def initialize(*meths)
|
178
|
+
super()
|
167
179
|
@meths = meths
|
168
180
|
end
|
169
181
|
|
@@ -185,6 +197,7 @@ module Contracts
|
|
185
197
|
# Example: <tt>Send[:valid?]</tt>
|
186
198
|
class Send < CallableClass
|
187
199
|
def initialize(*meths)
|
200
|
+
super()
|
188
201
|
@meths = meths
|
189
202
|
end
|
190
203
|
|
@@ -204,11 +217,12 @@ module Contracts
|
|
204
217
|
# Example: <tt>Exactly[Numeric]</tt>
|
205
218
|
class Exactly < CallableClass
|
206
219
|
def initialize(cls)
|
220
|
+
super()
|
207
221
|
@cls = cls
|
208
222
|
end
|
209
223
|
|
210
224
|
def valid?(val)
|
211
|
-
val.
|
225
|
+
val.instance_of?(@cls)
|
212
226
|
end
|
213
227
|
|
214
228
|
def to_s
|
@@ -222,6 +236,7 @@ module Contracts
|
|
222
236
|
# Example: <tt>Enum[:a, :b, :c]</tt>?
|
223
237
|
class Enum < CallableClass
|
224
238
|
def initialize(*vals)
|
239
|
+
super()
|
225
240
|
@vals = vals
|
226
241
|
end
|
227
242
|
|
@@ -235,6 +250,7 @@ module Contracts
|
|
235
250
|
# Example: <tt>Eq[Class]</tt>
|
236
251
|
class Eq < CallableClass
|
237
252
|
def initialize(value)
|
253
|
+
super()
|
238
254
|
@value = value
|
239
255
|
end
|
240
256
|
|
@@ -252,6 +268,7 @@ module Contracts
|
|
252
268
|
# Example: <tt>Not[nil]</tt>
|
253
269
|
class Not < CallableClass
|
254
270
|
def initialize(*vals)
|
271
|
+
super()
|
255
272
|
@vals = vals
|
256
273
|
end
|
257
274
|
|
@@ -275,12 +292,14 @@ module Contracts
|
|
275
292
|
# Example: <tt>CollectionOf[Array, Num]</tt>
|
276
293
|
class CollectionOf < CallableClass
|
277
294
|
def initialize(collection_class, contract)
|
295
|
+
super()
|
278
296
|
@collection_class = collection_class
|
279
297
|
@contract = contract
|
280
298
|
end
|
281
299
|
|
282
300
|
def valid?(vals)
|
283
301
|
return false unless vals.is_a?(@collection_class)
|
302
|
+
|
284
303
|
vals.all? do |val|
|
285
304
|
res, _ = Contract.valid?(val, @contract)
|
286
305
|
res
|
@@ -298,7 +317,7 @@ module Contracts
|
|
298
317
|
end
|
299
318
|
|
300
319
|
def new(contract)
|
301
|
-
@before_new
|
320
|
+
@before_new&.call
|
302
321
|
CollectionOf.new(@collection_class, contract)
|
303
322
|
end
|
304
323
|
|
@@ -324,7 +343,9 @@ module Contracts
|
|
324
343
|
# Example: <tt>Args[Or[String, Num]]</tt>
|
325
344
|
class Args < CallableClass
|
326
345
|
attr_reader :contract
|
346
|
+
|
327
347
|
def initialize(contract)
|
348
|
+
super()
|
328
349
|
@contract = contract
|
329
350
|
end
|
330
351
|
|
@@ -343,6 +364,7 @@ module Contracts
|
|
343
364
|
# Example: <tt>RangeOf[Nat]</tt>, <tt>RangeOf[Date]</tt>, ...
|
344
365
|
class RangeOf < CallableClass
|
345
366
|
def initialize(contract)
|
367
|
+
super()
|
346
368
|
@contract = contract
|
347
369
|
end
|
348
370
|
|
@@ -364,6 +386,7 @@ module Contracts
|
|
364
386
|
INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract"
|
365
387
|
|
366
388
|
def initialize(key, value = nil)
|
389
|
+
super()
|
367
390
|
if value
|
368
391
|
@key = key
|
369
392
|
@value = value
|
@@ -376,6 +399,7 @@ module Contracts
|
|
376
399
|
|
377
400
|
def valid?(hash)
|
378
401
|
return false unless hash.is_a?(Hash)
|
402
|
+
|
379
403
|
keys_match = hash.keys.map { |k| Contract.valid?(k, @key) }.all?
|
380
404
|
vals_match = hash.values.map { |v| Contract.valid?(v, @value) }.all?
|
381
405
|
|
@@ -393,15 +417,39 @@ module Contracts
|
|
393
417
|
end
|
394
418
|
end
|
395
419
|
|
420
|
+
# Use this to specify the Hash characteristics. This contracts fails
|
421
|
+
# if there are any extra keys that don't have contracts on them.
|
422
|
+
# Example: <tt>StrictHash[{ a: String, b: Bool }]</tt>
|
423
|
+
class StrictHash < CallableClass
|
424
|
+
attr_reader :contract_hash
|
425
|
+
|
426
|
+
def initialize(contract_hash)
|
427
|
+
super()
|
428
|
+
@contract_hash = contract_hash
|
429
|
+
end
|
430
|
+
|
431
|
+
def valid?(arg)
|
432
|
+
return false unless arg.is_a?(Hash)
|
433
|
+
return false unless arg.keys.sort.eql?(contract_hash.keys.sort)
|
434
|
+
|
435
|
+
contract_hash.all? do |key, contract|
|
436
|
+
contract_hash.key?(key) && Contract.valid?(arg[key], contract)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
396
441
|
# Use this for specifying contracts for keyword arguments
|
397
442
|
# Example: <tt>KeywordArgs[ e: Range, f: Optional[Num] ]</tt>
|
398
443
|
class KeywordArgs < CallableClass
|
399
444
|
def initialize(options)
|
445
|
+
super()
|
400
446
|
@options = options
|
401
447
|
end
|
402
448
|
|
403
449
|
def valid?(hash)
|
450
|
+
return false unless hash.is_a?(Hash)
|
404
451
|
return false unless hash.keys - options.keys == []
|
452
|
+
|
405
453
|
options.all? do |key, contract|
|
406
454
|
Optional._valid?(hash, key, contract)
|
407
455
|
end
|
@@ -420,6 +468,31 @@ module Contracts
|
|
420
468
|
attr_reader :options
|
421
469
|
end
|
422
470
|
|
471
|
+
# Use this for specifying contracts for class arguments
|
472
|
+
# Example: <tt>DescendantOf[ e: Range, f: Optional[Num] ]</tt>
|
473
|
+
class DescendantOf < CallableClass
|
474
|
+
def initialize(parent_class)
|
475
|
+
super()
|
476
|
+
@parent_class = parent_class
|
477
|
+
end
|
478
|
+
|
479
|
+
def valid?(given_class)
|
480
|
+
given_class.is_a?(Class) && given_class.ancestors.include?(parent_class)
|
481
|
+
end
|
482
|
+
|
483
|
+
def to_s
|
484
|
+
"DescendantOf[#{parent_class}]"
|
485
|
+
end
|
486
|
+
|
487
|
+
def inspect
|
488
|
+
to_s
|
489
|
+
end
|
490
|
+
|
491
|
+
private
|
492
|
+
|
493
|
+
attr_reader :parent_class
|
494
|
+
end
|
495
|
+
|
423
496
|
# Use this for specifying optional keyword argument
|
424
497
|
# Example: <tt>Optional[Num]</tt>
|
425
498
|
class Optional < CallableClass
|
@@ -428,11 +501,13 @@ module Contracts
|
|
428
501
|
|
429
502
|
def self._valid?(hash, key, contract)
|
430
503
|
return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional)
|
504
|
+
|
431
505
|
contract.within_opt_hash!
|
432
506
|
!hash.key?(key) || Contract.valid?(hash[key], contract)
|
433
507
|
end
|
434
508
|
|
435
509
|
def initialize(contract)
|
510
|
+
super()
|
436
511
|
@contract = contract
|
437
512
|
@within_opt_hash = false
|
438
513
|
end
|
@@ -461,6 +536,7 @@ module Contracts
|
|
461
536
|
|
462
537
|
def ensure_within_opt_hash
|
463
538
|
return if within_opt_hash
|
539
|
+
|
464
540
|
fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH
|
465
541
|
end
|
466
542
|
|
@@ -486,7 +562,9 @@ module Contracts
|
|
486
562
|
# Example: <tt>Func[Num => Num] # the function should take a number and return a number</tt>
|
487
563
|
class Func < CallableClass
|
488
564
|
attr_reader :contracts
|
565
|
+
|
489
566
|
def initialize(*contracts)
|
567
|
+
super()
|
490
568
|
@contracts = contracts
|
491
569
|
end
|
492
570
|
end
|