contracts 0.9 → 0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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