contracts-lite 0.14.0 → 0.15.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +30 -0
  3. data/.gitignore +6 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +131 -0
  6. data/.travis.yml +22 -0
  7. data/Gemfile +8 -4
  8. data/README.md +13 -3
  9. data/Rakefile +1 -0
  10. data/TUTORIAL.md +6 -6
  11. data/bin/console +11 -0
  12. data/{script → bin}/rubocop +1 -2
  13. data/contracts.gemspec +1 -1
  14. data/docs/_config.yml +1 -0
  15. data/docs/index.md +112 -0
  16. data/lib/contracts.rb +8 -210
  17. data/lib/contracts/args_validator.rb +96 -0
  18. data/lib/contracts/builtin_contracts.rb +42 -35
  19. data/lib/contracts/contract.rb +136 -0
  20. data/lib/contracts/contract/call_with.rb +119 -0
  21. data/lib/contracts/contract/failure_callback.rb +61 -0
  22. data/lib/contracts/{validators.rb → contract/validators.rb} +9 -14
  23. data/lib/contracts/core.rb +4 -25
  24. data/lib/contracts/decorators.rb +1 -1
  25. data/lib/contracts/engine/base.rb +4 -5
  26. data/lib/contracts/engine/eigenclass.rb +3 -4
  27. data/lib/contracts/engine/target.rb +1 -3
  28. data/lib/contracts/error_formatter.rb +6 -6
  29. data/lib/contracts/errors.rb +2 -2
  30. data/lib/contracts/formatters.rb +20 -21
  31. data/lib/contracts/invariants.rb +10 -6
  32. data/lib/contracts/method_handler.rb +26 -31
  33. data/lib/contracts/method_reference.rb +13 -14
  34. data/lib/contracts/support.rb +2 -16
  35. data/lib/contracts/version.rb +1 -1
  36. data/spec/contracts_spec.rb +25 -6
  37. data/spec/error_formatter_spec.rb +0 -1
  38. data/spec/fixtures/fixtures.rb +5 -5
  39. data/spec/ruby_version_specific/contracts_spec_1.9.rb +17 -1
  40. data/spec/ruby_version_specific/contracts_spec_2.0.rb +1 -1
  41. data/spec/ruby_version_specific/contracts_spec_2.1.rb +1 -1
  42. data/spec/spec_helper.rb +17 -68
  43. metadata +15 -8
  44. data/TODO.markdown +0 -6
  45. data/lib/contracts/call_with.rb +0 -97
@@ -1,3 +1,5 @@
1
+ require "set"
2
+ require "contracts/formatters"
1
3
  require "contracts/builtin_contracts"
2
4
  require "contracts/decorators"
3
5
  require "contracts/errors"
@@ -8,10 +10,14 @@ require "contracts/method_reference"
8
10
  require "contracts/support"
9
11
  require "contracts/engine"
10
12
  require "contracts/method_handler"
11
- require "contracts/validators"
12
- require "contracts/call_with"
13
13
  require "contracts/core"
14
14
 
15
+ require "contracts/args_validator"
16
+ require "contracts/contract/call_with"
17
+ require "contracts/contract/validators"
18
+ require "contracts/contract/failure_callback"
19
+ require "contracts/contract"
20
+
15
21
  module Contracts
16
22
  def self.included(base)
17
23
  base.send(:include, Core)
@@ -21,211 +27,3 @@ module Contracts
21
27
  base.send(:extend, Core)
22
28
  end
23
29
  end
