contracts-lite 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.markdown +80 -0
  3. data/Gemfile +16 -0
  4. data/LICENSE +23 -0
  5. data/README.md +102 -0
  6. data/TODO.markdown +6 -0
  7. data/TUTORIAL.md +747 -0
  8. data/benchmarks/bench.rb +67 -0
  9. data/benchmarks/hash.rb +69 -0
  10. data/benchmarks/invariants.rb +91 -0
  11. data/benchmarks/io.rb +62 -0
  12. data/benchmarks/wrap_test.rb +57 -0
  13. data/contracts.gemspec +13 -0
  14. data/lib/contracts.rb +231 -0
  15. data/lib/contracts/builtin_contracts.rb +541 -0
  16. data/lib/contracts/call_with.rb +97 -0
  17. data/lib/contracts/core.rb +52 -0
  18. data/lib/contracts/decorators.rb +47 -0
  19. data/lib/contracts/engine.rb +26 -0
  20. data/lib/contracts/engine/base.rb +136 -0
  21. data/lib/contracts/engine/eigenclass.rb +50 -0
  22. data/lib/contracts/engine/target.rb +70 -0
  23. data/lib/contracts/error_formatter.rb +121 -0
  24. data/lib/contracts/errors.rb +71 -0
  25. data/lib/contracts/formatters.rb +134 -0
  26. data/lib/contracts/invariants.rb +68 -0
  27. data/lib/contracts/method_handler.rb +195 -0
  28. data/lib/contracts/method_reference.rb +100 -0
  29. data/lib/contracts/support.rb +59 -0
  30. data/lib/contracts/validators.rb +139 -0
  31. data/lib/contracts/version.rb +3 -0
  32. data/script/rubocop +7 -0
  33. data/spec/builtin_contracts_spec.rb +461 -0
  34. data/spec/contracts_spec.rb +748 -0
  35. data/spec/error_formatter_spec.rb +68 -0
  36. data/spec/fixtures/fixtures.rb +710 -0
  37. data/spec/invariants_spec.rb +17 -0
  38. data/spec/module_spec.rb +18 -0
  39. data/spec/override_validators_spec.rb +162 -0
  40. data/spec/ruby_version_specific/contracts_spec_1.9.rb +24 -0
  41. data/spec/ruby_version_specific/contracts_spec_2.0.rb +55 -0
  42. data/spec/ruby_version_specific/contracts_spec_2.1.rb +63 -0
  43. data/spec/spec_helper.rb +102 -0
  44. data/spec/support.rb +10 -0
  45. data/spec/support_spec.rb +21 -0
  46. data/spec/validators_spec.rb +47 -0
  47. metadata +94 -0
