Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Added raise!, rescue! and ensure!

  • Loading branch information...
commit 77471b15c7aa7202ec3acc04bcc9393d19d6a834 1 parent bc77887
Eric Hodel authored
19 .travis.yml
... ... @@ -1,11 +1,14 @@
  1 +---
  2 +after_script:
  3 +- rake travis:after -t
1 4 before_script:
2   - - gem install hoe
3   - - rake check_extra_deps
4   -rvm:
5   - - 1.8.7
6   - - 1.9.2
7   - - 1.9.3
8   - - ruby-head
  5 +- gem install hoe-travis --no-rdoc --no-ri
  6 +- rake travis:before -t
  7 +language: ruby
9 8 notifications:
10 9 email:
11   - - drbrain@segment7.net
  10 + - drbrain@segment7.net
  11 +rvm:
  12 +- 1.9.2
  13 +- 1.9.3
  14 +script: rake travis
9 History.txt
... ... @@ -1,5 +1,12 @@
  1 +=== 1.1 / 2012-04-01
  2 +
  3 +* Minor enhancements
  4 + * Added raise! to raise exceptions
  5 + * Added rescue! to rescue exceptions raised
  6 + * Added ensure! to always execute a block of code to perform cleanup
  7 +
1 8 === 1.0 / 2011-12-20
2 9
3   -* Major enhancements
  10 +* Major enhancement
4 11 * Birthday!
5 12
18 README.rdoc
Source Rendered
@@ -6,16 +6,26 @@ bugs :: https://github.com/drbrain/return_bang/issues
6 6
7 7 == Description
8 8
9   -return_bang implements non-local exits from methods. Use return_bang to exit
10   -back to a processing loop from deeply nested code, or just to confound your
11   -enemies *and* your friends! What could possibly go wrong?
  9 +return_bang implements non-local exits for methods. As a bonus, you also get
  10 +exception handling that ignores standard Ruby's inflexible begin; rescue;
  11 +ensure; end syntax.
  12 +
  13 +Use return_bang to exit back to a processing loop from deeply nested code, or
  14 +just to confound your enemies *and* your friends! What could possibly go
  15 +wrong?
12 16
13 17 == Features
14 18
15 19 * Implements non-local exits for methods
16 20 * Nestable
17 21 * Named and stack-based exit points, go exactly where you need to be
18   -* Ignores pesky ensure blocks for when you really, really need to return
  22 +* Full exception handling support through raise!, rescue! and ensure!
  23 +* Ignores pesky ensure, rescue and require blocks for when you really, really
  24 + need to return
  25 +
  26 +== Problems
  27 +
  28 +* Not enough use of continuations
19 29
20 30 == Synopsis
21 31
4 Rakefile
@@ -5,14 +5,14 @@ require 'hoe'
5 5
6 6 Hoe.plugin :minitest
7 7 Hoe.plugin :git
  8 +Hoe.plugin :travis
8 9
9 10 Hoe.spec 'return_bang' do
10 11 developer 'Eric Hodel', 'drbrain@segment7.net'
11 12
12 13 rdoc_locations << 'docs.seattlerb.org:/data/www/docs.seattlerb.org/return_bang/'
13 14
14   - self.readme_file = 'README.rdoc'
15   - self.extra_rdoc_files << 'README.rdoc'
  15 + spec_extras['required_ruby_version'] = '>= 1.9.2'
16 16 end
17 17
18 18 # vim: syntax=ruby
265 lib/return_bang.rb
... ... @@ -1,54 +1,73 @@
1   -begin
2   - require 'continuation'
3   -rescue LoadError
4   - # in 1.8 it's built-in
5   -end
  1 +require 'continuation'
6 2
7 3 ##
8 4 # ReturnBang is allows you to perform non-local exits from your methods. One
9 5 # potential use of this is in a web framework so that a framework-provided
10 6 # utility methods can jump directly back to the request loop.
11 7 #
12   -# return_here is used to designate where execution should be resumed. Return
13   -# points may be arbitrarily nested. #return! resumes at the previous resume
14   -# point, #return_to returns to a named return point.
  8 +# Since providing just non-local exits is insufficient for modern Ruby
  9 +# development, full exception handling support is also provided via #raise!,
  10 +# #rescue! and #ensure!. This exception handling support completely bypasses
  11 +# Ruby's strict <tt>begin; rescue; ensure; return</tt> handling.