24
-
25
- # This is the main Contract class. When you write a new contract, you'll
26
- # write it as:
27
- #
28
- # Contract [contract names] => return_value
29
- #
30
- # This class also provides useful callbacks and a validation method.
31
- #
32
- # For #make_validator and related logic see file
33
- # lib/contracts/validators.rb
34
- # For #call_with and related logic see file
35
- # lib/contracts/call_with.rb
36
- class Contract < Contracts::Decorator
37
- extend Contracts::Validators
38
- include Contracts::CallWith
39
-
40
- # Default implementation of failure_callback. Provided as a block to be able
41
- # to monkey patch #failure_callback only temporary and then switch it back.
42
- # First important usage - for specs.
43
- DEFAULT_FAILURE_CALLBACK = proc do |data|
44
- if data[:return_value]
45
- # this failed on the return contract
46
- fail ReturnContractError.new(failure_msg(data), data)
47
- else
48
- # this failed for a param contract
49
- fail data[:contracts].failure_exception.new(failure_msg(data), data)
50
- end
51
- end
52
-
53
- attr_reader :args_contracts, :ret_contract, :klass, :method
54
- def initialize(klass, method, *contracts)
55
- unless contracts.last.is_a?(Hash)
56
- unless contracts.one?
57
- fail %{
58
- It looks like your contract for #{method.name} doesn't have a return
59
- value. A contract should be written as `Contract arg1, arg2 =>
60
- return_value`.
61
- }.strip
62
- end
63
- contracts = [nil => contracts[-1]]
64
- end
65
-
66
- # internally we just convert that return value syntax back to an array
67
- @args_contracts = contracts[0, contracts.size - 1] + contracts[-1].keys
68
-
69
- @ret_contract = contracts[-1].values[0]
70
-
71
- @args_validators = args_contracts.map do |contract|
72
- Contract.make_validator(contract)
73
- end
74
-
75
- @args_contract_index = args_contracts.index do |contract|
76
- contract.is_a? Contracts::Args
77
- end
78
-
79
- @ret_validator = Contract.make_validator(ret_contract)
80
-
81
- @pattern_match = false
82
-
83
- # == @has_proc_contract
84
- last_contract = args_contracts.last
85
- is_a_proc = last_contract.is_a?(Class) && (last_contract <= Proc || last_contract <= Method)
86
- maybe_a_proc = last_contract.is_a?(Contracts::Maybe) && last_contract.include_proc?
87
-
88
- @has_proc_contract = is_a_proc || maybe_a_proc || last_contract.is_a?(Contracts::Func)
89
-
90
- # ====
91
-
92
- # == @has_options_contract
93
- last_contract = args_contracts.last
94
- penultimate_contract = args_contracts[-2]
95
- @has_options_contract = if @has_proc_contract
96
- penultimate_contract.is_a?(Hash) || penultimate_contract.is_a?(Contracts::Builtin::KeywordArgs)
97
- else
98
- last_contract.is_a?(Hash) || last_contract.is_a?(Contracts::Builtin::KeywordArgs)
99
- end
100
- # ===
101
-
102
- @klass, @method = klass, method
103
- end
104
-
105
- def pretty_contract c
106
- c.is_a?(Class) ? c.name : c.class.name
107
- end
108
-
109
- def to_s
110
- args = args_contracts.map { |c| pretty_contract(c) }.join(", ")
111
- ret = pretty_contract(ret_contract)
112
- ("#{args} => #{ret}").gsub("Contracts::Builtin::", "")
113
- end
114
-
115
- # Given a hash, prints out a failure message.
116
- # This function is used by the default #failure_callback method
117
- # and uses the hash passed into the failure_callback method.
118
- def self.failure_msg(data)
119
- Contracts::ErrorFormatters.failure_msg(data)
120
- end
121
-
122
- # Callback for when a contract fails. By default it raises
123
- # an error and prints detailed info about the contract that
124
- # failed. You can also monkeypatch this callback to do whatever
125
- # you want...log the error, send you an email, print an error
126
- # message, etc.
127
- #
128
- # Example of monkeypatching:
129
- #
130
- # def Contract.failure_callback(data)
131
- # puts "You had an error!"
132
- # puts failure_msg(data)
133
- # exit
134
- # end
135
- def self.failure_callback(data, use_pattern_matching = true)
136
- if data[:contracts].pattern_match? && use_pattern_matching
137
- return DEFAULT_FAILURE_CALLBACK.call(data)
138
- end
139
-
140
- fetch_failure_callback.call(data)
141
- end
142
-
143
- # Used to override failure_callback without monkeypatching.
144
- #
145
- # Takes: block parameter, that should accept one argument - data.
146
- #
147
- # Example usage:
148
- #
149
- # Contract.override_failure_callback do |data|
150
- # puts "You had an error"
151
- # puts failure_msg(data)
152
- # exit
153
- # end
154
- def self.override_failure_callback(&blk)
155
- @failure_callback = blk
156
- end
157
-
158
- # Used to restore default failure callback
159
- def self.restore_failure_callback
160
- @failure_callback = DEFAULT_FAILURE_CALLBACK
161
- end
162
-
163
- def self.fetch_failure_callback
164
- @failure_callback ||= DEFAULT_FAILURE_CALLBACK
165
- end
166
-
167
- # Used to verify if an argument satisfies a contract.
168
- #
169
- # Takes: an argument and a contract.
170
- #
171
- # Returns: a tuple: [Boolean, metadata]. The boolean indicates
172
- # whether the contract was valid or not. If it wasn't, metadata
173
- # contains some useful information about the failure.
174
- def self.valid?(arg, contract)
175
- make_validator(contract)[arg]
176
- end
177
-
178
- def [](*args, &blk)
179
- call(*args, &blk)
180
- end
181
-
182
- def call(*args, &blk)
183
- call_with(nil, *args, &blk)
184
- end
185
-
186
- # if we specified a proc in the contract but didn't pass one in,
187
- # it's possible we are going to pass in a block instead. So lets
188
- # append a nil to the list of args just so it doesn't fail.
189
-
190
- # a better way to handle this might be to take this into account
191
- # before throwing a "mismatched # of args" error.
192
- # returns true if it appended nil
193
- def maybe_append_block! args, blk
194
- return false unless @has_proc_contract && !blk &&
195
- (@args_contract_index || args.size < args_contracts.size)
196
- args << nil
197
- true
198
- end
199
-
200
- # Same thing for when we have named params but didn't pass any in.
201
- # returns true if it appended nil
202
- def maybe_append_options! args, blk
203
- return false unless @has_options_contract
204
- if @has_proc_contract && (args_contracts[-2].is_a?(Hash) || args_contracts[-2].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-2].is_a?(Hash)
205
- args.insert(-2, {})
206
- elsif (args_contracts[-1].is_a?(Hash) || args_contracts[-1].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-1].is_a?(Hash)
207
- args << {}
208
- end
209
- true
210
- end
211
-
212
- # Used to determine type of failure exception this contract should raise in case of failure
213
- def failure_exception
214
- if pattern_match?
215
- PatternMatchingError
216
- else
217
- ParamContractError
218
- end
219
- end
220
-
221
- # @private
222
- # Used internally to mark contract as pattern matching contract
223
- def pattern_match!
224
- @pattern_match = true
225
- end
226
-
227
- # Used to determine if contract is a pattern matching contract
228
- def pattern_match?
229
- @pattern_match == true
230
- end
231
- end
@@ -0,0 +1,96 @@
1
+ module Contracts
2
+ class ArgsValidator
3
+ attr_accessor :splat_args_contract_index, :klass, :method, :contracts, :args_contracts, :args_validators
4
+ def initialize(opts)
5
+ @splat_args_contract_index = opts.fetch(:splat_args_contract_index)
6
+ @klass = opts.fetch(:klass)
7
+ @method = opts.fetch(:method)
8
+ @contracts = opts.fetch(:contracts)
9
+ @args_contracts = opts.fetch(:args_contracts)
10
+ @args_validators = opts.fetch(:args_validators)
11
+ end
12
+
13
+ # Loop forward validating the arguments up to the splat (if there is one)
14
+ # may change the `args` param
15
+ def validate_args_before_splat!(args)
16
+ (splat_args_contract_index || args.size).times do |i|
17
+ validate!(args, i)
18
+ end
19
+ end
20
+
21
+ ## possibilities
22
+ # - splat is last argument, like: def hello(a, b, *args)
23
+ # - splat is not last argument, like: def hello(*args, n)
24
+ def validate_splat_args_and_after!(args)
25
+ return unless splat_args_contract_index
26
+ from, count = splat_range(args)
27
+ validate_splat(args, from, count)
28
+
29
+ splat_upper_bound = from + count
30
+ return if splat_upper_bound == args.size
31
+
32
+ validate_rest(args, from, splat_upper_bound)
33
+ end
34
+
35
+ private
36
+
37
+ def validate_splat(args, from, count)
38
+ args.slice(from, count).each_with_index do |_arg, index|
39
+ arg_index = from + index
40
+ contract = args_contracts[from]
41
+ validator = args_validators[from]
42
+ validate!(args, arg_index, contract, validator)
43
+ end
44
+ end
45
+
46
+ def validate_rest(args, from, splat_upper_bound)
47
+ args[splat_upper_bound..-1].each_with_index do |_arg, index|
48
+ arg_index = splat_upper_bound + index
49
+ contract_index = from + index + 1
50
+ contract = args_contracts[contract_index]
51
+ validator = args_validators[contract_index]
52
+ validate!(args, arg_index, contract, validator)
53
+ end
54
+ end
55
+
56
+ # string, splat[integer], float
57
+ # - "aom", 2, 3, 4, 5, 0.1 >>> 1, 4
58
+ # - "aom", 2, 0.1 >>> 1, 1
59
+ # - "aom", 2, 3, 4, 5, 6, 7, 0.1 >>> 1, 6
60
+
61
+ # splat[integer]
62
+ # - 2, 3, 4, 5 >>> 0, 4
63
+ # - 2 >>> 0, 1
64
+ # - 2, 3, 4, 5, 6, 7 >>> 0, 6
65
+ def splat_range(args)
66
+ args_after_splat = args_contracts.size - (splat_args_contract_index + 1)
67
+ in_splat = args.size - args_after_splat - splat_args_contract_index
68
+
69
+ [splat_args_contract_index, in_splat]
70
+ end
71
+
72
+ def validate!(args, index, contract = nil, validator = nil)
73
+ arg = args[index]
74
+ contract ||= args_contracts[index]
75
+ validator ||= args_validators[index]
76
+ fail_if_invalid(validator, arg, index + 1, args.size, contract)
77
+
78
+ return unless contract.is_a?(Contracts::Func)
79
+ args[index] = Contract.new(klass, arg, *contract.contracts)
80
+ end
81
+
82
+ def fail_if_invalid(validator, arg, arg_pos, args_size, contract)
83
+ return if validator && validator.call(arg)
84
+ throw :return, Contracts::CallWith::SILENT_FAILURE unless Contract.failure_callback(
85
+ :arg => arg,
86
+ :contract => contract,
87
+ :class => klass,
88
+ :method => method,
89
+ :contracts => contracts,
90
+ :arg_pos => arg_pos,
91
+ :total_args => args_size,
92
+ :return_value => false
93
+ )
94
+ end
95
+ end
96
+ end
@@ -1,7 +1,3 @@
1
- require "contracts/formatters"
2
- require "set"
3
-
4
- # rdoc
5
1
  # This module contains all the builtin contracts.
