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,13 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  module CallWith
3
- def call_with(this, *args, &blk)
5
+ def call_with(this, *args, **kargs, &blk)
6
+ call_with_inner(false, this, *args, **kargs, &blk)
7
+ end
8
+
9
+ def call_with_inner(returns, this, *args, **kargs, &blk)
4
10
  args << blk if blk
5
11
 
6
12
  # Explicitly append blk=nil if nil != Proc contract violation anticipated
7
13
  nil_block_appended = maybe_append_block!(args, blk)
8
14
 
9
15
  # Explicitly append options={} if Hash contract is present
10
- maybe_append_options!(args, blk)
16
+ kargs_appended = maybe_append_options!(args, kargs, blk)
11
17
 
12
18
  # Loop forward validating the arguments up to the splat (if there is one)
13
19
  (@args_contract_index || args.size).times do |i|
@@ -16,17 +22,23 @@ module Contracts
16
22
  validator = @args_validators[i]
17
23
 
18
24
  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)
25
+ data = {
26
+ arg: arg,
27
+ contract: contract,
28
+ class: klass,
29
+ method: method,
30
+ contracts: self,
31
+ arg_pos: i+1,
32
+ total_args: args.size,
33
+ return_value: false,
34
+ }
35
+ return ParamContractError.new("as return value", data) if returns
36
+ return unless Contract.failure_callback(data)
27
37
  end
28
38
 
29
- if contract.is_a?(Contracts::Func)
39
+ if contract.is_a?(Contracts::Func) && blk && !nil_block_appended
40
+ blk = Contract.new(klass, arg, *contract.contracts)
41
+ elsif contract.is_a?(Contracts::Func)
30
42
  args[i] = Contract.new(klass, arg, *contract.contracts)
31
43
  end
32
44
  end
@@ -49,14 +61,18 @@ module Contracts
49
61
  validator = @args_validators[args_contracts.size - 1 - j]
50
62
 
51
63
  unless validator && validator[arg]
52
- return unless Contract.failure_callback(:arg => arg,
53
- :contract => contract,
54
- :class => klass,
55
- :method => method,
56
- :contracts => self,
57
- :arg_pos => i-1,
58
- :total_args => args.size,
59
- :return_value => false)
64
+ # rubocop:disable Style/SoleNestedConditional
65
+ return unless Contract.failure_callback({
66
+ :arg => arg,
67
+ :contract => contract,
68
+ :class => klass,
69
+ :method => method,
70
+ :contracts => self,
71
+ :arg_pos => i - 1,
72
+ :total_args => args.size,
73
+ :return_value => false,
74
+ })
75
+ # rubocop:enable Style/SoleNestedConditional
60
76
  end
61
77
 
62
78
  if contract.is_a?(Contracts::Func)
@@ -68,21 +84,27 @@ module Contracts
68
84
  # If we put the block into args for validating, restore the args
69
85
  # OR if we added a fake nil at the end because a block wasn't passed in.
70
86
  args.slice!(-1) if blk || nil_block_appended
87
+ args.slice!(-1) if kargs_appended
71
88
  result = if method.respond_to?(:call)
72
89
  # proc, block, lambda, etc
73
- method.call(*args, &blk)
90
+ method.call(*args, **kargs, &blk)
74
91
  else
75
- # original method name referrence
76
- method.send_to(this, *args, &blk)
92
+ # original method name reference
93
+ # Don't reassign blk, else Travis CI shows "stack level too deep".
94
+ target_blk = blk
95
+ target_blk = lambda { |*params| blk.call(*params) } if blk.is_a?(Contract)
96
+ method.send_to(this, *args, **kargs, &target_blk)
77
97
  end
78
98
 
79
99
  unless @ret_validator[result]
80
- Contract.failure_callback(:arg => result,
81
- :contract => ret_contract,
82
- :class => klass,
83
- :method => method,
84
- :contracts => self,
85
- :return_value => true)
100
+ Contract.failure_callback({
101
+ arg: result,
102
+ contract: ret_contract,
103
+ class: klass,
104
+ method: method,
105
+ contracts: self,
106
+ return_value: true,
107
+ })
86
108
  end