15 12 #
16 13 # require 'return_bang' gives you a module you may include only in your
17 14 # application or library code. require 'return_bang/everywhere' includes
18 15 # ReturnBang in Object, so it is only recommended for application code use.
19 16 #
20   -# Example:
  17 +# == Methods
  18 +#
  19 +# return_here is used to designate where execution should be resumed. Return
  20 +# points may be arbitrarily nested. #return! resumes at the previous resume
  21 +# point, #return_to returns to a named return point.
  22 +#
  23 +# #raise! is used to indicate an exceptional situation has occurred and you
  24 +# would like to skip the rest of the execution.
  25 +#
  26 +# #rescue! is used to rescue exceptions if you have a way to handle them.
  27 +#
  28 +# #ensure! is used when you need to perform cleanup where an exceptional
  29 +# situation may occur.
  30 +#
  31 +# == Example
21 32 #
22 33 # include ReturnBang
23 34 #
24 35 # def framework_loop
25 36 # loop do
26   -# # setup code
27   -#
28 37 # return_here do
29   -# user_code
30   -# end
  38 +# # setup this request
31 39 #
32   -# # resume execution here
33   -# end
34   -# end
  40 +# ensure! do
  41 +# # clean up this request
  42 +# end
  43 +#
  44 +# rescue! FrameworkError do
  45 +# # display framework error
  46 +# end
35 47 #
36   -# def render_error_and_return message
37   -# # generate error
  48 +# rescue! do
  49 +# # display application error
  50 +# end
38 51 #
39   -# return!
  52 +# user_code
  53 +# end
  54 +# end
40 55 # end
41 56 #
42 57 # def user_code
43 58 # user_utility_method
44   -# # these lines never reached
45   -# # ...
  59 +#
  60 +# other_condition = some_more code
  61 +#
  62 +# return! if other_condition
  63 +#
  64 +# # rest of user method
46 65 # end
47 66 #
48 67 # def user_utility_method
49   -# render_error_and_return "blah" if some_condition
50   -# # these lines never reached
51   -# # ...
  68 +# raise! "there was an error" if some_condition
  69 +#
  70 +# # rest of utility method
52 71 # end
53 72
54 73 module ReturnBang
@@ -63,31 +82,185 @@ module ReturnBang
63 82 class NonLocalJumpError < StandardError
64 83 end
65 84
  85 + def _make_exception args # :nodoc:
  86 + case args.length
  87 + when 0 then
  88 + if exception = Thread.current[:current_exception] then
  89 + exception
  90 + else
  91 + RuntimeError.new
  92 + end
  93 + when 1 then # exception or string
  94 + arg = args.first
  95 +
  96 + case arg = args.first
  97 + when Class then
  98 + unless Exception >= arg then
  99 + raise TypeError,
  100 + "exception class/object expected (not #{arg.inspect})"
  101 + end
  102 + arg.new
  103 + else
  104 + RuntimeError.new arg
  105 + end
  106 + when 2 then # exception, string
  107 + klass, message = args
  108 + klass.new message
  109 + else
  110 + raise ArgumentError, 'too many arguments to raise!'
  111 + end
  112 + end
  113 +
  114 + ##
  115 + # Executes the ensure blocks in +frames+ in the correct order.
  116 +
  117 + def _return_bang_cleanup frames # :nodoc:
  118 + chunked = frames.chunk do |type,|
  119 + type
  120 + end
  121 +
  122 + chunked.reverse_each do |type, chunk_frames|
  123 + case type
  124 + when :ensure then
  125 + chunk_frames.each do |_, block|
  126 + block.call
  127 + end
  128 + when :rescue then
  129 + if exception = Thread.current[:current_exception] then
  130 + frame = chunk_frames.find do |_, block, objects|
  131 + objects.any? do |object|
  132 + object === exception
  133 + end
  134 + end
  135 +
  136 + next unless frame
  137 +
  138 + # rebuild stack since we've got a handler for the exception.
  139 + unexecuted = frames[0, frames.index(frame) - 1]
  140 + _return_bang_stack.concat unexecuted if unexecuted
  141 +
  142 + _, handler, = frame
  143 + handler.call exception
  144 +
  145 + return # the exception was handled, don't continue up the stack
  146 + end
  147 + when :return then
  148 + # ignore
  149 + else
  150 + raise "[bug] unknown return_bang frame type #{type}"
  151 + end
  152 + end
  153 + end
  154 +