6
2
  # If you want to use them, first:
7
3
  #
@@ -22,56 +18,56 @@ module Contracts
22
18
  module Builtin
23
19
  # Check that an argument is +Numeric+.
24
20
  class Num
25
- def self.valid? val
21
+ def self.valid?(val)
26
22
  val.is_a? Numeric
27
23
  end
28
24
  end
29
25
 
30
26
  # Check that an argument is a positive number.
31
27
  class Pos
32
- def self.valid? val
28
+ def self.valid?(val)
33
29
  val && val.is_a?(Numeric) && val > 0
34
30
  end
35
31
  end
36
32
 
37
33
  # Check that an argument is a negative number.
38
34
  class Neg
39
- def self.valid? val
35
+ def self.valid?(val)
40
36
  val && val.is_a?(Numeric) && val < 0
41
37
  end
42
38
  end
43
39
 
44
40
  # Check that an argument is an +Integer+.
45
41
  class Int
46
- def self.valid? val
42
+ def self.valid?(val)
47
43
  val && val.is_a?(Integer)
48
44
  end
49
45
  end
50
46
 
51
47
  # Check that an argument is a natural number (includes zero).
52
48
  class Nat
53
- def self.valid? val
49
+ def self.valid?(val)
54
50
  val && val.is_a?(Integer) && val >= 0
