Module: ReturnSafeYield

Defined in:
lib/return_safe_yield.rb,
lib/return_safe_yield/version.rb

Defined Under Namespace

Classes: UnexpectedReturnException

Constant Summary collapse

VERSION =
"0.2.1"

Class Method Summary collapse

Class Method Details

.call_then_yield(first, *args, &_second) ⇒ Object

Calls the two given blocks (first, then ‘&_second`), even if the first block contains a return. The second block receives the return value of the first block as arguments.

The second block is not called if the first one raises an exception.

Example:

unknown_block = proc do
  return
end
ReturnSafeYield.call_then_yield(unknown_block) do
  # => This line is called even though the above block contains a `return`.
end

# => This line here might not be called however as the `return` statement
#    exits the current method context.

You can also pass arguments to the first block:

unknown_block = proc do |arg1, arg2|
end

ReturnSafeYield.call_then_yield(unknown_block, 'arg1 value', 'arg2 value') do
end

The second block receives the first block’s return value as arguments (this does not apply if return is used explicitely):

unknown_block = proc
  'return value'
end

ReturnSafeYield.call_then_yield(unknown_block) do |arg1|
  arg1 == 'return value' # => true
end


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/return_safe_yield.rb', line 42

def self.call_then_yield(first, *args, &_second)
  exception = false
  first_block_result = nil
  returned = true
  begin
    first_block_result = first.call(*args)
    returned = false
    return first_block_result
  rescue Exception
    exception = true
    fail
  ensure
    unless exception
      second_block_result = yield(first_block_result)

      # In this very particular case, using `return` inside of `ensure`
      # is fine as we're checking if there is an exception. There is no other
      # way of returning the second block's result otherwise.
      return second_block_result unless returned
    end
  end
end

.safe_yield(block, *args, &cb) ⇒ Object

Yields the given block and raises a UnexpectedReturnException exception if the block contained a return statement. Thus it is safe to assume that yielding a block in this way never jumps out of your surrounding routine.

Note that you cannot pass a block using ‘safe_yield do`, as it does not make sense to check for return statements in code controlled by the caller itself.

Example:

unknown_block = proc do |some_argument|
  return
end

ReturnSafeYield.safe_yield(unknown_block, some_argument)
# => Raises a UnexpectedReturnException exception


81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/return_safe_yield.rb', line 81

def self.safe_yield(block, *args, &cb)
  state = :returned
  result = block.call(*args, &cb)
  state = :regular
  return result
rescue Exception
  state = :exception
  fail
ensure
  if state == :returned
    fail UnexpectedReturnException, "Block #{block.inspect} contains a `return` which it is not supposed to."
  end
end