Index: lib/more/facets/forwardable_chain.rb
===================================================================
--- lib/more/facets/forwardable_chain.rb	(revision 0)
+++ lib/more/facets/forwardable_chain.rb	(revision 0)
@@ -0,0 +1,162 @@
+# The ForwardableChain module is a superset of the capabilities of Forwardable.
+# It provides delegation of specified methods to a chain of designated objects,
+# using the class method _def_delegator_chain_. This class method becomes
+# available to your module when ForwardableChain is included in your module as
+# in the example below. (Note: *include* rather than _extend_.) Your module will
+# reveal ForwardableChain to be in its type hierarchy (see Module#ancestors),
+# which can be useful for debugging.
+# 
+# The pattern implemented by ForwardableChain is useful in creating presenter
+# classes and facade classes whose purpose is to abstract away the complexity of
+# working with graphs of objects.
+# 
+# For example, say you have a class _AccountFacade_ which has attributes
+# _current_ and _legacy_ representing two distinct but analogous system
+# interfaces. You can use ForwardableChain to define methods of AccountFacade
+# which call methods deep in the object graph of _current_ and _legacy_.
+# 
+#   class AccountPresenter
+#     
+#     include ForwardableChain
+#     
+#     # Define a #name method that looks first in #current and then in
+#     # #legacy for an account name -- that is, #current overrides #legacy.
+#     def_delegator_chain :name, {:current => {:account => :name}},
+#                                {:legacy => {:account => {:name_info => :acct_name}}}
+#     
+#     # Define a #primary_ssn method that returns
+#     # current.account.primary_holder.ssn unless any of the intermediate return
+#     # values are nil.
+#     def_delegator_chain :primary_ssn, :current => {:account => {:primary_holder => :ssn}}
+#     
+#     # Define an #aggregate_balance method that returns current.grand_total
+#     # unless #current is nil.
+#     def_delegator_chain :aggregate_balance, :current => :grand_total
+#     
+#   end
+# 
+# The following is the sequence of method calls triggered by a call to
+# <em>AccountPresenter#name</em> defined above:
+# 
+# - <em>name</em> -- which calls ...
+#   - <em>current</em> -- if it returns +nil+ then _legacy_ is called, otherwise ...
+#     - <em>current.account</em> -- on which, unless it returns +nil+, is called ...
+#       - <em>current.account.name</em> -- if it returns +nil+, then is called ...
+#   - <em>legacy</em> -- unless it returns +nil+ ...
+#     - <em>legacy.account</em> -- on which, unless it returns +nil+, is called ...
+#       - <em>legacy.account.name_info</em> -- on which, unless it returns +nil+, is called ...
+#         - <em>legacy.account.name_info.acct_name</em>
+# 
+# Methods with arguments are also supported, but be sure that all the methods in
+# the chain can receive the same arguments, or else an ArgumentError will occur
+# when you call your delegator method.
+# 
+# You can also use instance variables as links in a delegate chain as follows.
+# 
+#   class AccountPresenter
+#     
+#     include ForwardableChain
+#     
+#     # Define an #aggregate_balance method that returns the @grand_total
+#     # variable of @current unless @current is nil.
+#     def_delegator_chain :aggregate_balance, :@current => :@grand_total
+#     
+#   end
+# 
+# Instance variables and methods both can appear in a chain. As you might
+# expect, arguments are ignored when accessing instance variables.
+module ForwardableChain
+  
+  module ClassMethods
+    
+    # Alias for #def_instance_delegator_chain.
+    def def_delegator_chain(method, *delegates)
+      def_instance_delegator_chain method, *delegates
+    end
+    
+    # Defines a method _method_ which delegates to a series of method calls
+    # described by one or more Hash objects provided as _delegates_.
+    # 
+    # See the examples at ForwardableChain.
+    def def_instance_delegator_chain(method, *delegates)
+      delegates.each do |delegate|
+        DelegateExtension.extend_recursive delegate
+        unless delegate.valid_recursive?
+          raise ArgumentError,
+                'expected the name of a new method to be defined, followed ' +
+                'by an argument array of zero or more hashes -- possibly '   +
+                'nested, each containing a single key -- representing a '    +
+                'chain of method calls and/or instance variables used to '   +
+                'supply the return value of the method'
+        end
+      end
+      define_method(method) do |*args|
+        result = nil
+        delegates.each do |delegate|
+          unless (value = delegate.call_recursive(self, *args)).nil?
+            result = value
+            break
+          end
+        end
+        result
+      end
+    end
+    
+  end
+  
+  module DelegateExtension #:nodoc:
+    
+    class << self
+      
+      def extend_recursive(hash)
+        return false unless hash.kind_of?(Hash)
+        hash.extend self
+        hash.each_pair { |key, value| extend_recursive value }
+        true
+      end
+      
+      def send_message_or_get_instance_variable(receiver,
+                                                message_or_instance_variable,
+                                                *args)
+        if message_or_instance_variable.to_s[0..0] == '@'
+          receiver.instance_variable_get message_or_instance_variable
+        else
+          receiver.send message_or_instance_variable, *args
+        end
+      end
+      
+    end
+    
+    def call_recursive(receiver, *args)
+      next_receiver_name = keys.first
+      next_receiver = DelegateExtension.send_message_or_get_instance_variable(receiver,
+                                                                              next_receiver_name,
+                                                                              *args)
+      return nil if next_receiver.nil?
+      next_message_or_delegate = self[next_receiver_name]
+      if next_message_or_delegate.respond_to?(:call_recursive)
+        return next_message_or_delegate.call_recursive(next_receiver, *args)
+      end
+      DelegateExtension.send_message_or_get_instance_variable next_receiver,
+                                                              next_message_or_delegate,
+                                                              *args
+    end
+    
+    def valid_recursive?
+      return false unless (length == 1)
+      next_receiver = self[keys.first]
+      return true if next_receiver.kind_of?(Symbol)
+      if next_receiver.respond_to?(:valid_recursive?)
+        return next_receiver.valid_recursive?
+      end
+      false
+    end
+    
+  end
+  
+  # Extends _other_module_ with the instance methods of ClassMethods.
+  def self.included(other_module)
+    other_module.extend ClassMethods
+  end
+  
+end
Index: test/more/test_forwardable_chain.rb
===================================================================
--- test/more/test_forwardable_chain.rb	(revision 0)
+++ test/more/test_forwardable_chain.rb	(revision 0)
@@ -0,0 +1,276 @@
+require 'test/unit'
+require 'rubygems'
+require 'mocha'
+require 'facets/forwardable_chain'
+
+module ForwardableChainTest #:nodoc: all
+  
+  class MethodWithNoDelegates < Test::Unit::TestCase
+    
+    class Foo
+      
+      include ForwardableChain
+      
+      def_instance_delegator_chain :no_delegates
+      
+    end
+    
+    def test_should_return_nil
+      assert_nil Foo.new.no_delegates
+    end
+    
+  end
+  
+  class MethodWithOneDelegateThatIsInvalidBecauseItContainsMoreThanOneKeyValuePair < Test::Unit::TestCase
+    
+    def test_should_raise_argument_error_from_definition
+      assert_raise(ArgumentError) do
+        self.class.class_eval <<-end_class_eval
+          class Foo
+            include ForwardableChain
+            def_instance_delegator_chain :one_invalid_delegate, :bar => :baz,
+                                                                :bat => :pwop
+          end
+        end_class_eval
+      end
+    end
+    
+  end
+  
+  class MethodWithOneSimpleMethodDelegate < Test::Unit::TestCase
+    
+    class Foo
+      
+      include ForwardableChain
+      
+      def_instance_delegator_chain :one_simple_delegate, :bar => :baz
+      
+    end
+    
+    def test_should_call_first_delegate_with_no_args
+      foo = Foo.new
+      
+      mock_bar = mock
+      mock_bar.expects(:baz).with().returns 'baz'
+      foo.expects(:bar).with().returns mock_bar
+      
+      assert_equal 'baz', foo.one_simple_delegate
+    end
+    
+  end
+  
+  class MethodWithOneSimpleVariableDelegate < Test::Unit::TestCase
+    
+    class Foo
+      
+      include ForwardableChain
+      
+      def_instance_delegator_chain :one_simple_delegate, :@bar => :@baz
+      
+    end
+    
+    def test_should_access_first_delegate
+      foo = Foo.new
+      
+      mock_bar = mock
+      mock_bar.expects(:instance_variable_get).with(:@baz).returns 'baz'
+      foo.expects(:instance_variable_get).with(:@bar).returns mock_bar
+      
+      assert_equal 'baz', foo.one_simple_delegate
+    end
+    
+  end
+  
+  class MethodWithOneComplexMethodDelegate < Test::Unit::TestCase
+    
+    class Foo
+      
+      include ForwardableChain
+      
+      def_instance_delegator_chain :one_complex_delegate, :bar => {:baz => :bat}
+      
+    end
+    
+    def setup
+      @foo = Foo.new
+    end
+    
+    def test_should_call_delegate_with_no_arguments
+      mock_baz = mock
+      mock_baz.expects(:bat).with().returns 'bat'
+      mock_bar = mock
+      mock_bar.expects(:baz).with().returns mock_baz
+      @foo.expects(:bar).with().returns mock_bar
+      
+      assert_equal 'bat', @foo.one_complex_delegate
+    end
+    
+    def test_should_call_delegate_with_one_argument
+      mock_baz = mock
+      mock_baz.expects(:bat).with(:an_arg).returns 'bat'
+      mock_bar = mock
+      mock_bar.expects(:baz).with(:an_arg).returns mock_baz
+      @foo.expects(:bar).with(:an_arg).returns mock_bar
+      
+      assert_equal 'bat', @foo.one_complex_delegate(:an_arg)
+    end
+    
+    def test_should_call_delegate_with_no_arguments_and_return_nil_if_delegate_has_nil_intermediate_value
+      mock_bar = mock
+      mock_bar.expects(:baz).with().returns nil
+      @foo.expects(:bar).with().returns mock_bar
+      
+      assert_nil @foo.one_complex_delegate
+    end
+    
+    def test_should_call_delegate_with_one_argument_and_return_nil_if_delegate_has_nil_intermediate_value
+      mock_bar = mock
+      mock_bar.expects(:baz).with(:an_arg).returns nil
+      @foo.expects(:bar).with(:an_arg).returns mock_bar
+      
+      assert_nil @foo.one_complex_delegate(:an_arg)
+    end
+    
+  end
+  
+  class MethodWithOneComplexVariableDelegate < Test::Unit::TestCase
+    
+    class Foo
+      
+      include ForwardableChain
+      
+      def_instance_delegator_chain :one_complex_delegate, :@bar => {:@baz => :@bat}
+      
+    end
+    
+    def setup
+      @foo = Foo.new
+    end
+    
+    def test_should_access_delegate
+      mock_baz = mock
+      mock_baz.expects(:instance_variable_get).with(:@bat).returns 'bat'
+      mock_bar = mock
+      mock_bar.expects(:instance_variable_get).with(:@baz).returns mock_baz
+      @foo.expects(:instance_variable_get).with(:@bar).returns mock_bar
+      
+      assert_equal 'bat', @foo.one_complex_delegate
+    end
+    
+    def test_should_access_delegate_and_return_nil_if_delegate_has_nil_intermediate_value
+      mock_bar = mock
+      mock_bar.expects(:instance_variable_get).with(:@baz).returns nil
+      @foo.expects(:instance_variable_get).with(:@bar).returns mock_bar
+      
+      assert_nil @foo.one_complex_delegate
+    end
+    
+  end
+  
+  class MethodWithTwoJaggedComplexMethodDelegates < Test::Unit::TestCase
+    
+    class Foo
+      
+      include ForwardableChain
+      
+      def_instance_delegator_chain :two_jagged_complex_delegates, {:bar => {:baz => :bat}},
+                                                                  {:pwop => {:ding => {:dit => :dat}}}
+      
+    end
+    
+    def setup
+      @foo = Foo.new
+    end
+    
+    def test_should_call_only_first_delegate_with_no_arguments
+      mock_baz = mock
+      mock_baz.expects(:bat).with().returns 'bat'
+      mock_bar = mock
+      mock_bar.expects(:baz).with().returns mock_baz
+      @foo.expects(:bar).with().returns mock_bar
+      
+      assert_equal 'bat', @foo.two_jagged_complex_delegates
+    end
+    
+    def test_should_call_only_first_delegate_with_one_argument
+      mock_baz = mock
+      mock_baz.expects(:bat).with(:an_arg).returns 'bat'
+      mock_bar = mock
+      mock_bar.expects(:baz).with(:an_arg).returns mock_baz
+      @foo.expects(:bar).with(:an_arg).returns mock_bar
+      
+      assert_equal 'bat', @foo.two_jagged_complex_delegates(:an_arg)
+    end
+    
+    def test_should_call_both_delegates_with_no_arguments_and_return_nil_if_both_delegates_have_nil_intermediate_values
+      mock_bar = mock
+      mock_bar.expects(:baz).with().returns nil
+      @foo.expects(:bar).with().returns mock_bar
+      
+      mock_ding = mock
+      mock_ding.expects(:dit).with().returns nil
+      mock_pwop = mock
+      mock_pwop.expects(:ding).with().returns mock_ding
+      @foo.expects(:pwop).with().returns mock_pwop
+      
+      assert_nil @foo.two_jagged_complex_delegates
+    end
+    
+    def test_should_call_both_delegates_with_one_argument_and_return_nil_if_both_delegates_have_nil_intermediate_values
+      mock_bar = mock
+      mock_bar.expects(:baz).with(:an_arg).returns nil
+      @foo.expects(:bar).with(:an_arg).returns mock_bar
+      
+      mock_ding = mock
+      mock_ding.expects(:dit).with(:an_arg).returns nil
+      mock_pwop = mock
+      mock_pwop.expects(:ding).with(:an_arg).returns mock_ding
+      @foo.expects(:pwop).with(:an_arg).returns mock_pwop
+      
+      assert_nil @foo.two_jagged_complex_delegates(:an_arg)
+    end
+    
+  end
+  
+  class MethodWithTwoJaggedComplexVariableDelegates < Test::Unit::TestCase
+    
+    class Foo
+      
+      include ForwardableChain
+      
+      def_instance_delegator_chain :two_jagged_complex_delegates, {:@bar => {:@baz => :@bat}},
+                                                                  {:@pwop => {:@ding => {:@dit => :@dat}}}
+      
+    end
+    
+    def setup
+      @foo = Foo.new
+    end
+    
+    def test_should_access_only_first_delegate
+      mock_baz = mock
+      mock_baz.expects(:instance_variable_get).with(:@bat).returns 'bat'
+      mock_bar = mock
+      mock_bar.expects(:instance_variable_get).with(:@baz).returns mock_baz
+      @foo.expects(:instance_variable_get).with(:@bar).returns mock_bar
+      
+      assert_equal 'bat', @foo.two_jagged_complex_delegates
+    end
+    
+    def test_should_access_both_delegates_and_return_nil_if_both_delegates_have_nil_intermediate_values
+      mock_bar = mock
+      mock_bar.expects(:instance_variable_get).with(:@baz).returns nil
+      @foo.expects(:instance_variable_get).with(:@bar).returns mock_bar
+      
+      mock_ding = mock
+      mock_ding.expects(:instance_variable_get).with(:@dit).returns nil
+      mock_pwop = mock
+      mock_pwop.expects(:instance_variable_get).with(:@ding).returns mock_ding
+      @foo.expects(:instance_variable_get).with(:@pwop).returns mock_pwop
+      
+      assert_nil @foo.two_jagged_complex_delegates
+    end
+    
+  end
+  
+end