55
51
  end
56
52
  end
57
53
 
58
54
  # Check that an argument is a positive natural number (excludes zero).
59
55
  class NatPos
60
- def self.valid? val
56
+ def self.valid?(val)
61
57
  val && val.is_a?(Integer) && val > 0
62
58
  end
63
59
  end
64
60
 
65
61
  # Passes for any argument.
66
62
  class Any
67
- def self.valid? val
63
+ def self.valid?(val)
68
64
  true
69
65
  end
70
66
  end
71
67
 
72
68
  # Fails for any argument.
73
69
  class None
74
- def self.valid? val
70
+ def self.valid?(val)
75
71
  false
76
72
  end
77
73
  end
@@ -91,6 +87,15 @@ module Contracts
91
87
  end
92
88
  end
93
89
 
90
+ class EnumInspector
91
+ include ::Contracts::Formatters
92
+ def self.inspect(vals, last_join_word)
93
+ vals[0, vals.size-1].map do |x|
94
+ InspectWrapper.create(x)
95
+ end.join(", ") + " #{last_join_word} " + InspectWrapper.create(vals[-1]).to_s
96
+ end
97
+ end
98
+
94
99
  # Takes a variable number of contracts.
95
100
  # The contract passes if any of the contracts pass.
96
101
  # Example: <tt>Or[Fixnum, Float]</tt>