66 155 def _return_bang_names # :nodoc:
67 156 Thread.current[:return_bang_names] ||= {}
68 157 end
69 158
70   - if {}.respond_to? :key then # 1.9
71   - def _return_bang_pop # :nodoc:
72   - return_point = _return_bang_stack.pop
  159 + def _return_bang_pop # :nodoc:
  160 + frame = _return_bang_stack.pop
  161 +
  162 + _return_bang_names.delete _return_bang_names.key _return_bang_stack.length
  163 +
  164 + frame
  165 + end
  166 +
  167 + def _return_bang_stack # :nodoc:
  168 + Thread.current[:return_bang_stack] ||= []
  169 + end
  170 +
  171 + ##
  172 + # Unwinds the stack to +continuation+ including trimming the stack above the
  173 + # continuation, removing named return_heres that can't be reached and
  174 + # executing any ensures in the trimmed stack.
73 175
74   - _return_bang_names.delete _return_bang_names.key _return_bang_stack.length
  176 + def _return_bang_unwind_to continuation # :nodoc:
  177 + found = false
75 178
76   - return_point
  179 + frames = _return_bang_stack.select do |_, block|
  180 + found || found = block == continuation
77 181 end
78   - else # 1.8
79   - def _return_bang_pop # :nodoc:
80   - return_point = _return_bang_stack.pop
81   - value = _return_bang_stack.length
82 182
83   - _return_bang_names.delete _return_bang_names.index value
  183 + start = _return_bang_stack.length - frames.length
  184 +
  185 + _return_bang_stack.slice! start, frames.length
84 186
85   - return_point
  187 + frames.each_index do |index|
  188 + offset = start + index
  189 +
  190 + _return_bang_names.delete _return_bang_names.key offset
86 191 end
  192 +
  193 + _return_bang_cleanup frames
87 194 end
88 195
89   - def _return_bang_stack # :nodoc:
90   - Thread.current[:return_bang_stack] ||= []
  196 + ##
  197 + # Adds an ensure block that will be run when exiting this return_here block.
  198 + #
  199 + # ensure! blocks run in the order defined and can be added at any time. If
  200 + # an exception is raised before an ensure! block is encountered, that block
  201 + # will not be executed.
  202 + #
  203 + # Example:
  204 + #
  205 + # return_here do
  206 + # ensure! do
  207 + # # this ensure! will be executed
  208 + # end
  209 + #
  210 + # raise! "uh-oh!"
  211 + #
  212 + # ensure! do
  213 + # # this ensure! will not be executed
  214 + # end
  215 + # end
  216 +
  217 + def ensure! &block
  218 + _return_bang_stack.push [:ensure, block]
  219 + end
  220 +
  221 + ##
  222 + # Raises an exception like Kernel#raise.
  223 + #
  224 + # ensure! blocks and rescue! exception handlers will be run as the exception
  225 + # is propagated up the stack.
  226 +
  227 + def raise! *args
  228 + Thread.current[:current_exception] = _make_exception args
  229 +
  230 + type, = _return_bang_stack.first
  231 +
  232 + _, final = _return_bang_stack.shift if type == :return
  233 +
  234 + frames = _return_bang_stack.dup
  235 +
  236 + _return_bang_stack.clear
  237 +
  238 + _return_bang_cleanup frames
  239 +
  240 + final.call if final
  241 + end
  242 +
  243 + ##
  244 + # Rescues +exceptions+ raised by raise! and yields the exception caught to
  245 + # the block given.
  246 + #
  247 + # If no exceptions are given, StandardError is rescued (like the rescue
  248 + # keyword).
  249 + #
  250 + # Example:
  251 + #
  252 + # return_here do
  253 + # rescue! do |e|
  254 + # puts "handled exception #{e.class}: #{e}"
  255 + # end
  256 + #
  257 + # raise! "raising an exception"
  258 + # end
  259 +
  260 + def rescue! *exceptions, &block
  261 + exceptions = [StandardError] if exceptions.empty?
  262 +
  263 + _return_bang_stack.push [:rescue, block, exceptions]
91 264 end
92 265
93 266 ##
@@ -99,7 +272,13 @@ def return! value = nil
99 272 raise NonLocalJumpError, 'nowhere to return to' if
100 273 _return_bang_stack.empty?
101 274
102   - _return_bang_pop.call value
  275 + _, continuation, = _return_bang_stack.reverse.find do |type,|
  276 + type == :return
  277 + end
  278 +
  279 + _return_bang_unwind_to continuation
  280 +
  281 + continuation.call value
