rconditions 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2007 Norman Timmler, Tammo Freese
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,5 @@
1
+ = RConditions
2
+
3
+ RConditions generates a bang method (for example, <tt>Array#empty!</tt>) for each predicate method (for example, <tt>Array#empty?</tt>). The bang method will return true if the predicate method returns true, otherwise it will throw an exception. The exception will include auto-generated modules that give information which predicate methods passed or failed in the bang method's execution.
4
+
5
+ For more information, see the roc or ri documentation of the module RConditions.
@@ -0,0 +1,234 @@
1
+ # = RConditions
2
+ #
3
+ # RConditions generates a bang method (for example, <tt>Array#empty!</tt>)
4
+ # for each predicate method (for example, <tt>Array#empty?</tt>). The bang
5
+ # method will return true if the predicate method returns true, otherwise it
6
+ # will throw an exception. The exception will include auto-generated modules
7
+ # that give information which predicate methods passed or failed in the bang
8
+ # method's execution.
9
+ #
10
+ # == Example
11
+ #
12
+ # Here is a simple example. The raised exception includes a module defined
13
+ # on the class where the method is defined:
14
+ #
15
+ # require "rconditions"
16
+ #
17
+ # begin
18
+ # [1,2].empty!
19
+ # rescue Array::NotEmptyError => e
20
+ # p e
21
+ # end
22
+ #
23
+ # # => #<Exception: Array::NotEmptyError>
24
+ #
25
+ # Here is a more complicated example:
26
+ #
27
+ # require "rconditions"
28
+ #
29
+ # class Posting
30
+ # attr_writer :spam, :active
31
+ #
32
+ # def spam?
33
+ # @spam
34
+ # end
35
+ #
36
+ # def active?
37
+ # @active
38
+ # end
39
+ #
40
+ # def visible?
41
+ # !spam? && active?
42
+ # end
43
+ # end
44
+ #
45
+ # class User
46
+ # attr_writer :admin
47
+ #
48
+ # def admin?
49
+ # @admin
50
+ # end
51
+ #
52
+ # def can_view_posting?(posting)
53
+ # admin? || posting.visible?
54
+ # end
55
+ # end
56
+ #
57
+ # user = User.new
58
+ # user.admin = false
59
+ #
60
+ # posting = Posting.new
61
+ # posting.active = false
62
+ # posting.spam = true
63
+ #
64
+ # begin
65
+ # user.can_view_posting!(posting)
66
+ # rescue Posting::SpamError => e
67
+ # p e
68
+ # rescue Posting::NotActiveError
69
+ # puts "should not get here"
70
+ # end
71
+ #
72
+ # # => #<Exception: User::NotAdminError, Posting::SpamError,
73
+ # # Posting::NotVisibleError, User::NotCanViewPostingError>
74
+ #
75
+ # As you can see, the exception includes modules for each of the evaluated
76
+ # predicate methods: <tt>User#can_view_posting!(posting)</tt> in the context
77
+ # above called <tt>User#can_view_posting?(posting)</tt>, which called
78
+ # <tt>User#admin?</tt> and <tt>Posting#can_view_posting?</tt>, which called
79
+ # <tt>Posting#spam?</tt>.
80
+ #
81
+ # The results were
82
+ #
83
+ # * <tt>User#admin? # => false</tt>
84
+ # * <tt>Posting#spam? # => true</tt>
85
+ # * <tt>Posting#can_view_posting? # => false</tt>
86
+ # * <tt>User#can_view_posting? # => false</tt>
87
+ #
88
+ # so the included exception modules are
89
+ #
90
+ # * <tt>User::NotAdminError</tt>
91
+ # * <tt>Posting::SpamError</tt>
92
+ # * <tt>Posting::NotCanViewPostingError</tt>
93
+ # * <tt>User::NotCanViewPostingError</tt>
94
+ #
95
+ # == Performance
96
+ #
97
+ # As almost all predicate methods are extended by default, RConditions carries
98
+ # a performance penalty which varies with the number of calls to predicate
99
+ # methods. A simple test setup using mongrel/rails was slowed down by 10-20%
100
+ # by adding <tt>require "rconditions"</tt> to the bottom of its
101
+ # <tt>enviroment.rb</tt>.
102
+ #
103
+ # The performance penalty can be avoided by applying RConditions only to new
104
+ # methods, that is, methods defined after <tt>require "rconditions"</tt>. To
105
+ # achieve this, set <tt>RCONDITIONS_ONLY_FOR_NEW_METHODS = true</tt> before
106
+ # the require call. Our test setup had no measurable slowdown afterwards.
107
+ #
108
+ # == Known Limitations
109
+ #
110
+ # * RConditions does only support predicate methods starting with a letter and
111
+ # containing only word characters except the question mark at the end.
112
+ # * RConditions does not support singleton methods. These includes all class
113
+ # methods.
114
+ module RConditions
115
+ class << self
116
+
117
+ # Extends the given predicate method and defines a bang method for it. You
118
+ # should never call this method directly, it is called from
119
+ # Module#method_added.
120
+ # * <tt>mod</tt> -- the module on which the method is defined.
121
+ # * <tt>id</tt> -- the name of the method.
122
+ def extend_predicate_method_and_define_bang_method(mod, id)
123
+ return if Thread.current[:rconditions_processing_predicate_method]
124
+ return if (predicate_method = id.to_s) !~ PREDICATE_METHOD
125
+
126
+ begin
127
+ Thread.current[:rconditions_processing_predicate_method] = true
128
+ extend_predicate_method(mod, predicate_method)
129
+ define_bang_method(mod, predicate_method)
130
+ ensure
131
+ Thread.current[:rconditions_processing_predicate_method] = false
132
+ end
133
+ end
134
+
135
+ def exception_modules #:nodoc:
136
+ Thread.current[:rconditions_exception_modules]
137
+ end
138
+
139
+ def exception_modules=(value) #:nodoc:
140
+ Thread.current[:rconditions_exception_modules] = value
141
+ end
142
+
143
+ private
144
+
145
+ PREDICATE_METHOD = /\A[a-zA-Z]\w*\?\z/ #:nodoc:
146
+
147
+ def extend_predicate_method(mod, predicate_method)
148
+ predicate_method_without_questionmark = predicate_method[0...-1]
149
+
150
+ positive_module = camelize("#{predicate_method_without_questionmark}_error")
151
+ mod.const_set(positive_module, Module.new) unless mod.const_defined?(positive_module)
152
+ negative_module = camelize("not_#{predicate_method_without_questionmark}_error")
153
+ mod.const_set(negative_module, Module.new) unless mod.const_defined?(negative_module)
154
+
155
+ time = Time.now
156
+ predicate_method_without_rconditions = "#{predicate_method_without_questionmark}_rconditions_#{mod.object_id}_#{time.to_i}_#{time.usec}"
157
+ original_visibility = visibility(mod, predicate_method)
158
+
159
+ mod.class_eval <<-EOS, __FILE__, __LINE__ + 1
160
+ alias_method :#{predicate_method_without_rconditions}, :#{predicate_method}
161
+ private :#{predicate_method_without_rconditions}
162
+
163
+ def #{predicate_method}(*args, &block)
164
+ result = #{predicate_method_without_rconditions}(*args, &block)
165
+ RConditions.exception_modules << (result ? #{positive_module} : #{negative_module}) if RConditions.exception_modules
166
+ result
167
+ end
168
+ #{original_visibility} :#{predicate_method}
169
+ EOS
170
+ end
171
+
172
+ def define_bang_method(mod, predicate_method)
173
+ bang_method = predicate_method[0...-1] + '!'
174
+ return if mod.method_defined?(bang_method) || mod.private_method_defined?(bang_method)
175
+
176
+ visibility = visibility(mod, predicate_method)
177
+
178
+ mod.class_eval <<-EOS, __FILE__, __LINE__ + 1
179
+ def #{bang_method}(*args, &block)
180
+ RConditions.exception_modules = []
181
+ result = #{predicate_method}(*args, &block)
182
+ return result if result
183
+
184
+ e = Exception.new(RConditions.exception_modules.map { |m| m.name }.join(', '))
185
+ RConditions.exception_modules.each { |m| e.extend(m) }
186
+ raise e
187
+ ensure
188
+ RConditions.exception_modules = nil
189
+ end
190
+ #{visibility} :#{bang_method}
191
+ EOS
192
+ end
193
+
194
+ def camelize(lower_case_and_underscored_word)
195
+ lower_case_and_underscored_word.gsub(/(^|_)(.)/) { $2.upcase }
196
+ end
197
+
198
+ def visibility(mod, id)
199
+ case id
200
+ when *mod.public_instance_methods(false): 'public'
201
+ when *mod.protected_instance_methods(false): 'protected'
202
+ when *mod.private_instance_methods(false): 'private'
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ unless defined? RCONDITIONS_ONLY_FOR_NEW_METHODS
209
+ # To apply RConditions to new methods only, set this constant to <tt>true</tt>
210
+ # before <tt>require "rconditions"</tt>.
211
+ RCONDITIONS_ONLY_FOR_NEW_METHODS = false
212
+ end
213
+
214
+ unless RCONDITIONS_ONLY_FOR_NEW_METHODS
215
+ ObjectSpace.each_object(Module) do |mod|
216
+ mod.instance_methods(false).each do |id|
217
+ RConditions.extend_predicate_method_and_define_bang_method(mod, id)
218
+ end
219
+ end
220
+ end
221
+
222
+ class Module
223
+ alias_method :method_added_without_rconditions, :method_added
224
+ private :method_added_without_rconditions
225
+
226
+ # To provide a bang method and exception modules for each predicate method,
227
+ # RConditions hooks into <tt>method_added</tt>. An existing
228
+ # <tt>method_added</tt> implementation will not be replaced, but executed
229
+ # afterwards.
230
+ def method_added(id)
231
+ RConditions.extend_predicate_method_and_define_bang_method(self, id)
232
+ method_added_without_rconditions(id)
233
+ end
234
+ end
@@ -0,0 +1,261 @@
1
+ require 'test/unit'
2
+ require 'rconditions'
3
+
4
+ class RConditionsTest < Test::Unit::TestCase
5
+
6
+ class Foo
7
+ def a?; false; end
8
+ def b?; false; end
9
+ def c?; a? || b?; end
10
+ def d?; true; end
11
+ def e?; !d?; end
12
+ end
13
+
14
+ class Bar < Foo
15
+ def a?; super; end
16
+ end
17
+
18
+ def test_defines_bang_method_on_predicate_methods_defined_before_r_conditions_was_required
19
+ assert [].class.method_defined?(:empty!)
20
+ end
21
+
22
+ def test_defines_bang_method_on_predicate_methods_defined_after_r_conditions_was_required
23
+ assert Foo.method_defined?(:a!)
24
+ end
25
+
26
+ def test_does_not_define_methods_for_non_predicate_methods
27
+ foo_methods_with_new_method = Foo.new.methods + ['new_method']
28
+ eval "class Foo;def new_method; end; end"
29
+ assert_equal foo_methods_with_new_method.sort, Foo.new.methods.sort
30
+ end
31
+
32
+ def test_bang_method_returns_true_if_predicate_method_returns_true
33
+ assert Foo.new.d!
34
+ end
35
+
36
+ def test_bang_method_passes_predicate_method_result
37
+ eval <<-EOS
38
+ class Foo
39
+ def find_file?; 'a string'; end
40
+ end
41
+ EOS
42
+ assert_equal 'a string', Foo.new.find_file!
43
+ end
44
+
45
+ def test_bang_method_raises_exception_if_predicate_method_returns_false
46
+ assert_raises(Exception) { Foo.new.a! }
47
+ end
48
+
49
+ def test_bang_method_passes_arguments_to_the_predicate_method
50
+ eval <<-EOS
51
+ class Foo
52
+ attr_reader :args, :block
53
+ def store_args?(*args, &block); @args, @block = args, block; end
54
+ end
55
+ EOS
56
+ foo = Foo.new
57
+ block = lambda { }
58
+ foo.store_args!(1, 2, &block)
59
+ assert_equal [1, 2], foo.args
60
+ assert_equal block, foo.block
61
+ end
62
+
63
+ def test_exception_raised_by_bang_method_includes_exception_module_named_after_the_predicate_method
64
+ assert exception_raised_by { Foo.new.a! }.kind_of?(Foo::NotAError)
65
+ assert exception_raised_by { Foo.new.b! }.kind_of?(Foo::NotBError)
66
+ assert exception_raised_by { Foo.new.c! }.kind_of?(Foo::NotCError)
67
+ assert exception_raised_by { Bar.new.a! }.kind_of?(Bar::NotAError)
68
+ end
69
+
70
+ def test_exception_raised_by_bang_method_includes_exception_modules_from_predicate_method_called_inside
71
+ e = exception_raised_by { Foo.new.c! }
72
+ assert e.kind_of?(Foo::NotAError)
73
+ assert e.kind_of?(Foo::NotBError)
74
+ end
75
+
76
+ def test_exception_raised_by_bang_method_includes_exception_module_from_superclass_predicate_method
77
+ exception_raised_by { Bar.new.a! }.kind_of?(Foo::AError)
78
+ end
79
+
80
+ def test_exception_raised_by_bang_method_includes_negated_exception_module_from_called_predicate_method
81
+ exception_raised_by { Foo.new.e! }.kind_of?(Foo::NotDError)
82
+ end
83
+
84
+ def test_no_visible_methods_added_except_bang_method
85
+ foo_methods_with_predicate_method = Foo.new.methods + ['new_predicate_method?']
86
+ eval "class Foo;def new_predicate_method?; false; end; end"
87
+ assert_equal ['new_predicate_method!'], Foo.new.methods - foo_methods_with_predicate_method
88
+ end
89
+
90
+ def test_no_bang_method_will_be_overridden
91
+ eval "class Foo;def do_not_override_me!; 'public'; end; end"
92
+ eval "class Foo;def do_not_override_me?; true; end; end"
93
+
94
+ assert_equal 'public', Foo.new.do_not_override_me!
95
+ end
96
+
97
+ def test_no_bang_method_will_be_overridden_even_if_it_is_private
98
+ eval "class Foo;private;def private_do_not_override_me!; 'private'; end; end"
99
+ eval "class Foo;def private_do_not_override_me?; true; end; end"
100
+ assert_equal 'private', Foo.new.__send__(:private_do_not_override_me!)
101
+ end
102
+
103
+ def test_no_bang_method_will_be_overridden_even_if_it_is_in_a_superclas
104
+ eval "class Foo;def super_do_not_override_me!; 'super'; end; end"
105
+ eval "class Bar;def super_do_not_override_me?; true; end; end"
106
+ assert_equal 'super', Foo.new.super_do_not_override_me!
107
+ end
108
+
109
+ def test_some_super_complex_thing
110
+ eval <<-EOS
111
+ class Foo
112
+ def k?; false; end
113
+ def m?; true; end
114
+ def n?; !(k? || m?); end
115
+ def o?; true; end
116
+ def p?; o? && !n?; end
117
+ def q?; !p?; end
118
+ end
119
+ EOS
120
+ e = exception_raised_by { Foo.new.q! }
121
+ assert e.kind_of?(Foo::NotKError)
122
+ assert e.kind_of?(Foo::MError)
123
+ assert e.kind_of?(Foo::NotNError)
124
+ assert e.kind_of?(Foo::OError)
125
+ assert e.kind_of?(Foo::PError)
126
+ assert e.kind_of?(Foo::NotQError)
127
+ end
128
+
129
+ def test_does_work_for_modules
130
+ # any? is defined in the module Enumerable and included in Array
131
+ assert Enumerable.instance_methods(false).include?('any?')
132
+ assert !Array.instance_methods(false).include?('any?')
133
+ assert Array.ancestors.include?(Enumerable)
134
+
135
+ e = exception_raised_by { [].any! }
136
+ assert e.kind_of?(Enumerable::NotAnyError)
137
+ assert_same Array::NotAnyError, Enumerable::NotAnyError
138
+ end
139
+
140
+ def test_does_work_for_alias_method
141
+ eval <<-EOS
142
+ class Foo
143
+ alias_method :a_alias?, :a?
144
+ end
145
+ EOS
146
+ e = exception_raised_by { Foo.new.a_alias! }
147
+ assert e.kind_of?(Foo::NotAAliasError)
148
+ assert e.kind_of?(Foo::NotAError)
149
+ end
150
+
151
+ def test_does_work_for_redefined_methods
152
+ eval <<-EOS
153
+ class Foo
154
+ alias_method :b_old?, :b?
155
+ def b?
156
+ b_old?
157
+ end
158
+ end
159
+ EOS
160
+ e = exception_raised_by { Foo.new.b! }
161
+ assert e.kind_of?(Foo::NotBError)
162
+ end
163
+
164
+ def test_ignores_methods_starting_with_non_letter
165
+ eval <<-EOS
166
+ class Foo
167
+ def _e?
168
+ end
169
+ end
170
+ EOS
171
+ assert !Foo.respond_to?(:_e!)
172
+ end
173
+
174
+ def test_ignores_methods_with_whitespace
175
+ eval <<-EOS
176
+ class Foo
177
+ define_method "I?\n am a method?" do
178
+ true
179
+ end
180
+ end
181
+ EOS
182
+ assert !Foo.new.respond_to?(:"I?\n am a method!")
183
+ end
184
+
185
+ def test_does_work_for_unnamed_class
186
+ c = Class.new
187
+ c.class_eval <<-EOS
188
+ def activated?
189
+ false
190
+ end
191
+ EOS
192
+ e = exception_raised_by { c.new.activated! }
193
+ assert e.kind_of?(c.const_get(:NotActivatedError))
194
+ self.class.const_set(:NowANamedClass, c)
195
+ assert e.kind_of?(NowANamedClass::NotActivatedError)
196
+ end
197
+
198
+ def test_does_work_for_unnamed_module
199
+ m = Module.new
200
+ m.module_eval <<-EOS
201
+ def activated?
202
+ false
203
+ end
204
+ EOS
205
+ Foo.send(:include, m)
206
+ e = exception_raised_by { Foo.new.activated! }
207
+ assert e.kind_of?(m.const_get(:NotActivatedError))
208
+ self.class.const_set(:NowANamedModule, m)
209
+ assert e.kind_of?(NowANamedModule::NotActivatedError)
210
+ end
211
+
212
+ def test_does_not_work_for_virtual_classes
213
+ f = Foo.new
214
+ class << f
215
+ def x?; b?; end
216
+ end
217
+ assert !f.respond_to?(:x!)
218
+ end
219
+
220
+ def test_exception_in_a_predicate_method_does_not_screw_up_the_next_result
221
+ eval <<-EOS
222
+ class Foo
223
+ def raises?; a?; raise "FAILURE"; end
224
+ end
225
+ EOS
226
+ e = exception_raised_by { Foo.new.raises! }
227
+ assert e.message == "FAILURE"
228
+ e = exception_raised_by { Foo.new.b! }
229
+ assert !e.kind_of?(Foo::NotAError)
230
+ end
231
+
232
+ def test_generated_methods_have_the_visibility_of_the_original_methods
233
+ eval <<-EOS
234
+ class Foo
235
+ public
236
+ def public_p?; true; end
237
+ protected
238
+ def protected_p?; true; end
239
+ private
240
+ def private_p?; true; end
241
+ end
242
+ EOS
243
+
244
+ assert Foo.public_instance_methods(false).include?('public_p?')
245
+ assert Foo.protected_instance_methods(false).include?('protected_p?')
246
+ assert Foo.private_instance_methods(false).include?('private_p?')
247
+
248
+ assert Foo.public_instance_methods(false).include?('public_p!')
249
+ assert Foo.protected_instance_methods(false).include?('protected_p!')
250
+ assert Foo.private_instance_methods(false).include?('private_p!')
251
+ end
252
+
253
+ private
254
+
255
+ def exception_raised_by(&block)
256
+ block.call
257
+ fail("No exception raised")
258
+ rescue Exception => e
259
+ e
260
+ end
261
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: rconditions
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-09-27 00:00:00 +02:00
8
+ summary: RConditions generates a bang method (for example, Array#empty!) for each predicate method (for example, Array#empty?). The bang method will return true if the predicate method returns true, otherwise it will throw an exception. The exception will include auto-generated modules that give information on predicate methods passed or failed in the bang method's execution.
9
+ require_paths:
10
+ - lib
11
+ email: tammo@tammofreese.de
12
+ homepage: http://rconditions.rubyforge.org
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: rconditions
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Norman Timmler
31
+ - Tammo Freese
32
+ files:
33
+ - test/rconditions_test.rb
34
+ - lib/rconditions.rb
35
+ - README
36
+ - MIT-LICENSE
37
+ test_files:
38
+ - test/rconditions_test.rb
39
+ rdoc_options: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ executables: []
44
+
45
+ extensions: []
46
+
47
+ requirements: []
48
+
49
+ dependencies: []
50
+