@@ -101,15 +106,13 @@ module Contracts
101
106
 
102
107
  def valid?(val)
103
108
  @vals.any? do |contract|
104
- res, _ = Contract.valid?(val, contract)
109
+ res, = Contract.valid?(val, contract)
105
110
  res
106
111
  end
107
112
  end
108
113
 
109
114
  def to_s
110
- @vals[0, @vals.size-1].map do |x|
111
- InspectWrapper.create(x)
112
- end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s
115
+ EnumInspector.inspect(@vals, "or")
113
116
  end
114
117
  end
115
118
 
@@ -123,16 +126,14 @@ module Contracts
123
126
 
124
127
  def valid?(val)
125
128
  results = @vals.map do |contract|
126
- res, _ = Contract.valid?(val, contract)
129
+ res, = Contract.valid?(val, contract)
127
130
  res
128
131
  end
129
132
  results.count(true) == 1
130
133
  end
131
134
 
132
135
  def to_s
133
- @vals[0, @vals.size-1].map do |x|
134
- InspectWrapper.create(x)
135
- end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s
136
+ EnumInspector.inspect(@vals, "xor")
136
137
  end
137
138
  end
138
139
 
@@ -146,15 +147,13 @@ module Contracts
146
147
 
147
148
  def valid?(val)
148
149
  @vals.all? do |contract|
149
- res, _ = Contract.valid?(val, contract)
150
+ res, = Contract.valid?(val, contract)
150
151
  res
151
152
  end
152
153
  end
153
154
 
154
155
  def to_s