87
109
 
88
110
  this.verify_invariants!(method) if this.respond_to?(:verify_invariants!)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  module Core
3
5
  def self.included(base)
@@ -9,8 +11,6 @@ module Contracts
9
11
  end
10
12
 
11
13
  def self.common(base)
12
- return if base.respond_to?(:Contract)
13
-
14
14
  base.extend(MethodDecorators)
15
15
 
16
16
  base.instance_eval do
@@ -27,7 +27,7 @@ module Contracts
27
27
  # NOTE: Workaround for `defined?(super)` bug in ruby 1.9.2
28
28
  # source: http://stackoverflow.com/a/11181685
29
29
  # bug: https://bugs.ruby-lang.org/issues/6644
30
- base.class_eval <<-RUBY
30
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
31
31
  # TODO: deprecate
32
32
  # Required when contracts are included in global scope
33
33
  def Contract(*args)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  module MethodDecorators
3
5
  def self.extended(klass)
@@ -6,6 +8,7 @@ module Contracts
6
8
 
7
9
  def inherited(subclass)
8
10
  Engine.fetch_from(subclass).set_eigenclass_owner
11
+ super
9
12
  end
10
13
 
11
14
  def method_added(name)
@@ -24,6 +27,7 @@ module Contracts
24
27
  class << self; attr_accessor :decorators; end
25
28
 
26
29
  def self.inherited(klass)
30
+ super
27
31
  name = klass.name.gsub(/^./) { |m| m.downcase }
28
32
 
29
33
  return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
@@ -32,11 +36,11 @@ module Contracts
32
36
  # make a new method that is the name of your decorator.
33
37
  # that method accepts random args and a block.
34
38
  # inside, `decorate` is called with those params.
35
- MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
39
+ MethodDecorators.module_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
36
40
  def #{klass}(*args, &blk)