103 282 end
104 283
105 284 ##
@@ -112,15 +291,21 @@ def return_here name = nil
112 291
113 292 value = callcc do |cc|
114 293 _return_bang_names[name] = _return_bang_stack.length if name
115   - _return_bang_stack.push cc
  294 + _return_bang_stack.push [:return, cc]
116 295
117 296 begin
118 297 yield
119 298 ensure
120   - _return_bang_pop
  299 + _return_bang_unwind_to cc
121 300 end
122 301 end
123 302
  303 + if exception = Thread.current[:current_exception] then
  304 + Thread.current[:current_exception] = nil
  305 +
  306 + raise exception
  307 + end
  308 +
124 309 # here is where the magic happens
125 310 unwind_to = Thread.current[:unwind_to]
126 311
318 test/test_return_bang.rb
@@ -13,6 +13,324 @@ def setup
13 13 def teardown
14 14 assert_empty _return_bang_stack
15 15 assert_empty _return_bang_names
  16 + assert_nil Thread.current[:current_exception]
  17 + end
  18 +
  19 + def test__make_exception
  20 + e = _make_exception []
  21 +
  22 + assert_instance_of RuntimeError, e
  23 + end
  24 +
  25 + def test__make_exception_class
  26 + e = _make_exception [StandardError]
  27 +
  28 + assert_instance_of StandardError, e
  29 + end
  30 +
  31 + def test__make_exception_class_message
  32 + e = _make_exception [StandardError, 'hello']
  33 +
  34 + assert_instance_of StandardError, e
  35 +
  36 + assert_equal 'hello', e.message
  37 + end
  38 +
  39 + def test__make_exception_current_exception
  40 + expected = ArgumentError.new
  41 + Thread.current[:current_exception] = expected
  42 +
  43 + e = _make_exception []
  44 +
  45 + assert_same expected, e
  46 + ensure
  47 + Thread.current[:current_exception] = nil
  48 + end
  49 +
  50 + def test__make_exception_message
  51 + e = _make_exception %w[hello]
  52 +
  53 + assert_instance_of RuntimeError, e
  54 + assert_equal 'hello', e.message
  55 + end
  56 +
  57 + def test__make_exception_non_Exception
  58 + e = assert_raises TypeError do
  59 + _make_exception [String]
  60 + end
  61 +
  62 + assert_equal 'exception class/object expected (not String)', e.message
  63 + end
  64 +
  65 + def test_ensure_bang
  66 + ensured = false
  67 +
  68 + return_here do
  69 + ensure! do
  70 + ensured = true
  71 + end
  72 + end
  73 +
  74 + assert ensured, 'ensured was not executed'
  75 + end
  76 +
  77 + def test_ensure_bang_multiple
  78 + ensured = []
  79 +
  80 + return_here do
  81 + ensure! do
  82 + ensured << 1
  83 + end
  84 + ensure! do
  85 + ensured << 2
  86 + end
  87 + end
  88 +
  89 + assert_equal [1, 2], ensured
  90 + end
  91 +
  92 + def test_ensure_bang_multiple_return
  93 + ensured = []
  94 +
  95 + return_here do
  96 + ensure! do
  97 + ensured << 1
  98 + end
  99 + ensure! do
  100 + ensured << 2
  101 + end
  102 +
  103 + return!
  104 + end
  105 +
  106 + assert_equal [1, 2], ensured
  107 + end
  108 +
  109 + def test_ensure_bang_nest
  110 + ensured = []
  111 +
  112 + return_here do
  113 + ensure! do
  114 + ensured << 2
  115 + end
  116 + return_here do
  117 + ensure! do
  118 + ensured << 1
  119 + end
  120 + end
  121 + end
  122 +
  123 + assert_equal [1, 2], ensured
  124 + end
  125 +
  126 + def test_ensure_bang_nest_raise
  127 + ensured = []
  128 +
  129 + assert_raises RuntimeError do
  130 + return_here do
  131 + ensure! do
  132 + ensured << 2
  133 + end
  134 + return_here do
  135 + ensure! do
  136 + ensured << 1
  137 + end
  138 +
  139 + raise!
  140 + end
  141 + end
  142 + end
  143 +
  144 + assert_equal [1, 2], ensured
  145 + end
  146 +
  147 + def test_ensure_bang_raise_after
  148 + ensured = false
  149 +
  150 + assert_raises RuntimeError do
  151 + return_here do
  152 + ensure! do
  153 + ensured = true
  154 + end
  155 +
  156 + refute ensured, 'ensure! executed too soon'
  157 +
  158 + raise!
  159 + end
  160 + end
  161 +
  162 + assert ensured, 'ensure! not executed'
  163 + end
  164 +
  165 + def test_ensure_bang_raise_before
  166 + ensured = false
  167 +
  168 + assert_raises RuntimeError do
  169 + return_here do
  170 + raise!
  171 +
  172 + ensure! do
  173 + ensured = true
  174 + end
  175 + end
  176 + end
  177 +
  178 + refute ensured, 'ensure! must not be executed'
  179 + end
  180 +
  181 + def test_ensure_bang_raise_in_ensure
  182 + ensured = []
  183 +
  184 + assert_raises RuntimeError do
  185 + return_here do
  186 + ensure! do
  187 + ensured << 2
  188 + end
  189 +
  190 + return_here do
  191 + ensure! do
  192 + ensured << 1
  193 + raise!
  194 + end
  195 + end
  196 + end
  197 + end
  198 +
  199 + assert_equal [1, 2], ensured
  200 + end
  201 +
  202 + def test_raise_bang
  203 + e = assert_raises RuntimeError do
  204 + return_here do
  205 + raise! 'hello'
  206 + end
  207 + end
  208 +
  209 + assert_equal 'hello', e.message
  210 + end
  211 +
  212 + def test_raise_bang_ignore_rescue
  213 + assert_raises RuntimeError do
  214 + return_here do
  215 + begin
  216 + raise! 'hello'
  217 + rescue
  218 + flunk 'must not execute rescue body'
  219 + end
  220 + end
  221 + end
  222 + end
  223 +
  224 + def test_raise_bang_re_raise
  225 + rescues = []
  226 +
  227 + assert_raises ArgumentError do
  228 + return_here do
  229 + rescue! do
  230 + rescues << 2
  231 + end
  232 +
  233 + return_here do
  234 + rescue! do
  235 + rescues << 1
  236 +
  237 + raise!
  238 + end
  239 +
  240 + raise! ArgumentError, 'hello'
  241 + end
  242 + end
  243 + end
  244 +
  245 + assert_equal [1, 2], rescues
  246 + end
  247 +
  248 + def test_rescue_bang
  249 + rescued = false
  250 +
  251 + assert_raises RuntimeError do
  252 + return_here do
  253 + rescue! do
  254 + rescued = true
  255 + end
  256 +
  257 + raise! 'hello'
  258 + end
  259 + end
  260 +
  261 + assert rescued, 'rescue not executed'
  262 + end
  263 +
  264 + def test_rescue_bang_default
  265 + rescued = false
  266 +
  267 + assert_raises Exception do
  268 + return_here do
  269 + rescue! do
  270 + rescued = true
  271 + end
  272 +
  273 + raise! Exception
  274 + end
  275 + end
  276 +
  277 + refute rescued, 'rescue must default to StandardError'
  278 + end
  279 +
  280 + def test_rescue_bang_exceptions
  281 + rescued = false
  282 + ensured = true
  283 +
  284 + return_here do
  285 + rescue! do
  286 + rescued = true
  287 + end
  288 +
  289 + ensure! do
  290 + ensured = true
  291 + end
  292 +
  293 + return!
  294 + end
  295 +
  296 + refute rescued, 'rescue! must not execute'
  297 + assert ensured, 'ensure! must execute'
  298 + end
  299 +
  300 + def test_rescue_bang_multiple
  301 + rescued = false
  302 +
  303 + assert_raises TypeError do
  304 + return_here do
  305 + rescue! ArgumentError, TypeError do
  306 + rescued = true
  307 + end
  308 +
  309 + raise! TypeError
  310 + end
  311 + end
  312 +
  313 + assert rescued, 'rescue not executed'
  314 + end
  315 +
  316 + def test_rescue_bang_type
  317 + rescued = false
  318 +
  319 + assert_raises StandardError do
  320 + return_here do
  321 + rescue! StandardError do
  322 + rescued = true
  323 + end
  324 +
  325 + rescue! RuntimeError do
  326 + flunk 'wrong rescue! executed'
  327 + end
  328 +
  329 + raise! StandardError
  330 + end
  331 + end
  332 +
  333 + assert rescued, 'StandardError exception not rescued'
16 334 end
17 335
18 336 def test_return_bang_no_return_here

0 comments on commit 77471b1

Please sign in to comment.
Something went wrong with that request. Please try again.