155
- @vals[0, @vals.size-1].map do |x|
156
- InspectWrapper.create(x)
157
- end.join(", ") + " and " + InspectWrapper.create(@vals[-1]).to_s
156
+ EnumInspector.inspect(@vals, "and")
158
157
  end
159
158
  end
160
159
 
@@ -257,7 +256,7 @@ module Contracts
257
256
 
258
257
  def valid?(val)
259
258
  @vals.all? do |contract|
260
- res, _ = Contract.valid?(val, contract)
259
+ res, = Contract.valid?(val, contract)
261
260
  !res
262
261
  end
263
262
  end
@@ -282,7 +281,7 @@ module Contracts
282
281
  def valid?(vals)
283
282
  return false unless vals.is_a?(@collection_class)
284
283
  vals.all? do |val|
285
- res, _ = Contract.valid?(val, @contract)
284
+ res, = Contract.valid?(val, @contract)
286
285
  res
287
286
  end
288
287
  end
@@ -302,7 +301,7 @@ module Contracts
302
301
  CollectionOf.new(@collection_class, contract)
303
302
  end
304
303
 
305
- alias_method :[], :new
304
+ alias [] new
306
305
  end
307
306
  end
308
307
 
@@ -321,20 +320,28 @@ module Contracts
321
320
  # Used for <tt>*args</tt> (variadic functions). Takes a contract
322
321
  # and uses it to validate every element passed in
323
322
  # through <tt>*args</tt>.
324
- # Example: <tt>Args[Or[String, Num]]</tt>
325
- class Args < CallableClass
323
+ # Example: <tt>SplatArgs[Or[String, Num]]</tt>
324
+ class SplatArgs < CallableClass
326
325
  attr_reader :contract
327
326
  def initialize(contract)
328
327
  @contract = contract
329
328
  end
330
329
 
331
330
  def to_s
332
- "Args[#{@contract}]"
331
+ "SplatArgs[#{@contract}]"
332
+ end
333
+ end
334
+
335
+ # for compatibility
336
+ class Args < SplatArgs
337
+ def initialize(contract)
338
+ puts "DEPRECATION WARNING: \nContract::Args was renamed to Contract::SplatArgs, please update your before the next major version"
339
+ @contract = contract
333
340
  end
334
341
  end
335
342
 
336
343
  class Bool
337
- def self.valid? val
344
+ def self.valid?(val)
338
345
  val.is_a?(TrueClass) || val.is_a?(FalseClass)
339
346
  end
340
347
  end
@@ -361,7 +368,7 @@ module Contracts
361
368
  # one for hash keys and one for hash values.
362
369
  # Example: <tt>HashOf[Symbol, String]</tt>
363
370
  class HashOf < CallableClass
364
- INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract"
371
+ INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract".freeze
365
372
 
366
373
  def initialize(key, value = nil)
367
374
  if value
@@ -389,7 +396,7 @@ module Contracts
389
396
  private
390
397
 
391
398
  def validate_hash(hash)
392
- fail ArgumentError, INVALID_KEY_VALUE_PAIR unless hash.count == 1
399
+ raise ArgumentError, INVALID_KEY_VALUE_PAIR unless hash.count == 1
393
400
  end
394
401
  end
395
402
 
@@ -468,7 +475,7 @@ module Contracts
468
475
  # Example: <tt>Optional[Num]</tt>
469
476
  class Optional < CallableClass
470
477
  UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH =
471
- "Unable to use Optional contract outside of KeywordArgs contract"
478
+ "Unable to use Optional contract outside of KeywordArgs contract".freeze
472
479
 
473
480
  def self._valid?(hash, key, contract)
474
481
  return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional)
@@ -505,7 +512,7 @@ module Contracts
505
512
 
506
513
  def ensure_within_opt_hash
507
514
  return if within_opt_hash
508
- fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH
515
+ raise ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH
509
516
  end
510
517
 
511
518
  def formatted_contract