contracts-lite 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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