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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/code_style_checks.yaml +36 -0
  3. data/.github/workflows/tests.yaml +41 -0
  4. data/CHANGELOG.markdown +54 -7
  5. data/Gemfile +9 -5
  6. data/LICENSE +23 -0
  7. data/README.md +14 -6
  8. data/Rakefile +5 -6
  9. data/TUTORIAL.md +28 -1
  10. data/contracts.gemspec +9 -1
  11. data/dependabot.yml +20 -0
  12. data/features/basics/pretty-print.feature +241 -0
  13. data/features/support/env.rb +2 -0
  14. data/lib/contracts.rb +64 -19
  15. data/lib/contracts/attrs.rb +26 -0
  16. data/lib/contracts/builtin_contracts.rb +85 -7
  17. data/lib/contracts/call_with.rb +50 -28
  18. data/lib/contracts/core.rb +3 -3
  19. data/lib/contracts/decorators.rb +6 -2
  20. data/lib/contracts/engine.rb +2 -0
  21. data/lib/contracts/engine/base.rb +4 -3
  22. data/lib/contracts/engine/eigenclass.rb +3 -2
  23. data/lib/contracts/engine/target.rb +2 -0
  24. data/lib/contracts/errors.rb +3 -0
  25. data/lib/contracts/formatters.rb +17 -11
  26. data/lib/contracts/invariants.rb +8 -4
  27. data/lib/contracts/method_handler.rb +30 -28
  28. data/lib/contracts/method_reference.rb +4 -2
  29. data/lib/contracts/support.rb +14 -10
  30. data/lib/contracts/validators.rb +6 -2
  31. data/lib/contracts/version.rb +3 -1
  32. data/spec/attrs_spec.rb +119 -0
  33. data/spec/builtin_contracts_spec.rb +155 -97
  34. data/spec/contracts_spec.rb +54 -12
  35. data/spec/fixtures/fixtures.rb +49 -2
  36. data/spec/methods_spec.rb +54 -0
  37. data/spec/override_validators_spec.rb +3 -3
  38. data/spec/ruby_version_specific/contracts_spec_2.0.rb +17 -2
  39. data/spec/ruby_version_specific/contracts_spec_2.1.rb +1 -1
  40. data/spec/validators_spec.rb +1 -1
  41. metadata +22 -10
  42. data/script/cucumber +0 -5
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "aruba/cucumber"
2
4
  require "aruba/jruby" if RUBY_PLATFORM == "java"
3
5
 
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?(Hash)
100
+ penultimate_contract.is_a?(Contracts::Builtin::KeywordArgs)
96
101
  else
97
- last_contract.is_a?(Hash)
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 c
105
- c.is_a?(Class) ? c.name : c.class.name
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
- expected = Contracts::Formatters::Expected.new(data[:contract]).contract
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
- %{#{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} }
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 = true)
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
- (@args_contract_index || args.size < args_contracts.size)
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
- if @has_proc_contract && args_contracts[-2].is_a?(Hash) && !args[-2].is_a?(Hash)
219
- args.insert(-2, {})
220
- elsif args_contracts[-1].is_a?(Hash) && !args[-1].is_a?(Hash)
221
- args << {}
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 && val.is_a?(Numeric) && val > 0
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 && val.is_a?(Numeric) && val < 0
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 && val.is_a?(Integer)
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 && val.is_a?(Integer) && val >= 0
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 && val.is_a?(Integer) && val > 0
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.class == @cls
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 && @before_new.call
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