@@ -0,0 +1,121 @@
1
+ module Contracts
2
+ class ErrorFormatters
3
+ def self.failure_msg(data)
4
+ class_for(data).new(data).message
5
+ end
6
+
7
+ def self.class_for(data)
8
+ return Contracts::KeywordArgsErrorFormatter if keyword_args?(data)
9
+ DefaultErrorFormatter
10
+ end
11
+
12
+ def self.keyword_args?(data)
13
+ data[:contract].is_a?(Contracts::Builtin::KeywordArgs) && data[:arg].is_a?(Hash)
14
+ end
15
+ end
16
+
17
+ class DefaultErrorFormatter
18
+ attr_accessor :data
19
+ def initialize(data)
20
+ @data = data
21
+ end
22
+
23
+ def message
24
+ %{#{header}
25
+ Expected: #{expected},
26
+ Actual: #{data[:arg].inspect}
27
+ Value guarded in: #{data[:class]}::#{method_name}
28
+ With Contract: #{data[:contracts]}
29
+ At: #{position} }
30
+ end
31
+
32
+ private
33
+
34
+ def header
35
+ if data[:return_value]
36
+ "Contract violation for return value:"
37
+ else
38
+ "Contract violation for argument #{data[:arg_pos]} of #{data[:total_args]}:"
39
+ end
40
+ end
41
+
42
+ def expected
43
+ Contracts::Formatters::Expected.new(data[:contract]).contract
44
+ end
45
+
46
+ def position
47
+ Contracts::Support.method_position(data[:method])
48
+ end
49
+
50
+ def method_name
51
+ Contracts::Support.method_name(data[:method])
52
+ end
53
+ end
54
+
55
+ class KeywordArgsErrorFormatter < DefaultErrorFormatter
56
+ def message
57
+ s = []
58
+ s << "#{header}"
59
+ s << " Expected: #{expected}"
60
+ s << " Actual: #{data[:arg].inspect}"
61
+ s << " Missing Contract: #{missing_contract_info}" unless missing_contract_info.empty?
62
+ s << " Invalid Args: #{invalid_args_info}" unless invalid_args_info.empty?
63
+ s << " Missing Args: #{missing_args_info}" unless missing_args_info.empty?
64
+ s << " Value guarded in: #{data[:class]}::#{method_name}"
65
+ s << " With Contract: #{data[:contracts]}"
66
+ s << " At: #{position} "
67
+
68
+ s.join("\n")
69
+ end
70
+
71
+ private
72
+
73
+ def missing_args_info
74
+ @missing_args_info ||= begin
75
+ missing_keys = contract_options.keys - arg.keys
76
+ contract_options.select do |key, _|
77
+ missing_keys.include?(key)
78
+ end
79
+ end
80
+ end
81
+
82
+ def missing_contract_info
83
+ @missing_contract_info ||= begin
84
+ contract_keys = contract_options.keys
85
+ arg.select { |key, _| !contract_keys.include?(key) }
86
+ end
87
+ end
88
+
89
+ def invalid_args_info
90
+ @invalid_args_info ||= begin
91
+ invalid_keys = []
92
+ arg.each do |key, value|
93
+ contract = contract_options[key]
94
+ next unless contract
95
+ invalid_keys.push(key) unless check_contract(contract, value)
96
+ end
97
+ invalid_keys.map do |key|
98
+ {key => arg[key], :contract => contract_options[key] }
99
+ end
100
+ end
101
+ end
102
+
103
+ def check_contract(contract, value)
104
+ if contract.respond_to?(:valid?)
105
+ contract.valid?(value)
106
+ else
107
+ value.is_a?(contract)
108
+ end
109
+ rescue
110
+ false
111
+ end
112
+
113
+ def contract_options
114
+ @contract_options ||= data[:contract].send(:options)
115
+ end
116
+
117
+ def arg
118
+ data[:arg]
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,71 @@
1
+ # @private
2
+ # Base class for Contract errors
3
+ #
4
+ # If default failure callback is used it stores failure data
5
+ class ContractBaseError < ArgumentError
6
+ attr_reader :data
7
+
8
+ def initialize(message, data)
9
+ super(message)
10
+ @data = data
11
+ end
12
+
13
+ # Used to convert to simple ContractError from other contract errors
14
+ def to_contract_error
15
+ self
16
+ end
17
+ end
18
+
19
+ # Default contract error
20
+ #
21
+ # If default failure callback is used, users normally see only these contract errors
22
+ class ContractError < ContractBaseError
23
+ end
24
+
25
+ class ParamContractError < ContractError
26
+ end
27
+
28
+ class ReturnContractError < ContractError
29
+ end
30
+
31
+ # @private
32
+ # Special contract error used internally to detect pattern failure during pattern matching
33
+ class PatternMatchingError < ContractBaseError
34
+ # Used to convert to ContractError from PatternMatchingError
35
+ def to_contract_error
36
+ ContractError.new(to_s, data)
37
+ end
38
+ end
39
+
40
+ # Base invariant violation error
41
+ class InvariantError < StandardError
42
+ def to_contract_error
43
+ self
44
+ end
45
+ end
46
+
47
+ module Contracts
48
+ # Error issued when user haven't included Contracts in original class but used Contract definition in singleton class
49
+ #
50
+ # Provides useful description for user of the gem and an example of correct usage.
51
+ class ContractsNotIncluded < TypeError
52
+ DEFAULT_MESSAGE = %{In order to use contracts in singleton class, please include Contracts module in original class
53
+ Example:
54
+
55
+ ```ruby
56
+ class Example
57
+ include Contracts # this line is required
58
+ class << self
59
+ # you can use `Contract` definition here now
60
+ end
61
+ end
62
+ ```}
63
+
64
+ attr_reader :message
65
+ alias_method :to_s, :message
66
+
67
+ def initialize(message = DEFAULT_MESSAGE)
68
+ @message = message
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,134 @@
1
+ module Contracts
2
+ # A namespace for classes related to formatting.
3
+ module Formatters
4
+ # Used to format contracts for the `Expected:` field of error output.
5
+ class Expected
6
+ # @param full [Boolean] if false only unique `to_s` values will be output,
7
+ # non unique values become empty string.
8
+ def initialize(contract, full = true)
9
+ @contract, @full = contract, full
10
+ end
11
+
12
+ # Formats any type of Contract.
13
+ def contract(contract = @contract)
14
+ if contract.is_a?(Hash)
15
+ hash_contract(contract)
16
+ elsif contract.is_a?(Array)
17
+ array_contract(contract)
18
+ else
19
+ InspectWrapper.create(contract, @full)
20
+ end
21
+ end
22
+
23
+ # Formats Hash contracts.
24
+ def hash_contract(hash)
25
+ @full = true # Complex values output completely, overriding @full
26
+ hash.inject({}) do |repr, (k, v)|
27
+ repr.merge(k => InspectWrapper.create(contract(v), @full))
28
+ end.inspect
29
+ end
30
+
31
+ # Formats Array contracts.
32
+ def array_contract(array)
33
+ @full = true
34
+ array.map { |v| InspectWrapper.create(contract(v), @full) }.inspect
35
+ end
36
+ end
37
+
38
+ # A wrapper class to produce correct inspect behaviour for different
39
+ # contract values - constants, Class contracts, instance contracts etc.
40
+ module InspectWrapper
41
+ # InspectWrapper is a factory, will never be an instance
42
+ # @return [ClassInspectWrapper, ObjectInspectWrapper]
43
+ def self.create(value, full = true)
44
+ if value.class == Class
45
+ ClassInspectWrapper
46
+ else
47
+ ObjectInspectWrapper
48
+ end.new(value, full)
49
+ end
50
+
51
+ # @param full [Boolean] if false only unique `to_s` values will be output,
52
+ # non unique values become empty string.
53
+ def initialize(value, full)
54
+ @value, @full = value, full
55
+ end
56
+
57
+ # Inspect different types of contract values.
58
+ # Contracts module prefix will be removed from classes.
59
+ # Custom to_s messages will be wrapped in round brackets to differentiate
60
+ # from standard Strings.
61
+ # Primitive values e.g. 42, true, nil will be left alone.
62
+ def inspect
63
+ return "" unless full?
64
+ return @value.inspect if empty_val?
65
+ return @value.to_s if plain?
66
+ return delim(@value.to_s) if useful_to_s?
67
+ useful_inspect
68
+ end
69
+
70
+ def delim(value)
71
+ @full ? "(#{value})" : "#{value}"
72
+ end
73
+
74
+ # Eliminates eronious quotes in output that plain inspect includes.
75
+ def to_s
76
+ inspect
77
+ end
78
+
79
+ private
80
+
81
+ def empty_val?
82
+ @value.nil? || @value == ""
83
+ end
84
+
85
+ def full?
86
+ @full ||
87
+ @value.is_a?(Hash) || @value.is_a?(Array) ||
88
+ (!plain? && useful_to_s?)
89
+ end
90
+
91
+ def plain?
92
+ # Not a type of contract that can have a custom to_s defined
93
+ !@value.is_a?(Builtin::CallableClass) && @value.class != Class
94
+ end
95
+
96
+ def useful_to_s?
97
+ # Useless to_s value or no custom to_s behavious defined
98
+ !empty_to_s? && custom_to_s?
99
+ end
100
+
101
+ def empty_to_s?
102
+ @value.to_s.empty?
103
+ end
104
+
105
+ def strip_prefix(val)
106
+ val.gsub(/^Contracts::Builtin::/, "")
107
+ end
108
+ end
109
+
110
+ class ClassInspectWrapper
111
+ include InspectWrapper
112
+
113
+ def custom_to_s?
114
+ @value.to_s != @value.name
115
+ end
116
+
117
+ def useful_inspect
118
+ strip_prefix(empty_to_s? ? @value.name : @value.inspect)
119
+ end
120
+ end
121
+
122
+ class ObjectInspectWrapper
123
+ include InspectWrapper
124
+
125
+ def custom_to_s?
126
+ !@value.to_s.match(/#\<\w+:.+\>/)
127
+ end
128
+
129
+ def useful_inspect
130
+ strip_prefix(empty_to_s? ? @value.class.name : @value.inspect)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,68 @@
1
+ module Contracts
2
+ module Invariants
3
+ def self.included(base)
4
+ common base
5
+ end
6
+
7
+ def self.extended(base)
8
+ common base
9
+ end
10
+
11
+ def self.common(base)
12
+ return if base.respond_to?(:Invariant)
13
+
14
+ base.extend(InvariantExtension)
15
+ end
16
+
17
+ def verify_invariants!(method)
18
+ return unless self.class.respond_to?(:invariants)
19
+
20
+ self.class.invariants.each do |invariant|
21
+ invariant.check_on(self, method)
22
+ end
23
+ end
24
+
25
+ module InvariantExtension
26
+ def invariant(name, &condition)
27
+ return if ENV["NO_CONTRACTS"]
28
+
29
+ invariants << Invariant.new(self, name, &condition)
30
+ end
31
+
32
+ def invariants
33
+ @invariants ||= []
34
+ end
35
+ end
36
+
37
+ class Invariant
38
+ def initialize(klass, name, &condition)
39
+ @klass, @name, @condition = klass, name, condition
40
+ end
41
+
42
+ def expected
43
+ "#{@name} condition to be true"
44
+ end
45
+
46
+ def check_on(target, method)
47
+ return if target.instance_eval(&@condition)
48
+
49
+ self.class.failure_callback(:expected => expected,
50
+ :actual => false,
51
+ :target => target,
52
+ :method => method)
53
+ end
54
+
55
+ def self.failure_callback(data)
56
+ fail InvariantError, failure_msg(data)
57
+ end
58
+
59
+ def self.failure_msg(data)
60
+ %{Invariant violation:
61
+ Expected: #{data[:expected]}
62
+ Actual: #{data[:actual]}
63
+ Value guarded in: #{data[:target].class}::#{Support.method_name(data[:method])}
64
+ At: #{Support.method_position(data[:method])}}
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,195 @@
1
+ module Contracts
2
+ # Handles class and instance methods addition
3
+ # Represents single such method
4
+ class MethodHandler
5
+ METHOD_REFERENCE_FACTORY = {
6
+ :class_methods => SingletonMethodReference,
7
+ :instance_methods => MethodReference
8
+ }
9
+
10
+ RAW_METHOD_STRATEGY = {
11
+ :class_methods => lambda { |target, name| target.method(name) },
12
+ :instance_methods => lambda { |target, name| target.instance_method(name) }
13
+ }
14
+
15
+ # Creates new instance of MethodHandler
16
+ #
17
+ # @param [Symbol] method_name
18
+ # @param [Bool] is_class_method
19
+ # @param [Class] target - class that method got added to
20
+ def initialize(method_name, is_class_method, target)
21
+ @method_name = method_name
22
+ @is_class_method = is_class_method
23
+ @target = target
24
+ end
25
+
26
+ # Handles method addition
27
+ def handle
28
+ return unless engine?
29
+ return if decorators.empty?
30
+
31
+ validate_decorators!
32
+ validate_pattern_matching!
33
+
34
+ engine.add_method_decorator(method_type, method_name, decorator)
35
+ mark_pattern_matching_decorators
36
+ method_reference.make_alias(target)
37
+ redefine_method
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :method_name, :is_class_method, :target
43
+
44
+ def engine?
45
+ Engine.applied?(target)
46
+ end
47
+
48
+ def engine
49
+ Engine.fetch_from(target)
50
+ end
51
+
52
+ def decorators
53
+ @_decorators ||= engine.all_decorators
54
+ end
55
+
56
+ def method_type
57
+ @_method_type ||= is_class_method ? :class_methods : :instance_methods
58
+ end
59
+ # _method_type is required for assigning it to local variable with
60
+ # the same name. See: #redefine_method
61
+ alias_method :_method_type, :method_type
62
+
63
+ def method_reference
64
+ @_method_reference ||= METHOD_REFERENCE_FACTORY[method_type].new(method_name, raw_method)
65
+ end
66
+
67
+ def raw_method
68
+ RAW_METHOD_STRATEGY[method_type].call(target, method_name)
69
+ end
70
+
71
+ def ignore_decorators?
72
+ ENV["NO_CONTRACTS"] && !pattern_matching?
73
+ end
74
+
75
+ def decorated_methods
76
+ @_decorated_methods ||= engine.decorated_methods_for(method_type, method_name)
77
+ end
78
+
79
+ def pattern_matching?
80
+ return @_pattern_matching if defined?(@_pattern_matching)
81
+ @_pattern_matching = decorated_methods.any? { |x| x.method != method_reference }
82
+ end
83
+
84
+ def mark_pattern_matching_decorators
85
+ return unless pattern_matching?
86
+ decorated_methods.each(&:pattern_match!)
87
+ end
88
+
89
+ def decorator
90
+ @_decorator ||= decorator_class.new(target, method_reference, *decorator_args)
91
+ end
92
+
93
+ def decorator_class
94
+ decorators.first[0]
95
+ end
96
+
97
+ def decorator_args
98
+ decorators.first[1]
99
+ end
100
+
101
+ def redefine_method
102
+ return if ignore_decorators?
103
+
104
+ # Those are required for instance_eval to be able to refer them
105
+ name = method_name
106
+ method_type = _method_type
107
+ current_engine = engine
108
+
109
+ # We are gonna redefine original method here
110
+ method_reference.make_definition(target) do |*args, &blk|
111
+ engine = current_engine.nearest_decorated_ancestor
112
+
113
+ # If we weren't able to find any ancestor that has decorated methods
114
+ # FIXME : this looks like untested code (commenting it out doesn't make specs red)
115
+ 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."
117
+ end
118
+
119
+ # Fetch decorated methods out of the contracts engine
120
+ decorated_methods = engine.decorated_methods_for(method_type, name)
121
+
122
+ # This adds support for overloading methods. Here we go
123
+ # through each method and call it with the arguments.
124
+ # If we get a failure_exception, we move to the next
125
+ # function. Otherwise we return the result.
126
+ # If we run out of functions, we raise the last error, but
127
+ # convert it to_contract_error.
128
+ success = false
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
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
149
+ end
150
+
151
+ # Return the result of successfully called method
152
+ result
153
+ end
154
+ end
155
+
156
+ def validate_decorators!
157
+ return if decorators.size == 1
158
+
159
+ fail %{
160
+ Oops, it looks like method '#{name}' has multiple contracts:
161
+ #{decorators.map { |x| x[1][0].inspect }.join("\n")}
162
+
163
+ Did you accidentally put more than one contract on a single function, like so?
164
+
165
+ Contract String => String
166
+ Contract Num => String
167
+ def foo x
168
+ end
169
+
170
+ If you did NOT, then you have probably discovered a bug in this library.
171
+ Please file it along with the relevant code at:
172
+ https://github.com/egonSchiele/contracts.ruby/issues
173
+ }
174
+ end
175
+
176
+ def validate_pattern_matching!
177
+ new_args_contract = decorator.args_contracts
178
+ matched = decorated_methods.select do |contract|
179
+ contract.args_contracts == new_args_contract
180
+ end
181
+
182
+ return if matched.empty?
183
+
184
+ fail ContractError.new(%{
185
+ It looks like you are trying to use pattern-matching, but
186
+ multiple definitions for function '#{method_name}' have the same
187
+ contract for input parameters:
188
+
189
+ #{(matched + [decorator]).map(&:to_s).join("\n")}
190
+
191
+ Each definition needs to have a different contract for the parameters.
192
+ }, {})
193
+ end
194
+ end
195
+ end