contracts 0.9 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +5 -2
- data/TUTORIAL.md +136 -21
- data/benchmarks/hash.rb +69 -0
- data/lib/contracts.rb +20 -156
- data/lib/contracts/builtin_contracts.rb +100 -3
- data/lib/contracts/call_with.rb +96 -0
- data/lib/contracts/decorators.rb +4 -194
- data/lib/contracts/engine.rb +26 -0
- data/lib/contracts/engine/base.rb +136 -0
- data/lib/contracts/engine/eigenclass.rb +46 -0
- data/lib/contracts/engine/target.rb +68 -0
- data/lib/contracts/method_handler.rb +195 -0
- data/lib/contracts/support.rb +45 -30
- data/lib/contracts/validators.rb +127 -0
- data/lib/contracts/version.rb +1 -1
- data/spec/builtin_contracts_spec.rb +78 -2
- data/spec/contracts_spec.rb +64 -1
- data/spec/fixtures/fixtures.rb +77 -5
- data/spec/override_validators_spec.rb +162 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +10 -2
- metadata +12 -6
- data/lib/contracts/eigenclass.rb +0 -38
- data/lib/contracts/modules.rb +0 -17
@@ -0,0 +1,26 @@
|
|
1
|
+
require "contracts/engine/base"
|
2
|
+
require "contracts/engine/target"
|
3
|
+
require "contracts/engine/eigenclass"
|
4
|
+
|
5
|
+
require "forwardable"
|
6
|
+
|
7
|
+
module Contracts
|
8
|
+
# Engine facade, normally you shouldn't refer internals of Engine
|
9
|
+
# module directly.
|
10
|
+
module Engine
|
11
|
+
class << self
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
# .apply(klass) - enables contracts engine on klass
|
15
|
+
# .applied?(klass) - returns true if klass has contracts engine
|
16
|
+
# .fetch_from(klass) - returns contracts engine for klass
|
17
|
+
def_delegators :base_engine, :apply, :applied?, :fetch_from
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def base_engine
|
22
|
+
Base
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Engine
|
3
|
+
# Contracts engine
|
4
|
+
class Base
|
5
|
+
# Enable contracts engine for klass
|
6
|
+
#
|
7
|
+
# @param [Class] klass - target class
|
8
|
+
def self.apply(klass)
|
9
|
+
Engine::Target.new(klass).apply
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns true if klass has contracts engine
|
13
|
+
#
|
14
|
+
# @param [Class] klass - target class
|
15
|
+
# @return [Bool]
|
16
|
+
def self.applied?(klass)
|
17
|
+
Engine::Target.new(klass).applied?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Fetches contracts engine out of klass
|
21
|
+
#
|
22
|
+
# @param [Class] klass - target class
|
23
|
+
# @return [Engine::Base or Engine::Eigenclass]
|
24
|
+
def self.fetch_from(klass)
|
25
|
+
Engine::Target.new(klass).engine
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates new instance of contracts engine
|
29
|
+
#
|
30
|
+
# @param [Class] klass - class that owns this engine
|
31
|
+
def initialize(klass)
|
32
|
+
@klass = klass
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds provided decorator to the engine
|
36
|
+
# It validates that decorator can be added to this engine at the
|
37
|
+
# moment
|
38
|
+
#
|
39
|
+
# @param [Decorator:Class] decorator_class
|
40
|
+
# @param args - arguments for decorator
|
41
|
+
def decorate(decorator_class, *args)
|
42
|
+
validate!
|
43
|
+
decorators << [decorator_class, args]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets eigenclass' owner to klass
|
47
|
+
def set_eigenclass_owner
|
48
|
+
eigenclass_engine.owner_class = klass
|
49
|
+
end
|
50
|
+
|
51
|
+
# Fetches all accumulated decorators (both this engine and
|
52
|
+
# corresponding eigenclass' engine)
|
53
|
+
# It clears all accumulated decorators
|
54
|
+
#
|
55
|
+
# @return [ArrayOf[Decorator]]
|
56
|
+
def all_decorators
|
57
|
+
pop_decorators + eigenclass_engine.all_decorators
|
58
|
+
end
|
59
|
+
|
60
|
+
# Fetches decorators of specified type for method with name
|
61
|
+
#
|
62
|
+
# @param [Or[:class_methods, :instance_methods]] type - method type
|
63
|
+
# @param [Symbol] name - method name
|
64
|
+
# @return [ArrayOf[Decorator]]
|
65
|
+
def decorated_methods_for(type, name)
|
66
|
+
Array(decorated_methods[type][name])
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns true if there are any decorated methods
|
70
|
+
#
|
71
|
+
# @return [Bool]
|
72
|
+
def decorated_methods?
|
73
|
+
!decorated_methods[:class_methods].empty? ||
|
74
|
+
!decorated_methods[:instance_methods].empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Adds method decorator
|
78
|
+
#
|
79
|
+
# @param [Or[:class_methods, :instance_methods]] type - method type
|
80
|
+
# @param [Symbol] name - method name
|
81
|
+
# @param [Decorator] decorator - method decorator
|
82
|
+
def add_method_decorator(type, name, decorator)
|
83
|
+
decorated_methods[type][name] ||= []
|
84
|
+
decorated_methods[type][name] << decorator
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns nearest ancestor's engine that has decorated methods
|
88
|
+
#
|
89
|
+
# @return [Engine::Base or Engine::Eigenclass]
|
90
|
+
def nearest_decorated_ancestor
|
91
|
+
current = klass
|
92
|
+
current_engine = self
|
93
|
+
ancestors = current.ancestors[1..-1]
|
94
|
+
|
95
|
+
while current && current_engine && !current_engine.decorated_methods?
|
96
|
+
current = ancestors.shift
|
97
|
+
current_engine = Engine.fetch_from(current)
|
98
|
+
end
|
99
|
+
|
100
|
+
current_engine
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
attr_reader :klass
|
106
|
+
|
107
|
+
def decorated_methods
|
108
|
+
@_decorated_methods ||= { :class_methods => {}, :instance_methods => {} }
|
109
|
+
end
|
110
|
+
|
111
|
+
# No-op because it is safe to add decorators to normal classes
|
112
|
+
def validate!
|
113
|
+
end
|
114
|
+
|
115
|
+
def pop_decorators
|
116
|
+
decorators.tap { clear_decorators }
|
117
|
+
end
|
118
|
+
|
119
|
+
def eigenclass
|
120
|
+
Support.eigenclass_of(klass)
|
121
|
+
end
|
122
|
+
|
123
|
+
def eigenclass_engine
|
124
|
+
Eigenclass.lift(eigenclass, klass)
|
125
|
+
end
|
126
|
+
|
127
|
+
def decorators
|
128
|
+
@_decorators ||= []
|
129
|
+
end
|
130
|
+
|
131
|
+
def clear_decorators
|
132
|
+
@_decorators = []
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Engine
|
3
|
+
# Special case of contracts engine for eigenclasses
|
4
|
+
# We don't care about eigenclass of eigenclass at this point
|
5
|
+
class Eigenclass < Base
|
6
|
+
# Class that owns this eigenclass
|
7
|
+
attr_accessor :owner_class
|
8
|
+
|
9
|
+
# Automatically enables eigenclass engine if it is not
|
10
|
+
# Returns its engine
|
11
|
+
# NOTE: Required by jruby in 1.9 mode. Otherwise inherited
|
12
|
+
# eigenclasses don't have their engines
|
13
|
+
#
|
14
|
+
# @param [Class] eigenclass - class in question
|
15
|
+
# @param [Class] owner - owner of eigenclass
|
16
|
+
# @return [Engine::Eigenclass]
|
17
|
+
def self.lift(eigenclass, owner)
|
18
|
+
return Engine.fetch_from(eigenclass) if Engine.applied?(eigenclass)
|
19
|
+
|
20
|
+
Target.new(eigenclass).apply(Eigenclass)
|
21
|
+
Engine.fetch_from(owner).set_eigenclass_owner
|
22
|
+
Engine.fetch_from(eigenclass)
|
23
|
+
end
|
24
|
+
|
25
|
+
# No-op for eigenclasses
|
26
|
+
def set_eigenclass_owner
|
27
|
+
end
|
28
|
+
|
29
|
+
# Fetches just eigenclasses decorators
|
30
|
+
def all_decorators
|
31
|
+
pop_decorators
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Fails when contracts are not included in owner class
|
37
|
+
def validate!
|
38
|
+
fail ContractsNotIncluded unless owner?
|
39
|
+
end
|
40
|
+
|
41
|
+
def owner?
|
42
|
+
!!owner_class
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Engine
|
3
|
+
# Represents class in question
|
4
|
+
class Target
|
5
|
+
# Creates new instance of Target
|
6
|
+
#
|
7
|
+
# @param [Class] target - class in question
|
8
|
+
def initialize(target)
|
9
|
+
@target = target
|
10
|
+
end
|
11
|
+
|
12
|
+
# Enable contracts engine for target
|
13
|
+
# - it is no-op if contracts engine is already enabled
|
14
|
+
# - it automatically enables contracts engine for its eigenclass
|
15
|
+
# - it sets owner class to target for its eigenclass
|
16
|
+
#
|
17
|
+
# @param [Engine::Base:Class] engine_class - type of engine to
|
18
|
+
# enable (Base or Eigenclass)
|
19
|
+
def apply(engine_class = Base)
|
20
|
+
return if applied?
|
21
|
+
|
22
|
+
apply_to_eigenclass
|
23
|
+
|
24
|
+
eigenclass.class_eval do
|
25
|
+
define_method(:__contracts_engine) do
|
26
|
+
@__contracts_engine ||= engine_class.new(self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
engine.set_eigenclass_owner
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns true if target has contracts engine already
|
34
|
+
#
|
35
|
+
# @return [Bool]
|
36
|
+
def applied?
|
37
|
+
target.respond_to?(:__contracts_engine)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns contracts engine of target
|
41
|
+
#
|
42
|
+
# @return [Engine::Base or Engine::Eigenclass]
|
43
|
+
def engine
|
44
|
+
applied? && target.__contracts_engine
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :target
|
50
|
+
|
51
|
+
def apply_to_eigenclass
|
52
|
+
return unless meaningless_eigenclass?
|
53
|
+
|
54
|
+
self.class.new(eigenclass).apply(Eigenclass)
|
55
|
+
eigenclass.extend(MethodDecorators)
|
56
|
+
eigenclass.send(:include, Contracts)
|
57
|
+
end
|
58
|
+
|
59
|
+
def eigenclass
|
60
|
+
Support.eigenclass_of(target)
|
61
|
+
end
|
62
|
+
|
63
|
+
def meaningless_eigenclass?
|
64
|
+
!Support.eigenclass?(target)
|
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
|