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.
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
@@ -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::Args => lambda do |contract|
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
- key = if validator_strategies.key?(klass)
89
- klass
90
- else
91
- if contract.respond_to? :valid?
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)
@@ -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
- "No contract for #{self}.#{funcname}"
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
- "No contract for #{self.class}.#{funcname}"
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
@@ -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(/^./) { |m| m.downcase }
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 = klass
91
+ current = klass
92
92
  current_engine = self
93
- ancestors = current.ancestors[1..-1]
93
+ ancestors = current.ancestors[1..-1]
94
94
 
95
95
  while current && current_engine && !current_engine.decorated_methods?
96
- current = ancestors.shift
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;;Core`
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
- fail ContractsNotIncluded unless owner?
41
+ raise ContractsNotIncluded unless owner?
43
42
  end
44
43
 
45
44
  def owner?