Skip to content
Browse files

Added raise!, rescue! and ensure!

  • Loading branch information...
1 parent bc77887 commit 77471b15c7aa7202ec3acc04bcc9393d19d6a834 @drbrain committed Apr 1, 2012
Showing with 578 additions and 55 deletions.
  1. +11 −8 .travis.yml
  2. +8 −1 History.txt
  3. +14 −4 README.rdoc
  4. +2 −2 Rakefile
  5. +225 −40 lib/return_bang.rb
  6. +318 −0 test/test_return_bang.rb
View
19 .travis.yml
@@ -1,11 +1,14 @@
+---
+after_script:
+- rake travis:after -t
before_script:
- - gem install hoe
- - rake check_extra_deps
-rvm:
- - 1.8.7
- - 1.9.2
- - 1.9.3
- - ruby-head
+- gem install hoe-travis --no-rdoc --no-ri
+- rake travis:before -t
+language: ruby
notifications:
email:
- - drbrain@segment7.net
+ - drbrain@segment7.net
+rvm:
+- 1.9.2
+- 1.9.3
+script: rake travis
View
9 History.txt
@@ -1,5 +1,12 @@
+=== 1.1 / 2012-04-01
+
+* Minor enhancements
+ * Added raise! to raise exceptions
+ * Added rescue! to rescue exceptions raised
+ * Added ensure! to always execute a block of code to perform cleanup
+
=== 1.0 / 2011-12-20
-* Major enhancements
+* Major enhancement
* Birthday!
View
18 README.rdoc
@@ -6,16 +6,26 @@ bugs :: https://github.com/drbrain/return_bang/issues
== Description
-return_bang implements non-local exits from methods. Use return_bang to exit
-back to a processing loop from deeply nested code, or just to confound your
-enemies *and* your friends! What could possibly go wrong?
+return_bang implements non-local exits for methods. As a bonus, you also get
+exception handling that ignores standard Ruby's inflexible begin; rescue;
+ensure; end syntax.
+
+Use return_bang to exit back to a processing loop from deeply nested code, or
+just to confound your enemies *and* your friends! What could possibly go
+wrong?
== Features
* Implements non-local exits for methods
* Nestable
* Named and stack-based exit points, go exactly where you need to be
-* Ignores pesky ensure blocks for when you really, really need to return
+* Full exception handling support through raise!, rescue! and ensure!
+* Ignores pesky ensure, rescue and require blocks for when you really, really
+ need to return
+
+== Problems
+
+* Not enough use of continuations
== Synopsis
View
4 Rakefile
@@ -5,14 +5,14 @@ require 'hoe'
Hoe.plugin :minitest
Hoe.plugin :git
+Hoe.plugin :travis
Hoe.spec 'return_bang' do
developer 'Eric Hodel', 'drbrain@segment7.net'
rdoc_locations << 'docs.seattlerb.org:/data/www/docs.seattlerb.org/return_bang/'
- self.readme_file = 'README.rdoc'
- self.extra_rdoc_files << 'README.rdoc'
+ spec_extras['required_ruby_version'] = '>= 1.9.2'
end
# vim: syntax=ruby
View
265 lib/return_bang.rb
@@ -1,54 +1,73 @@
-begin
- require 'continuation'
-rescue LoadError
- # in 1.8 it's built-in
-end
+require 'continuation'
##
# ReturnBang is allows you to perform non-local exits from your methods. One
# potential use of this is in a web framework so that a framework-provided
# utility methods can jump directly back to the request loop.
#
-# return_here is used to designate where execution should be resumed. Return
-# points may be arbitrarily nested. #return! resumes at the previous resume
-# point, #return_to returns to a named return point.
+# Since providing just non-local exits is insufficient for modern Ruby
+# development, full exception handling support is also provided via #raise!,
+# #rescue! and #ensure!. This exception handling support completely bypasses
+# Ruby's strict <tt>begin; rescue; ensure; return</tt> handling.
#
# require 'return_bang' gives you a module you may include only in your
# application or library code. require 'return_bang/everywhere' includes
# ReturnBang in Object, so it is only recommended for application code use.
#
-# Example:
+# == Methods
+#
+# return_here is used to designate where execution should be resumed. Return
+# points may be arbitrarily nested. #return! resumes at the previous resume
+# point, #return_to returns to a named return point.
+#
+# #raise! is used to indicate an exceptional situation has occurred and you
+# would like to skip the rest of the execution.
+#
+# #rescue! is used to rescue exceptions if you have a way to handle them.
+#
+# #ensure! is used when you need to perform cleanup where an exceptional
+# situation may occur.
+#
+# == Example
#
# include ReturnBang
#
# def framework_loop
# loop do
-# # setup code
-#
# return_here do
-# user_code
-# end
+# # setup this request
#
-# # resume execution here
-# end
-# end
+# ensure! do
+# # clean up this request
+# end
+#
+# rescue! FrameworkError do
+# # display framework error
+# end
#
-# def render_error_and_return message
-# # generate error
+# rescue! do
+# # display application error
+# end
#
-# return!
+# user_code
+# end
+# end
# end
#
# def user_code
# user_utility_method
-# # these lines never reached
-# # ...
+#
+# other_condition = some_more code
+#
+# return! if other_condition
+#
+# # rest of user method
# end
#
# def user_utility_method
-# render_error_and_return "blah" if some_condition
-# # these lines never reached
-# # ...
+# raise! "there was an error" if some_condition
+#
+# # rest of utility method
# end
module ReturnBang
@@ -63,31 +82,185 @@ module ReturnBang
class NonLocalJumpError < StandardError
end
+ def _make_exception args # :nodoc:
+ case args.length
+ when 0 then
+ if exception = Thread.current[:current_exception] then
+ exception
+ else
+ RuntimeError.new
+ end
+ when 1 then # exception or string
+ arg = args.first
+
+ case arg = args.first
+ when Class then
+ unless Exception >= arg then
+ raise TypeError,
+ "exception class/object expected (not #{arg.inspect})"
+ end
+ arg.new
+ else
+ RuntimeError.new arg
+ end
+ when 2 then # exception, string
+ klass, message = args
+ klass.new message
+ else
+ raise ArgumentError, 'too many arguments to raise!'
+ end
+ end
+
+ ##
+ # Executes the ensure blocks in +frames+ in the correct order.
+
+ def _return_bang_cleanup frames # :nodoc:
+ chunked = frames.chunk do |type,|
+ type
+ end
+
+ chunked.reverse_each do |type, chunk_frames|
+ case type
+ when :ensure then
+ chunk_frames.each do |_, block|
+ block.call
+ end
+ when :rescue then
+ if exception = Thread.current[:current_exception] then
+ frame = chunk_frames.find do |_, block, objects|
+ objects.any? do |object|
+ object === exception
+ end
+ end
+
+ next unless frame
+
+ # rebuild stack since we've got a handler for the exception.
+ unexecuted = frames[0, frames.index(frame) - 1]
+ _return_bang_stack.concat unexecuted if unexecuted
+
+ _, handler, = frame
+ handler.call exception
+
+ return # the exception was handled, don't continue up the stack
+ end
+ when :return then
+ # ignore
+ else
+ raise "[bug] unknown return_bang frame type #{type}"
+ end
+ end
+ end
+
def _return_bang_names # :nodoc:
Thread.current[:return_bang_names] ||= {}
end
- if {}.respond_to? :key then # 1.9
- def _return_bang_pop # :nodoc:
- return_point = _return_bang_stack.pop
+ def _return_bang_pop # :nodoc:
+ frame = _return_bang_stack.pop
+
+ _return_bang_names.delete _return_bang_names.key _return_bang_stack.length
+
+ frame
+ end
+
+ def _return_bang_stack # :nodoc:
+ Thread.current[:return_bang_stack] ||= []
+ end
+
+ ##
+ # Unwinds the stack to +continuation+ including trimming the stack above the
+ # continuation, removing named return_heres that can't be reached and
+ # executing any ensures in the trimmed stack.
- _return_bang_names.delete _return_bang_names.key _return_bang_stack.length
+ def _return_bang_unwind_to continuation # :nodoc:
+ found = false
- return_point
+ frames = _return_bang_stack.select do |_, block|
+ found || found = block == continuation
end
- else # 1.8
- def _return_bang_pop # :nodoc:
- return_point = _return_bang_stack.pop
- value = _return_bang_stack.length
- _return_bang_names.delete _return_bang_names.index value
+ start = _return_bang_stack.length - frames.length
+
+ _return_bang_stack.slice! start, frames.length
- return_point
+ frames.each_index do |index|
+ offset = start + index
+
+ _return_bang_names.delete _return_bang_names.key offset
end
+
+ _return_bang_cleanup frames
end
- def _return_bang_stack # :nodoc:
- Thread.current[:return_bang_stack] ||= []
+ ##
+ # Adds an ensure block that will be run when exiting this return_here block.
+ #
+ # ensure! blocks run in the order defined and can be added at any time. If
+ # an exception is raised before an ensure! block is encountered, that block
+ # will not be executed.
+ #
+ # Example:
+ #
+ # return_here do
+ # ensure! do
+ # # this ensure! will be executed
+ # end
+ #
+ # raise! "uh-oh!"
+ #
+ # ensure! do
+ # # this ensure! will not be executed
+ # end
+ # end
+
+ def ensure! &block
+ _return_bang_stack.push [:ensure, block]
+ end
+
+ ##
+ # Raises an exception like Kernel#raise.
+ #
+ # ensure! blocks and rescue! exception handlers will be run as the exception
+ # is propagated up the stack.
+
+ def raise! *args
+ Thread.current[:current_exception] = _make_exception args
+
+ type, = _return_bang_stack.first
+
+ _, final = _return_bang_stack.shift if type == :return
+
+ frames = _return_bang_stack.dup
+
+ _return_bang_stack.clear
+
+ _return_bang_cleanup frames
+
+ final.call if final
+ end
+
+ ##
+ # Rescues +exceptions+ raised by raise! and yields the exception caught to
+ # the block given.
+ #
+ # If no exceptions are given, StandardError is rescued (like the rescue
+ # keyword).
+ #
+ # Example:
+ #
+ # return_here do
+ # rescue! do |e|
+ # puts "handled exception #{e.class}: #{e}"
+ # end
+ #
+ # raise! "raising an exception"
+ # end
+
+ def rescue! *exceptions, &block
+ exceptions = [StandardError] if exceptions.empty?
+
+ _return_bang_stack.push [:rescue, block, exceptions]
end
##
@@ -99,7 +272,13 @@ def return! value = nil
raise NonLocalJumpError, 'nowhere to return to' if
_return_bang_stack.empty?
- _return_bang_pop.call value
+ _, continuation, = _return_bang_stack.reverse.find do |type,|
+ type == :return
+ end
+
+ _return_bang_unwind_to continuation
+
+ continuation.call value
end
##
@@ -112,15 +291,21 @@ def return_here name = nil
value = callcc do |cc|
_return_bang_names[name] = _return_bang_stack.length if name
- _return_bang_stack.push cc
+ _return_bang_stack.push [:return, cc]
begin
yield
ensure
- _return_bang_pop
+ _return_bang_unwind_to cc
end
end
+ if exception = Thread.current[:current_exception] then
+ Thread.current[:current_exception] = nil
+
+ raise exception
+ end
+
# here is where the magic happens
unwind_to = Thread.current[:unwind_to]
View
318 test/test_return_bang.rb
@@ -13,6 +13,324 @@ def setup
def teardown
assert_empty _return_bang_stack
assert_empty _return_bang_names
+ assert_nil Thread.current[:current_exception]
+ end
+
+ def test__make_exception
+ e = _make_exception []
+
+ assert_instance_of RuntimeError, e
+ end
+
+ def test__make_exception_class
+ e = _make_exception [StandardError]
+
+ assert_instance_of StandardError, e
+ end
+
+ def test__make_exception_class_message
+ e = _make_exception [StandardError, 'hello']
+
+ assert_instance_of StandardError, e
+
+ assert_equal 'hello', e.message
+ end
+
+ def test__make_exception_current_exception
+ expected = ArgumentError.new
+ Thread.current[:current_exception] = expected
+
+ e = _make_exception []
+
+ assert_same expected, e
+ ensure
+ Thread.current[:current_exception] = nil
+ end
+
+ def test__make_exception_message
+ e = _make_exception %w[hello]
+
+ assert_instance_of RuntimeError, e
+ assert_equal 'hello', e.message
+ end
+
+ def test__make_exception_non_Exception
+ e = assert_raises TypeError do
+ _make_exception [String]
+ end
+
+ assert_equal 'exception class/object expected (not String)', e.message
+ end
+
+ def test_ensure_bang
+ ensured = false
+
+ return_here do
+ ensure! do
+ ensured = true
+ end
+ end
+
+ assert ensured, 'ensured was not executed'
+ end
+
+ def test_ensure_bang_multiple
+ ensured = []
+
+ return_here do
+ ensure! do
+ ensured << 1
+ end
+ ensure! do
+ ensured << 2
+ end
+ end
+
+ assert_equal [1, 2], ensured
+ end
+
+ def test_ensure_bang_multiple_return
+ ensured = []
+
+ return_here do
+ ensure! do
+ ensured << 1
+ end
+ ensure! do
+ ensured << 2
+ end
+
+ return!
+ end
+
+ assert_equal [1, 2], ensured
+ end
+
+ def test_ensure_bang_nest
+ ensured = []
+
+ return_here do
+ ensure! do
+ ensured << 2
+ end
+ return_here do
+ ensure! do
+ ensured << 1
+ end
+ end
+ end
+
+ assert_equal [1, 2], ensured
+ end
+
+ def test_ensure_bang_nest_raise
+ ensured = []
+
+ assert_raises RuntimeError do
+ return_here do
+ ensure! do
+ ensured << 2
+ end
+ return_here do
+ ensure! do
+ ensured << 1
+ end
+
+ raise!
+ end
+ end
+ end
+
+ assert_equal [1, 2], ensured
+ end
+
+ def test_ensure_bang_raise_after
+ ensured = false
+
+ assert_raises RuntimeError do
+ return_here do
+ ensure! do
+ ensured = true
+ end
+
+ refute ensured, 'ensure! executed too soon'
+
+ raise!
+ end
+ end
+
+ assert ensured, 'ensure! not executed'
+ end
+
+ def test_ensure_bang_raise_before
+ ensured = false
+
+ assert_raises RuntimeError do
+ return_here do
+ raise!
+
+ ensure! do
+ ensured = true
+ end
+ end
+ end
+
+ refute ensured, 'ensure! must not be executed'
+ end
+
+ def test_ensure_bang_raise_in_ensure
+ ensured = []
+
+ assert_raises RuntimeError do
+ return_here do
+ ensure! do
+ ensured << 2
+ end
+
+ return_here do
+ ensure! do
+ ensured << 1
+ raise!
+ end
+ end
+ end
+ end
+
+ assert_equal [1, 2], ensured
+ end
+
+ def test_raise_bang
+ e = assert_raises RuntimeError do
+ return_here do
+ raise! 'hello'
+ end
+ end
+
+ assert_equal 'hello', e.message
+ end
+
+ def test_raise_bang_ignore_rescue
+ assert_raises RuntimeError do
+ return_here do
+ begin
+ raise! 'hello'
+ rescue
+ flunk 'must not execute rescue body'
+ end
+ end
+ end
+ end
+
+ def test_raise_bang_re_raise
+ rescues = []
+
+ assert_raises ArgumentError do
+ return_here do
+ rescue! do
+ rescues << 2
+ end
+
+ return_here do
+ rescue! do
+ rescues << 1
+
+ raise!
+ end
+
+ raise! ArgumentError, 'hello'
+ end
+ end
+ end
+
+ assert_equal [1, 2], rescues
+ end
+
+ def test_rescue_bang
+ rescued = false
+
+ assert_raises RuntimeError do
+ return_here do
+ rescue! do
+ rescued = true
+ end
+
+ raise! 'hello'
+ end
+ end
+
+ assert rescued, 'rescue not executed'
+ end
+
+ def test_rescue_bang_default
+ rescued = false
+
+ assert_raises Exception do
+ return_here do
+ rescue! do
+ rescued = true
+ end
+
+ raise! Exception
+ end
+ end
+
+ refute rescued, 'rescue must default to StandardError'
+ end
+
+ def test_rescue_bang_exceptions
+ rescued = false
+ ensured = true
+
+ return_here do
+ rescue! do
+ rescued = true
+ end
+
+ ensure! do
+ ensured = true
+ end
+
+ return!
+ end
+
+ refute rescued, 'rescue! must not execute'
+ assert ensured, 'ensure! must execute'
+ end
+
+ def test_rescue_bang_multiple
+ rescued = false
+
+ assert_raises TypeError do
+ return_here do
+ rescue! ArgumentError, TypeError do
+ rescued = true
+ end
+
+ raise! TypeError
+ end
+ end
+
+ assert rescued, 'rescue not executed'
+ end
+
+ def test_rescue_bang_type
+ rescued = false
+
+ assert_raises StandardError do
+ return_here do
+ rescue! StandardError do
+ rescued = true
+ end
+
+ rescue! RuntimeError do
+ flunk 'wrong rescue! executed'
+ end
+
+ raise! StandardError
+ end
+ end
+
+ assert rescued, 'StandardError exception not rescued'
end
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.