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/lib/contracts/call_with.rb
CHANGED
@@ -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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
76
|
-
|
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(
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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!)
|
data/lib/contracts/core.rb
CHANGED
@@ -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)
|
data/lib/contracts/decorators.rb
CHANGED
@@ -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 <<-
|
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
|
-
|
43
|
+
RUBY_EVAL
|
40
44
|
end
|
41
45
|
|
42
46
|
def initialize(klass, method)
|
data/lib/contracts/engine.rb
CHANGED
@@ -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
|
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
|
data/lib/contracts/errors.rb
CHANGED
data/lib/contracts/formatters.rb
CHANGED
@@ -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
|
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
|
-
|
18
|
+
case contract
|
19
|
+
when Hash
|
15
20
|
hash_contract(contract)
|
16
|
-
|
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
|
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) }
|
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
|
44
|
-
if value.
|
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
|
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(
|
132
|
+
!@value.to_s.match(/#<\w+:.+>/)
|
127
133
|
end
|
128
134
|
|
129
135
|
def useful_inspect
|
data/lib/contracts/invariants.rb
CHANGED
@@ -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(
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
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
|
-
|
129
|
-
i = 0
|
130
|
-
result = nil
|
132
|
+
|
131
133
|
expected_error = decorated_methods[0].failure_exception
|
134
|
+
last_error = nil
|
132
135
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
152
|
-
|
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 '#{
|
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
|