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.
- 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
@@ -0,0 +1,136 @@
|
|
1
|
+
# This is the main Contract class. When you write a new contract, you'll
|
2
|
+
# write it as:
|
3
|
+
#
|
4
|
+
# Contract [contract names] => return_value
|
5
|
+
#
|
6
|
+
# This class also provides useful callbacks and a validation method.
|
7
|
+
class Contract < Contracts::Decorator
|
8
|
+
extend Contracts::Validators
|
9
|
+
extend Contracts::FailureCallback
|
10
|
+
include Contracts::CallWith
|
11
|
+
|
12
|
+
# Used to verify if an argument satisfies a contract.
|
13
|
+
#
|
14
|
+
# Takes: an argument and a contract.
|
15
|
+
#
|
16
|
+
# Returns: a tuple: [Boolean, metadata]. The boolean indicates
|
17
|
+
# whether the contract was valid or not. If it wasn't, metadata
|
18
|
+
# contains some useful information about the failure.
|
19
|
+
def self.valid?(arg, contract)
|
20
|
+
make_validator(contract)[arg]
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :args_contracts, :ret_contract, :klass, :method, :ret_validator, :args_validators
|
24
|
+
def initialize(klass, method, *contracts)
|
25
|
+
contracts = correct_ret_only_contract(contracts, method)
|
26
|
+
|
27
|
+
# internally we just convert that return value syntax back to an array
|
28
|
+
@args_contracts = contracts[0, contracts.size - 1] + contracts[-1].keys
|
29
|
+
@ret_contract = contracts[-1].values[0]
|
30
|
+
|
31
|
+
determine_has_proc_contract!
|
32
|
+
determine_has_options_contract!
|
33
|
+
|
34
|
+
@pattern_match = false
|
35
|
+
@klass = klass
|
36
|
+
@method = method
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{args_contracts_to_s} => #{ret_contract_to_s}".gsub!("Contracts::Builtin::", "")
|
41
|
+
end
|
42
|
+
|
43
|
+
def [](*args, &blk)
|
44
|
+
call(*args, &blk)
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(*args, &blk)
|
48
|
+
call_with(nil, *args, &blk)
|
49
|
+
end
|
50
|
+
|
51
|
+
# mark contract as pattern matching contract
|
52
|
+
def pattern_match!
|
53
|
+
@pattern_match = true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Used to determine if contract is a pattern matching contract
|
57
|
+
def pattern_match?
|
58
|
+
@pattern_match
|
59
|
+
end
|
60
|
+
|
61
|
+
# Used to determine type of failure exception this contract should raise in case of failure
|
62
|
+
def failure_exception
|
63
|
+
return PatternMatchingError if pattern_match?
|
64
|
+
ParamContractError
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# BEFORE
|
70
|
+
# [Contracts::Builtin::Num]
|
71
|
+
# AFTER:
|
72
|
+
# [{nil=>Contracts::Builtin::Num}]
|
73
|
+
def correct_ret_only_contract(contracts, method)
|
74
|
+
unless contracts.last.is_a?(Hash)
|
75
|
+
unless contracts.one?
|
76
|
+
raise %{
|
77
|
+
It looks like your contract for #{method.name} doesn't have a return
|
78
|
+
value. A contract should be written as `Contract arg1, arg2 =>
|
79
|
+
return_value`.
|
80
|
+
}.strip
|
81
|
+
end
|
82
|
+
contracts = [nil => contracts[-1]]
|
83
|
+
end
|
84
|
+
contracts
|
85
|
+
end
|
86
|
+
|
87
|
+
def splat_args_contract_index
|
88
|
+
@splat_args_contract_index ||= args_contracts.index do |contract|
|
89
|
+
contract.is_a?(Contracts::SplatArgs)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def args_validators
|
94
|
+
@args_validators ||= args_contracts.map do |contract|
|
95
|
+
Contract.make_validator(contract)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def ret_validator
|
100
|
+
@ret_validator ||= Contract.make_validator(ret_contract)
|
101
|
+
end
|
102
|
+
|
103
|
+
def determine_has_proc_contract!
|
104
|
+
@has_proc_contract = kinda_proc?(args_contracts.last)
|
105
|
+
end
|
106
|
+
|
107
|
+
def determine_has_options_contract!
|
108
|
+
relevant_contract = (@has_proc_contract ? args_contracts[-2] : args_contracts[-1])
|
109
|
+
@has_options_contract = kinda_hash?(relevant_contract)
|
110
|
+
end
|
111
|
+
|
112
|
+
def args_contracts_to_s
|
113
|
+
args_contracts.map { |c| pretty_contract(c) }.join(", ")
|
114
|
+
end
|
115
|
+
|
116
|
+
def ret_contract_to_s
|
117
|
+
pretty_contract(ret_contract)
|
118
|
+
end
|
119
|
+
|
120
|
+
def pretty_contract(c)
|
121
|
+
return c.name if c.is_a?(Class)
|
122
|
+
c.class.name
|
123
|
+
end
|
124
|
+
|
125
|
+
def kinda_hash?(v)
|
126
|
+
v.is_a?(Hash) || v.is_a?(Contracts::Builtin::KeywordArgs)
|
127
|
+
end
|
128
|
+
|
129
|
+
def kinda_proc?(v)
|
130
|
+
is_a_proc = v.is_a?(Class) && (v <= Proc || v <= Method)
|
131
|
+
maybe_a_proc = v.is_a?(Contracts::Maybe) && v.include_proc?
|
132
|
+
is_func_contract = v.is_a?(Contracts::Func)
|
133
|
+
|
134
|
+
(is_a_proc || maybe_a_proc || is_func_contract)
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Contracts
|
2
|
+
module CallWith
|
3
|
+
SILENT_FAILURE = "silent_failure".freeze
|
4
|
+
def call_with(this, *args, &blk)
|
5
|
+
args << blk if blk
|
6
|
+
|
7
|
+
nil_block_appended = maybe_append_block!(args, blk)
|
8
|
+
maybe_append_options!(args, blk)
|
9
|
+
|
10
|
+
return if SILENT_FAILURE == catch(:return) do
|
11
|
+
args_validator.validate_args_before_splat!(args)
|
12
|
+
end
|
13
|
+
|
14
|
+
return if SILENT_FAILURE == catch(:return) do
|
15
|
+
args_validator.validate_splat_args_and_after!(args)
|
16
|
+
end
|
17
|
+
|
18
|
+
handle_result(this, args, blk, nil_block_appended)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def handle_result(this, args, blk, nil_block_appended)
|
24
|
+
restore_args!(args, blk, nil_block_appended)
|
25
|
+
result = execute_args(this, args, blk)
|
26
|
+
|
27
|
+
validate_result(result)
|
28
|
+
verify_invariants!(this)
|
29
|
+
wrap_result_if_func(result)
|
30
|
+
end
|
31
|
+
|
32
|
+
def args_validator
|
33
|
+
@args_validator ||= Contracts::ArgsValidator.new(
|
34
|
+
klass: klass,
|
35
|
+
method: method,
|
36
|
+
contracts: self,
|
37
|
+
args_contracts: args_contracts,
|
38
|
+
args_validators: args_validators,
|
39
|
+
splat_args_contract_index: splat_args_contract_index
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Restore the args
|
44
|
+
# - if we put the block into args for validating
|
45
|
+
# - OR if we added a fake nil at the end because a block wasn't passed in.
|
46
|
+
def restore_args!(args, blk, nil_block_appended)
|
47
|
+
args.slice!(-1) if blk || nil_block_appended
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_result(result)
|
51
|
+
return if ret_validator.call(result)
|
52
|
+
Contract.failure_callback(
|
53
|
+
:arg => result,
|
54
|
+
:contract => ret_contract,
|
55
|
+
:class => klass,
|
56
|
+
:method => method,
|
57
|
+
:contracts => self,
|
58
|
+
:return_value => true
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def verify_invariants!(this)
|
63
|
+
return unless this.respond_to?(:verify_invariants!)
|
64
|
+
this.verify_invariants!(method)
|
65
|
+
end
|
66
|
+
|
67
|
+
def wrap_result_if_func(result)
|
68
|
+
return result unless ret_contract.is_a?(Contracts::Func)
|
69
|
+
Contract.new(klass, result, *ret_contract.contracts)
|
70
|
+
end
|
71
|
+
|
72
|
+
def execute_args(this, args, blk)
|
73
|
+
# a `call`-able method, like proc, block, lambda
|
74
|
+
return method.call(*args, &blk) if method.respond_to?(:call)
|
75
|
+
|
76
|
+
# original method name referrence
|
77
|
+
method.send_to(this, *args, &blk)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Explicitly append blk=nil if nil != Proc contract violation anticipated
|
81
|
+
# if we specified a proc in the contract but didn't pass one in,
|
82
|
+
# it's possible we are going to pass in a block instead. So lets
|
83
|
+
# append a nil to the list of args just so it doesn't fail.
|
84
|
+
#
|
85
|
+
# a better way to handle this might be to take this into account
|
86
|
+
# before throwing a "mismatched # of args" error.
|
87
|
+
# returns true if it appended nil
|
88
|
+
def maybe_append_block!(args, blk)
|
89
|
+
return unless @has_proc_contract && !blk && needs_more_args?(args)
|
90
|
+
args << nil
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
def needs_more_args?(args)
|
95
|
+
is_splat = splat_args_contract_index
|
96
|
+
more_args_expected = args.size < args_contracts.size
|
97
|
+
(is_splat || more_args_expected)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Explicitly append options={} if Hash contract is present
|
101
|
+
# Same thing for when we have named params but didn't pass any in.
|
102
|
+
# returns true if it appended {}
|
103
|
+
def maybe_append_options!(args, blk)
|
104
|
+
return unless @has_options_contract
|
105
|
+
return args.insert(-2, {}) if use_penultimate_contract?(args)
|
106
|
+
return args.insert(-1, {}) if use_last_contract?(args)
|
107
|
+
end
|
108
|
+
|
109
|
+
def use_last_contract?(args)
|
110
|
+
return if args[-1].is_a?(Hash)
|
111
|
+
kinda_hash?(args_contracts[-1])
|
112
|
+
end
|
113
|
+
|
114
|
+
def use_penultimate_contract?(args)
|
115
|
+
return if args[-2].is_a?(Hash)
|
116
|
+
kinda_hash?(args_contracts[-2])
|
117
|
+
end
|
118
|
+
end # end CallWith
|
119
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Contracts
|
2
|
+
module FailureCallback
|
3
|
+
# Default implementation of failure_callback. Provided as a block to be able
|
4
|
+
# to monkey patch #failure_callback only temporary and then switch it back.
|
5
|
+
# First important usage - for specs.
|
6
|
+
DEFAULT_FAILURE_CALLBACK = proc do |data|
|
7
|
+
msg = Contracts::ErrorFormatters.failure_msg(data)
|
8
|
+
|
9
|
+
# this failed on the return contract
|
10
|
+
raise ReturnContractError.new(msg, data) if data[:return_value]
|
11
|
+
|
12
|
+
# this failed for a param contract
|
13
|
+
raise data[:contracts].failure_exception.new(msg, data)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Callback for when a contract fails. By default it raises
|
17
|
+
# an error and prints detailed info about the contract that
|
18
|
+
# failed. You can also monkeypatch this callback to do whatever
|
19
|
+
# you want...log the error, send you an email, print an error
|
20
|
+
# message, etc.
|
21
|
+
#
|
22
|
+
# Example of monkeypatching:
|
23
|
+
#
|
24
|
+
# def Contract.failure_callback(data)
|
25
|
+
# puts "You had an error!"
|
26
|
+
# puts failure_msg(data)
|
27
|
+
# exit
|
28
|
+
# end
|
29
|
+
def failure_callback(data, use_pattern_matching = true)
|
30
|
+
if data[:contracts].pattern_match? && use_pattern_matching
|
31
|
+
return DEFAULT_FAILURE_CALLBACK.call(data)
|
32
|
+
end
|
33
|
+
|
34
|
+
fetch_failure_callback.call(data)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Used to override failure_callback without monkeypatching.
|
38
|
+
#
|
39
|
+
# Takes: block parameter, that should accept one argument - data.
|
40
|
+
#
|
41
|
+
# Example usage:
|
42
|
+
#
|
43
|
+
# Contract.override_failure_callback do |data|
|
44
|
+
# puts "You had an error"
|
45
|
+
# puts failure_msg(data)
|
46
|
+
# exit
|
47
|
+
# end
|
48
|
+
def override_failure_callback(&blk)
|
49
|
+
@failure_callback = blk
|
50
|
+
end
|
51
|
+
|
52
|
+
# Used to restore default failure callback
|
53
|
+
def restore_failure_callback
|
54
|
+
@failure_callback = DEFAULT_FAILURE_CALLBACK
|
55
|
+
end
|
56
|
+
|
57
|
+
def fetch_failure_callback
|
58
|
+
@failure_callback ||= DEFAULT_FAILURE_CALLBACK
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -37,7 +37,7 @@ module Contracts
|
|
37
37
|
end
|
38
38
|
end,
|
39
39
|
|
40
|
-
Contracts::
|
40
|
+
Contracts::SplatArgs => lambda do |contract|
|
41
41
|
lambda do |arg|
|
42
42
|
Contract.valid?(arg, contract.contract)
|
43
43
|
end
|
@@ -84,20 +84,15 @@ module Contracts
|
|
84
84
|
# don't have to go through this decision tree every time.
|
85
85
|
# Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
|
86
86
|
def make_validator!(contract)
|
87
|
+
validator_strategies[validator_key(contract)].call(contract)
|
88
|
+
end
|
89
|
+
|
90
|
+
def validator_key(contract)
|
87
91
|
klass = contract.class
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
:valid
|
93
|
-
elsif klass == Class || klass == Module
|
94
|
-
:class
|
95
|
-
else
|
96
|
-
:default
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
validator_strategies[key].call(contract)
|
92
|
+
return klass if validator_strategies.key?(klass)
|
93
|
+
return :valid if contract.respond_to? :valid?
|
94
|
+
return :class if klass == Class || klass == Module
|
95
|
+
:default
|
101
96
|
end
|
102
97
|
|
103
98
|
def make_validator(contract)
|
data/lib/contracts/core.rb
CHANGED
@@ -14,37 +14,16 @@ module Contracts
|
|
14
14
|
base.instance_eval do
|
15
15
|
def functype(funcname)
|
16
16
|
contracts = Engine.fetch_from(self).decorated_methods_for(:class_methods, funcname)
|
17
|
-
if contracts.nil?
|
18
|
-
|
19
|
-
else
|
20
|
-
"#{funcname} :: #{contracts[0]}"
|
21
|
-
end
|
17
|
+
return "No contract for #{self}.#{funcname}" if contracts.nil?
|
18
|
+
"#{funcname} :: #{contracts[0]}"
|
22
19
|
end
|
23
20
|
end
|
24
21
|
|
25
|
-
# NOTE: Workaround for `defined?(super)` bug in ruby 1.9.2
|
26
|
-
# source: http://stackoverflow.com/a/11181685
|
27
|
-
# bug: https://bugs.ruby-lang.org/issues/6644
|
28
|
-
base.class_eval <<-RUBY
|
29
|
-
# TODO: deprecate
|
30
|
-
# Required when contracts are included in global scope
|
31
|
-
def Contract(*args)
|
32
|
-
if defined?(super)
|
33
|
-
super
|
34
|
-
else
|
35
|
-
self.class.Contract(*args)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
RUBY
|
39
|
-
|
40
22
|
base.class_eval do
|
41
23
|
def functype(funcname)
|
42
24
|
contracts = Engine.fetch_from(self.class).decorated_methods_for(:instance_methods, funcname)
|
43
|
-
if contracts.nil?
|
44
|
-
|
45
|
-
else
|
46
|
-
"#{funcname} :: #{contracts[0]}"
|
47
|
-
end
|
25
|
+
return "No contract for #{self.class}.#{funcname}" if contracts.nil?
|
26
|
+
"#{funcname} :: #{contracts[0]}"
|
48
27
|
end
|
49
28
|
end
|
50
29
|
end
|
data/lib/contracts/decorators.rb
CHANGED
@@ -25,7 +25,7 @@ module Contracts
|
|
25
25
|
class << self; attr_accessor :decorators; end
|
26
26
|
|
27
27
|
def self.inherited(klass)
|
28
|
-
name = klass.name.gsub(
|
28
|
+
name = klass.name.gsub(/^./, &:downcase)
|
29
29
|
|
30
30
|
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
31
31
|
|
@@ -88,12 +88,12 @@ module Contracts
|
|
88
88
|
#
|
89
89
|
# @return [Engine::Base or Engine::Eigenclass]
|
90
90
|
def nearest_decorated_ancestor
|
91
|
-
current
|
91
|
+
current = klass
|
92
92
|
current_engine = self
|
93
|
-
ancestors
|
93
|
+
ancestors = current.ancestors[1..-1]
|
94
94
|
|
95
95
|
while current && current_engine && !current_engine.decorated_methods?
|
96
|
-
current
|
96
|
+
current = ancestors.shift
|
97
97
|
current_engine = Engine.fetch_from(current)
|
98
98
|
end
|
99
99
|
|
@@ -109,8 +109,7 @@ module Contracts
|
|
109
109
|
end
|
110
110
|
|
111
111
|
# No-op because it is safe to add decorators to normal classes
|
112
|
-
def validate
|
113
|
-
end
|
112
|
+
def validate!; end
|
114
113
|
|
115
114
|
def pop_decorators
|
116
115
|
decorators.tap { clear_decorators }
|
@@ -20,15 +20,14 @@ module Contracts
|
|
20
20
|
Target.new(eigenclass).apply(Eigenclass)
|
21
21
|
eigenclass.extend(MethodDecorators)
|
22
22
|
# FIXME; this should detect what user uses `include Contracts` or
|
23
|
-
# `include Contracts
|
23
|
+
# `include Contracts::Core`
|
24
24
|
eigenclass.send(:include, Contracts)
|
25
25
|
Engine.fetch_from(owner).set_eigenclass_owner
|
26
26
|
Engine.fetch_from(eigenclass)
|
27
27
|
end
|
28
28
|
|
29
29
|
# No-op for eigenclasses
|
30
|
-
def set_eigenclass_owner
|
31
|
-
end
|
30
|
+
def set_eigenclass_owner; end
|
32
31
|
|
33
32
|
# Fetches just eigenclasses decorators
|
34
33
|
def all_decorators
|
@@ -39,7 +38,7 @@ module Contracts
|
|
39
38
|
|
40
39
|
# Fails when contracts are not included in owner class
|
41
40
|
def validate!
|
42
|
-
|
41
|
+
raise ContractsNotIncluded unless owner?
|
43
42
|
end
|
44
43
|
|
45
44
|
def owner?
|