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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +30 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rubocop.yml +131 -0
- data/.travis.yml +22 -0
- data/Gemfile +8 -4
- data/README.md +13 -3
- data/Rakefile +1 -0
- data/TUTORIAL.md +6 -6
- data/bin/console +11 -0
- data/{script → bin}/rubocop +1 -2
- data/contracts.gemspec +1 -1
- data/docs/_config.yml +1 -0
- data/docs/index.md +112 -0
- data/lib/contracts.rb +8 -210
- data/lib/contracts/args_validator.rb +96 -0
- data/lib/contracts/builtin_contracts.rb +42 -35
- data/lib/contracts/contract.rb +136 -0
- data/lib/contracts/contract/call_with.rb +119 -0
- data/lib/contracts/contract/failure_callback.rb +61 -0
- data/lib/contracts/{validators.rb → contract/validators.rb} +9 -14
- data/lib/contracts/core.rb +4 -25
- data/lib/contracts/decorators.rb +1 -1
- data/lib/contracts/engine/base.rb +4 -5
- data/lib/contracts/engine/eigenclass.rb +3 -4
- data/lib/contracts/engine/target.rb +1 -3
- data/lib/contracts/error_formatter.rb +6 -6
- data/lib/contracts/errors.rb +2 -2
- data/lib/contracts/formatters.rb +20 -21
- data/lib/contracts/invariants.rb +10 -6
- data/lib/contracts/method_handler.rb +26 -31
- data/lib/contracts/method_reference.rb +13 -14
- data/lib/contracts/support.rb +2 -16
- data/lib/contracts/version.rb +1 -1
- data/spec/contracts_spec.rb +25 -6
- data/spec/error_formatter_spec.rb +0 -1
- data/spec/fixtures/fixtures.rb +5 -5
- data/spec/ruby_version_specific/contracts_spec_1.9.rb +17 -1
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +1 -1
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +1 -1
- data/spec/spec_helper.rb +17 -68
- metadata +15 -8
- data/TODO.markdown +0 -6
- data/lib/contracts/call_with.rb +0 -97
@@ -2,8 +2,6 @@ module Contracts
|
|
2
2
|
module Engine
|
3
3
|
# Represents class in question
|
4
4
|
class Target
|
5
|
-
# Creates new instance of Target
|
6
|
-
#
|
7
5
|
# @param [Class] target - class in question
|
8
6
|
def initialize(target)
|
9
7
|
@target = target
|
@@ -54,7 +52,7 @@ module Contracts
|
|
54
52
|
self.class.new(eigenclass).apply(Eigenclass)
|
55
53
|
eigenclass.extend(MethodDecorators)
|
56
54
|
# FIXME; this should detect what user uses `include Contracts` or
|
57
|
-
# `include Contracts
|
55
|
+
# `include Contracts::Core`
|
58
56
|
eigenclass.send(:include, Contracts)
|
59
57
|
end
|
60
58
|
|
@@ -1,5 +1,8 @@
|
|
1
1
|
module Contracts
|
2
2
|
class ErrorFormatters
|
3
|
+
# # Given a hash, prints out a failure message.
|
4
|
+
# # This function is used by the default #failure_callback method
|
5
|
+
# # and uses the hash passed into the failure_callback method.
|
3
6
|
def self.failure_msg(data)
|
4
7
|
class_for(data).new(data).message
|
5
8
|
end
|
@@ -55,7 +58,7 @@ module Contracts
|
|
55
58
|
class KeywordArgsErrorFormatter < DefaultErrorFormatter
|
56
59
|
def message
|
57
60
|
s = []
|
58
|
-
s <<
|
61
|
+
s << header.to_s
|
59
62
|
s << " Expected: #{expected}"
|
60
63
|
s << " Actual: #{data[:arg].inspect}"
|
61
64
|
s << " Missing Contract: #{missing_contract_info}" unless missing_contract_info.empty?
|
@@ -101,11 +104,8 @@ module Contracts
|
|
101
104
|
end
|
102
105
|
|
103
106
|
def check_contract(contract, value)
|
104
|
-
if contract.respond_to?(:valid?)
|
105
|
-
|
106
|
-
else
|
107
|
-
value.is_a?(contract)
|
108
|
-
end
|
107
|
+
return contract.valid?(value) if contract.respond_to?(:valid?)
|
108
|
+
value.is_a?(contract)
|
109
109
|
rescue
|
110
110
|
false
|
111
111
|
end
|
data/lib/contracts/errors.rb
CHANGED
@@ -59,10 +59,10 @@ module Contracts
|
|
59
59
|
# you can use `Contract` definition here now
|
60
60
|
end
|
61
61
|
end
|
62
|
-
```}
|
62
|
+
```}.freeze
|
63
63
|
|
64
64
|
attr_reader :message
|
65
|
-
|
65
|
+
alias to_s message
|
66
66
|
|
67
67
|
def initialize(message = DEFAULT_MESSAGE)
|
68
68
|
@message = message
|
data/lib/contracts/formatters.rb
CHANGED
@@ -6,18 +6,15 @@ module Contracts
|
|
6
6
|
# @param full [Boolean] if false only unique `to_s` values will be output,
|
7
7
|
# non unique values become empty string.
|
8
8
|
def initialize(contract, full = true)
|
9
|
-
@contract
|
9
|
+
@contract = contract
|
10
|
+
@full = full
|
10
11
|
end
|
11
12
|
|
12
13
|
# Formats any type of Contract.
|
13
14
|
def contract(contract = @contract)
|
14
|
-
if contract.is_a?(Hash)
|
15
|
-
|
16
|
-
|
17
|
-
array_contract(contract)
|
18
|
-
else
|
19
|
-
InspectWrapper.create(contract, @full)
|
20
|
-
end
|
15
|
+
return hash_contract(contract) if contract.is_a?(Hash)
|
16
|
+
return array_contract(contract) if contract.is_a?(Array)
|
17
|
+
InspectWrapper.create(contract, @full)
|
21
18
|
end
|
22
19
|
|
23
20
|
# Formats Hash contracts.
|
@@ -41,17 +38,19 @@ module Contracts
|
|
41
38
|
# InspectWrapper is a factory, will never be an instance
|
42
39
|
# @return [ClassInspectWrapper, ObjectInspectWrapper]
|
43
40
|
def self.create(value, full = true)
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
41
|
+
inspector_klass(value).new(value, full)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.inspector_klass(value)
|
45
|
+
return ClassInspectWrapper if value.class == Class
|
46
|
+
ObjectInspectWrapper
|
49
47
|
end
|
50
48
|
|
51
49
|
# @param full [Boolean] if false only unique `to_s` values will be output,
|
52
50
|
# non unique values become empty string.
|
53
51
|
def initialize(value, full)
|
54
|
-
@value
|
52
|
+
@value = value
|
53
|
+
@full = full
|
55
54
|
end
|
56
55
|
|
57
56
|
# Inspect different types of contract values.
|
@@ -61,17 +60,17 @@ module Contracts
|
|
61
60
|
# Primitive values e.g. 42, true, nil will be left alone.
|
62
61
|
def inspect
|
63
62
|
return "" unless full?
|
64
|
-
return @value.inspect
|
65
|
-
return @value.to_s
|
63
|
+
return @value.inspect if empty_val?
|
64
|
+
return @value.to_s if plain?
|
66
65
|
return delim(@value.to_s) if useful_to_s?
|
67
66
|
useful_inspect
|
68
67
|
end
|
69
68
|
|
70
69
|
def delim(value)
|
71
|
-
@full ? "(#{value})" :
|
70
|
+
@full ? "(#{value})" : value.to_s
|
72
71
|
end
|
73
72
|
|
74
|
-
# Eliminates
|
73
|
+
# Eliminates erroneous quotes in output that plain inspect includes.
|
75
74
|
def to_s
|
76
75
|
inspect
|
77
76
|
end
|
@@ -84,17 +83,17 @@ module Contracts
|
|
84
83
|
|
85
84
|
def full?
|
86
85
|
@full ||
|
87
|
-
@value.is_a?(Hash) ||
|
86
|
+
@value.is_a?(Hash) ||
|
87
|
+
@value.is_a?(Array) ||
|
88
88
|
(!plain? && useful_to_s?)
|
89
89
|
end
|
90
90
|
|
91
|
+
# Not a type of contract that can have a custom to_s defined
|
91
92
|
def plain?
|
92
|
-
# Not a type of contract that can have a custom to_s defined
|
93
93
|
!@value.is_a?(Builtin::CallableClass) && @value.class != Class
|
94
94
|
end
|
95
95
|
|
96
96
|
def useful_to_s?
|
97
|
-
# Useless to_s value or no custom to_s behavious defined
|
98
97
|
!empty_to_s? && custom_to_s?
|
99
98
|
end
|
100
99
|
|
data/lib/contracts/invariants.rb
CHANGED
@@ -36,7 +36,9 @@ module Contracts
|
|
36
36
|
|
37
37
|
class Invariant
|
38
38
|
def initialize(klass, name, &condition)
|
39
|
-
@klass
|
39
|
+
@klass = klass
|
40
|
+
@name = name
|
41
|
+
@condition = condition
|
40
42
|
end
|
41
43
|
|
42
44
|
def expected
|
@@ -46,14 +48,16 @@ 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)
|
56
|
-
|
60
|
+
raise InvariantError, failure_msg(data)
|
57
61
|
end
|
58
62
|
|
59
63
|
def self.failure_msg(data)
|
@@ -3,14 +3,14 @@ module Contracts
|
|
3
3
|
# Represents single such method
|
4
4
|
class MethodHandler
|
5
5
|
METHOD_REFERENCE_FACTORY = {
|
6
|
-
:class_methods
|
6
|
+
:class_methods => SingletonMethodReference,
|
7
7
|
:instance_methods => MethodReference
|
8
|
-
}
|
8
|
+
}.freeze
|
9
9
|
|
10
10
|
RAW_METHOD_STRATEGY = {
|
11
|
-
:class_methods
|
11
|
+
:class_methods => lambda { |target, name| target.method(name) },
|
12
12
|
:instance_methods => lambda { |target, name| target.instance_method(name) }
|
13
|
-
}
|
13
|
+
}.freeze
|
14
14
|
|
15
15
|
# Creates new instance of MethodHandler
|
16
16
|
#
|
@@ -18,9 +18,9 @@ module Contracts
|
|
18
18
|
# @param [Bool] is_class_method
|
19
19
|
# @param [Class] target - class that method got added to
|
20
20
|
def initialize(method_name, is_class_method, target)
|
21
|
-
@method_name
|
21
|
+
@method_name = method_name
|
22
22
|
@is_class_method = is_class_method
|
23
|
-
@target
|
23
|
+
@target = target
|
24
24
|
end
|
25
25
|
|
26
26
|
# Handles method addition
|
@@ -58,7 +58,7 @@ module Contracts
|
|
58
58
|
end
|
59
59
|
# _method_type is required for assigning it to local variable with
|
60
60
|
# the same name. See: #redefine_method
|
61
|
-
|
61
|
+
alias _method_type method_type
|
62
62
|
|
63
63
|
def method_reference
|
64
64
|
@_method_reference ||= METHOD_REFERENCE_FACTORY[method_type].new(method_name, raw_method)
|
@@ -102,8 +102,8 @@ module Contracts
|
|
102
102
|
return if ignore_decorators?
|
103
103
|
|
104
104
|
# Those are required for instance_eval to be able to refer them
|
105
|
-
name
|
106
|
-
method_type
|
105
|
+
name = method_name
|
106
|
+
method_type = _method_type
|
107
107
|
current_engine = engine
|
108
108
|
|
109
109
|
# We are gonna redefine original method here
|
@@ -113,11 +113,15 @@ module Contracts
|
|
113
113
|
# If we weren't able to find any ancestor that has decorated methods
|
114
114
|
# FIXME : this looks like untested code (commenting it out doesn't make specs red)
|
115
115
|
unless engine
|
116
|
-
|
116
|
+
raise "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
117
|
end
|
118
118
|
|
119
119
|
# Fetch decorated methods out of the contracts engine
|
120
120
|
decorated_methods = engine.decorated_methods_for(method_type, name)
|
121
|
+
expected_error = decorated_methods[0].failure_exception
|
122
|
+
error_to_return = nil
|
123
|
+
result = nil
|
124
|
+
success = nil
|
121
125
|
|
122
126
|
# This adds support for overloading methods. Here we go
|
123
127
|
# through each method and call it with the arguments.
|
@@ -125,38 +129,29 @@ 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
|
131
|
-
expected_error = decorated_methods[0].failure_exception
|
132
|
-
|
133
|
-
until success
|
134
|
-
decorated_method = decorated_methods[i]
|
135
|
-
i += 1
|
132
|
+
decorated_methods.any? do |decorated_method|
|
136
133
|
begin
|
137
|
-
success = true
|
138
134
|
result = decorated_method.call_with(self, *args, &blk)
|
135
|
+
success = true
|
139
136
|
rescue expected_error => error
|
140
|
-
|
141
|
-
|
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
|
137
|
+
error_to_return = error
|
138
|
+
nil
|
148
139
|
end
|
149
140
|
end
|
141
|
+
return result if success
|
150
142
|
|
151
|
-
|
152
|
-
|
143
|
+
begin
|
144
|
+
::Contract.failure_callback(error_to_return.data, false)
|
145
|
+
rescue expected_error
|
146
|
+
raise error_to_return.to_contract_error
|
147
|
+
end
|
153
148
|
end
|
154
149
|
end
|
155
150
|
|
156
151
|
def validate_decorators!
|
157
152
|
return if decorators.size == 1
|
158
153
|
|
159
|
-
|
154
|
+
raise %{
|
160
155
|
Oops, it looks like method '#{name}' has multiple contracts:
|
161
156
|
#{decorators.map { |x| x[1][0].inspect }.join("\n")}
|
162
157
|
|
@@ -181,7 +176,7 @@ https://github.com/egonSchiele/contracts.ruby/issues
|
|
181
176
|
|
182
177
|
return if matched.empty?
|
183
178
|
|
184
|
-
|
179
|
+
raise ContractError.new(%{
|
185
180
|
It looks like you are trying to use pattern-matching, but
|
186
181
|
multiple definitions for function '#{method_name}' have the same
|
187
182
|
contract for input parameters:
|
@@ -7,21 +7,20 @@ module Contracts
|
|
7
7
|
# name - name of the method
|
8
8
|
# method - method object
|
9
9
|
def initialize(name, method)
|
10
|
-
@name
|
10
|
+
@name = name
|
11
11
|
@method = method
|
12
12
|
end
|
13
13
|
|
14
|
-
# Returns method_position, delegates to Support.method_position
|
15
14
|
def method_position
|
16
15
|
Support.method_position(@method)
|
17
16
|
end
|
18
17
|
|
19
18
|
# Makes a method re-definition in proper way
|
20
19
|
def make_definition(this, &blk)
|
21
|
-
is_private
|
20
|
+
is_private = private?(this)
|
22
21
|
is_protected = protected?(this)
|
23
22
|
alias_target(this).send(:define_method, name, &blk)
|
24
|
-
make_private(this)
|
23
|
+
make_private(this) if is_private
|
25
24
|
make_protected(this) if is_protected
|
26
25
|
end
|
27
26
|
|
@@ -45,18 +44,18 @@ module Contracts
|
|
45
44
|
|
46
45
|
private
|
47
46
|
|
48
|
-
# Makes a method private
|
49
|
-
def make_private(this)
|
50
|
-
original_name = name
|
51
|
-
alias_target(this).class_eval { private original_name }
|
52
|
-
end
|
53
|
-
|
54
47
|
def private?(this)
|
55
|
-
this.private_instance_methods.
|
48
|
+
this.private_instance_methods.include?(name)
|
56
49
|
end
|
57
50
|
|
58
51
|
def protected?(this)
|
59
|
-
this.protected_instance_methods.
|
52
|
+
this.protected_instance_methods.include?(name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Makes a method private
|
56
|
+
def make_private(this)
|
57
|
+
original_name = name
|
58
|
+
alias_target(this).class_eval { private original_name }
|
60
59
|
end
|
61
60
|
|
62
61
|
# Makes a method protected
|
@@ -85,11 +84,11 @@ module Contracts
|
|
85
84
|
private
|
86
85
|
|
87
86
|
def private?(this)
|
88
|
-
this.private_methods.
|
87
|
+
this.private_methods.include?(name)
|
89
88
|
end
|
90
89
|
|
91
90
|
def protected?(this)
|
92
|
-
this.protected_methods.
|
91
|
+
this.protected_methods.include?(name)
|
93
92
|
end
|
94
93
|
|
95
94
|
# Return alias target for singleton methods.
|
data/lib/contracts/support.rb
CHANGED
@@ -3,17 +3,8 @@ module Contracts
|
|
3
3
|
class << self
|
4
4
|
def method_position(method)
|
5
5
|
return method.method_position if method.is_a?(MethodReference)
|
6
|
-
|
7
|
-
|
8
|
-
if method.respond_to?(:__file__)
|
9
|
-
method.__file__ + ":" + method.__line__.to_s
|
10
|
-
else
|
11
|
-
method.inspect
|
12
|
-
end
|
13
|
-
else
|
14
|
-
file, line = method.source_location
|
15
|
-
file + ":" + line.to_s
|
16
|
-
end
|
6
|
+
file, line = method.source_location
|
7
|
+
"#{file}:#{line}"
|
17
8
|
end
|
18
9
|
|
19
10
|
def method_name(method)
|
@@ -33,11 +24,6 @@ module Contracts
|
|
33
24
|
contract.object_id
|
34
25
|
end
|
35
26
|
|
36
|
-
def eigenclass_hierarchy_supported?
|
37
|
-
return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
|
38
|
-
RUBY_VERSION.to_f > 1.8
|
39
|
-
end
|
40
|
-
|
41
27
|
def eigenclass_of(target)
|
42
28
|
class << target; self; end
|
43
29
|
end
|
data/lib/contracts/version.rb
CHANGED
data/spec/contracts_spec.rb
CHANGED
@@ -7,12 +7,23 @@ RSpec.describe "Contracts:" do
|
|
7
7
|
it "should fail for insufficient arguments" do
|
8
8
|
expect do
|
9
9
|
@o.hello
|
10
|
-
end.to raise_error
|
10
|
+
end.to raise_error(ArgumentError)
|
11
11
|
end
|
12
12
|
|
13
13
|
it "should fail for insufficient contracts" do
|
14
14
|
expect { @o.bad_double(2) }.to raise_error(ContractError)
|
15
15
|
end
|
16
|
+
|
17
|
+
it "requires last argument to be a hash for more than one contracts" do
|
18
|
+
expect{
|
19
|
+
Class.new(GenericExample) do
|
20
|
+
Contract C::Num, C::Num
|
21
|
+
def no_args_bad_contract(num)
|
22
|
+
1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}.to raise_error(RuntimeError, Regexp.new("A contract should be written as"))
|
26
|
+
end
|
16
27
|
end
|
17
28
|
|
18
29
|
describe "contracts for functions with no arguments" do
|
@@ -32,7 +43,7 @@ RSpec.describe "Contracts:" do
|
|
32
43
|
1
|
33
44
|
end
|
34
45
|
end
|
35
|
-
end.to raise_error
|
46
|
+
end.to raise_error(NameError)
|
36
47
|
end
|
37
48
|
end
|
38
49
|
|
@@ -462,9 +473,17 @@ RSpec.describe "Contracts:" do
|
|
462
473
|
@o.with_partial_sums(1, 2, 3)
|
463
474
|
end.to raise_error(ContractError, /Actual: nil/)
|
464
475
|
|
465
|
-
|
466
|
-
|
467
|
-
|
476
|
+
to_call = lambda {
|
477
|
+
expect do
|
478
|
+
# because lambda is not a valid &block, it is considered to belong to the splat argument, and raises for num contract!
|
479
|
+
@o.with_partial_sums(1, 2, 3, lambda { |x| x })
|
480
|
+
end
|
481
|
+
}
|
482
|
+
to_call.call.to raise_error(ContractError, /Contract violation for argument 4 of 5/)
|
483
|
+
to_call.call.to raise_error(ContractError, /Actual: #<Proc/)
|
484
|
+
to_call.call.to raise_error(ContractError, Regexp.new("Expected: \\(SplatArgs\\[Contracts::Builtin::Num\\]\\)"))
|
485
|
+
|
486
|
+
|
468
487
|
end
|
469
488
|
|
470
489
|
context "when block has Func contract" do
|
@@ -696,7 +715,7 @@ RSpec.describe "Contracts:" do
|
|
696
715
|
it "should apply the contract to an inherited method" do
|
697
716
|
c = Child.new
|
698
717
|
expect { c.double(2) }.to_not raise_error
|
699
|
-
expect { c.double("asd") }.to raise_error
|
718
|
+
expect { c.double("asd") }.to raise_error(ParamContractError)
|
700
719
|
end
|
701
720
|
end
|
702
721
|
|