37
41
  ::Contracts::Engine.fetch_from(self).decorate(#{klass}, *args, &blk)
38
42
  end
39
- ruby_eval
43
+ RUBY_EVAL
40
44
  end
41
45
 
42
46
  def initialize(klass, method)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "contracts/engine/base"
2
4
  require "contracts/engine/target"
3
5
  require "contracts/engine/eigenclass"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  module Engine
3
5
  # Contracts engine
@@ -90,7 +92,7 @@ module Contracts
90
92
  def nearest_decorated_ancestor
91
93
  current = klass
92
94
  current_engine = self
93
- ancestors = current.ancestors[1..-1]
95
+ ancestors = current.ancestors[1..]
94
96
 
95
97
  while current && current_engine && !current_engine.decorated_methods?
96
98
  current = ancestors.shift
@@ -109,8 +111,7 @@ module Contracts
109
111
  end
110
112
 
111
113
  # No-op because it is safe to add decorators to normal classes
112
- def validate!
113
- end
114
+ def validate!; end
114
115
 
115
116
  def pop_decorators
116
117
  decorators.tap { clear_decorators }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  module Engine
3
5
  # Special case of contracts engine for eigenclasses
@@ -27,8 +29,7 @@ module Contracts
27
29
  end
28
30
 
29
31
  # No-op for eigenclasses
30
- def set_eigenclass_owner
31
- end
32
+ def set_eigenclass_owner; end
32
33
 
33
34
  # Fetches just eigenclasses decorators
34
35
  def all_decorators
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  module Engine
3
5
  # Represents class in question
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # @private
2
4
  # Base class for Contract errors
3
5
  #
@@ -65,6 +67,7 @@ module Contracts
65
67
  alias_method :to_s, :message
66
68
 
67
69
  def initialize(message = DEFAULT_MESSAGE)
70
+ super
68
71
  @message = message
69
72
  end
70
73
  end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pp"
4
+
1
5
  module Contracts
2
6
  # A namespace for classes related to formatting.
3
7
  module Formatters
@@ -5,18 +9,19 @@ module Contracts
5
9
  class Expected
6
10
  # @param full [Boolean] if false only unique `to_s` values will be output,
7
11
  # non unique values become empty string.
8
- def initialize(contract, full = true)
12
+ def initialize(contract, full: true)
9
13
  @contract, @full = contract, full
10
14
  end
11
15
 
12
16
  # Formats any type of Contract.
13
17
  def contract(contract = @contract)
14
- if contract.is_a?(Hash)
18
+ case contract
19
+ when Hash
15
20
  hash_contract(contract)
16
- elsif contract.is_a?(Array)
21
+ when Array
17
22
  array_contract(contract)
18
23
  else
19
- InspectWrapper.create(contract, @full)
24
+ InspectWrapper.create(contract, full: @full)
20
25
  end
21
26
  end
22
27
 
@@ -24,14 +29,14 @@ module Contracts
24
29
  def hash_contract(hash)
25
30
  @full = true # Complex values output completely, overriding @full
26
31
  hash.inject({}) do |repr, (k, v)|
27
- repr.merge(k => InspectWrapper.create(contract(v), @full))
28
- end.inspect
32
+ repr.merge(k => InspectWrapper.create(contract(v), full: @full))
33
+ end
29
34
  end
30
35
 
31
36
  # Formats Array contracts.
32
37
  def array_contract(array)
33
38
  @full = true
34
- array.map { |v| InspectWrapper.create(contract(v), @full) }.inspect
39
+ array.map { |v| InspectWrapper.create(contract(v), full: @full) }
35
40
  end
36
41
  end
37
42
 
@@ -40,8 +45,8 @@ module Contracts
40
45
  module InspectWrapper
41
46
  # InspectWrapper is a factory, will never be an instance
42
47
  # @return [ClassInspectWrapper, ObjectInspectWrapper]
43
- def self.create(value, full = true)
44
- if value.class == Class
48
+ def self.create(value, full: true)
49
+ if value.instance_of?(Class)
45
50
  ClassInspectWrapper
46
51
  else
47
52
  ObjectInspectWrapper
@@ -64,6 +69,7 @@ module Contracts
64
69
  return @value.inspect if empty_val?
65
70
  return @value.to_s if plain?
66
71
  return delim(@value.to_s) if useful_to_s?
72
+
67
73
  useful_inspect
68
74
  end
69
75
 
@@ -94,7 +100,7 @@ module Contracts
94
100
  end
95
101
 
96
102
  def useful_to_s?
97
- # Useless to_s value or no custom to_s behavious defined
103
+ # Useless to_s value or no custom to_s behaviour defined
98
104
  !empty_to_s? && custom_to_s?
99
105
  end
100
106
 
@@ -123,7 +129,7 @@ module Contracts
123
129
  include InspectWrapper
124
130
 
125
131
  def custom_to_s?
126
- !@value.to_s.match(/#\<\w+:.+\>/)
132
+ !@value.to_s.match(/#<\w+:.+>/)
127
133
  end
128
134
 
129
135
  def useful_inspect
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  module Invariants
3
5
  def self.included(base)
@@ -46,10 +48,12 @@ module Contracts
46
48
  def check_on(target, method)
47
49
  return if target.instance_eval(&@condition)
48
50
 
49
- self.class.failure_callback(:expected => expected,
50
- :actual => false,
51
- :target => target,
52
- :method => method)
51
+ self.class.failure_callback({
52
+ expected: expected,
53
+ actual: false,
54
+ target: target,
55
+ method: method,
56
+ })
53
57
  end
54
58
 
55
59
  def self.failure_callback(data)
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contracts
2
4
  # Handles class and instance methods addition
3
5
  # Represents single such method
4
6
  class MethodHandler
5
7
  METHOD_REFERENCE_FACTORY = {
6
8
  :class_methods => SingletonMethodReference,
7
- :instance_methods => MethodReference
9
+ :instance_methods => MethodReference,
8
10
  }
9
11
 
10
12
  RAW_METHOD_STRATEGY = {
11
13
  :class_methods => lambda { |target, name| target.method(name) },
12
- :instance_methods => lambda { |target, name| target.instance_method(name) }
14
+ :instance_methods => lambda { |target, name| target.instance_method(name) },
13
15
  }
14
16
 
15
17
  # Creates new instance of MethodHandler
@@ -78,11 +80,13 @@ module Contracts
78
80
 
79
81
  def pattern_matching?
80
82
  return @_pattern_matching if defined?(@_pattern_matching)
83
+
81
84
  @_pattern_matching = decorated_methods.any? { |x| x.method != method_reference }
82
85
  end
83
86
 
84
87
  def mark_pattern_matching_decorators
85
88
  return unless pattern_matching?
89
+
86
90
  decorated_methods.each(&:pattern_match!)
87
91
  end
88
92
 
@@ -107,13 +111,13 @@ module Contracts
107
111
  current_engine = engine
108
112
 
109
113
  # We are gonna redefine original method here
110
- method_reference.make_definition(target) do |*args, &blk|
114
+ method_reference.make_definition(target) do |*args, **kargs, &blk|
111
115
  engine = current_engine.nearest_decorated_ancestor
112
116
 
113
117
  # If we weren't able to find any ancestor that has decorated methods
114
118
  # FIXME : this looks like untested code (commenting it out doesn't make specs red)
115
119
  unless engine
116
- fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case."
120
+ fail "Couldn't find decorator for method #{self.class.name}:#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case."
117
121
  end
118
122
 
119
123
  # Fetch decorated methods out of the contracts engine
@@ -125,31 +129,26 @@ module Contracts
125
129
  # function. Otherwise we return the result.
126
130
  # If we run out of functions, we raise the last error, but
127
131
  # convert it to_contract_error.
128
- success = false
129
- i = 0
130
- result = nil
132
+
131
133
  expected_error = decorated_methods[0].failure_exception
134
+ last_error = nil
132
135
 
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
136
+ decorated_methods.each do |decorated_method|
137
+ result = decorated_method.call_with_inner(true, self, *args, **kargs, &blk)
138
+ return result unless result.is_a?(ParamContractError)
139
+
140
+ last_error = result
149
141
  end
150
142
 
151
- # Return the result of successfully called method
152
- result
143
+ begin
144
+ if ::Contract.failure_callback(last_error&.data, use_pattern_matching: false)
145
+ decorated_methods.last.call_with_inner(false, self, *args, **kargs, &blk)
146
+ end
147
+ # rubocop:disable Naming/RescuedExceptionsVariableName
148
+ rescue expected_error => final_error
149
+ raise final_error.to_contract_error
150
+ # rubocop:enable Naming/RescuedExceptionsVariableName
151
+ end
153
152
  end
154
153
  end
155
154
 
@@ -157,7 +156,7 @@ module Contracts
157
156
  return if decorators.size == 1
158
157
 
159
158
  fail %{
160
- Oops, it looks like method '#{name}' has multiple contracts:
159
+ Oops, it looks like method '#{method_name}' has multiple contracts:
161
160
  #{decorators.map { |x| x[1][0].inspect }.join("\n")}
162
161
 
163
162
  Did you accidentally put more than one contract on a single function, like so?
@@ -181,7 +180,8 @@ https://github.com/egonSchiele/contracts.ruby/issues
181
180
 
182
181
  return if matched.empty?
183
182
 
184
- fail ContractError.new(%{
183
+ fail ContractError.new(
184
+ %{
185
185
  It looks like you are trying to use pattern-matching, but
186
186
  multiple definitions for function '#{method_name}' have the same
187
187
  contract for input parameters:
@@ -189,7 +189,9 @@ contract for input parameters:
189
189
  #{(matched + [decorator]).map(&:to_s).join("\n")}
190
190
 
191
191
  Each definition needs to have a different contract for the parameters.
192
- }, {})
192
+ },
193
+ {},
194
+ )
193
195
  end
194
196
  end
195
197
  end