Skip to content

Instantly share code, notes, and snippets.

@ctm
Created September 10, 2015 14:42
Show Gist options
  • Save ctm/9b553a582938364dfca6 to your computer and use it in GitHub Desktop.
Save ctm/9b553a582938364dfca6 to your computer and use it in GitHub Desktop.
#! /usr/bin/env ruby
require 'rspec'
require 'forwardable'
# This is a cleaner implementation of Forwardable. It does not use string evaluation,
# nor does it have any rescues in it. This code grew out of a discussion at RubiABQ
# http://www.meetup.com/Rubyists-in-Albuquerque/events/225108828/
module MyForwardable
def def_delegator(accessor, method, ali = method)
delegator_method =
case accessor
when /\A@/
:def_ivar_delegator
when /\A[A-Z]/ # TODO: There's a better way to detect if a token is a constant
# but this is fine for a proof-of-concept
:def_const_delegator
else
fail "Accessor #{accessor.inspect} is not an ivar or constant"
end
send delegator_method, accessor, method, ali
end
private
# ivars are looked up on self, using instance_variable_get
def def_ivar_delegator(accessor, method, ali)
def_delegator_helper true, :instance_variable_get, accessor, method, ali
end
# constants are looked up on self's class, using const_get
def def_const_delegator(accessor, method, ali)
def_delegator_helper false, :const_get, accessor, method, ali
end
# if +source_is_self+ is true, we look for the delegate source in self. Otherwise
# we look for it in self's class. +delegate_getter+ is the name of the method to
# retrive the delegate itself.
def def_delegator_helper(source_is_self, delegate_getter, accessor, method, ali)
# If we've extended a class, then we use define_method, otherwise we use
# define_singleton_method. Another (better?) way to assign method_definer would
# be to use respond_to?(:define_singleton_method)
method_definer = is_a?(Class) ? :define_method : :define_singleton_method
__send__(method_definer, ali) do |*args, &block|
delegate_source = source_is_self ? self : self.class
delegate = delegate_source.__send__(delegate_getter, accessor)
delegate.__send__ method, *args, &block
end
end
end
# Now lets test both the original Forwardable and MyForwardable
[Forwardable, MyForwardable].each do |module_to_test|
RSpec.describe module_to_test do
describe '#def_delagator' do
# This is basically a translation of the example code in the Forwardable
# documentation. It is changed slightly because we want to use the same
# code both with Forwardable and MyForwardable, so we use Class.new to
# create the class and then extend by module_to_test.
describe 'with ivar from class' do
let(:klass) do
Class.new.tap do |klass|
klass.class_eval do
attr_accessor :records
extend module_to_test
def_delegator :@records, :[], :record_number
end
end
end
let(:r) do
klass.new.tap do |instance|
instance.records = [4, 5, 6]
end
end
it 'works with an alias' do
expect(r.record_number(0)).to eq 4
end
end
# This is basically a translation of another example from the Forwardable
# documentation.
#
# It might be tempting to quit with just these two examples, but that would
# be misleading, because the first test pairs ivar with class and this one
# pairs constant with instance, but it's still possible to pair ivar with
# instance (even though that's kind of unlikely) and class with constant.
describe 'with constant from instance' do
let(:my_hash) do
Hash.new.tap do |h|
h.extend module_to_test
h.def_delegator "STDOUT", "puts"
end
end
let(:message) { "Howdy!" }
it 'forwards' do
expect(STDOUT).to receive(:puts).with(message)
my_hash.puts message
end
end
# Yes, it's possible for a string to have ivars. It's unlikely, but
# we can make it happen and then test to make sure we can forward to it.
describe 'with ivar from instance' do
let(:ivar_contents) { double('ivar contents') }
let(:method) { :a_method }
let(:args) { %(this is a test) }
let(:crazy_string) do
"now with more ivars".tap do |s|
ivc = ivar_contents
s.instance_eval { @an_ivar = ivc }
s.extend module_to_test
s.def_delegator '@an_ivar', method, :ali
end
end
it 'forwards' do
expect(ivar_contents).to receive(method).with(*args)
crazy_string.ali *args
end
end
# Final test of (ivar, constant) x (instance, class)
describe 'with constant from class' do
let(:constant_contents) { double('constant contents') }
let(:klass) do
Class.new.tap do |klass|
contents = constant_contents
klass.class_eval do
const_set(:CLASS_CONSTANT, contents)
extend module_to_test
def_delegator 'CLASS_CONSTANT', :a_method
end
end
end
let(:instance) { klass.new }
it 'forwards' do
expect(constant_contents).to receive(:a_method)
instance.send :a_method
end
end
end
end
end
# The above code has been tested via:
#
# bash-3.2$ time rspec forwardable.rb
# ........
#
# Finished in 0.00724 seconds (files took 0.06759 seconds to load)
# 8 examples, 0 failures
#
#
# real 0m0.130s
# user 0m0.110s
# sys 0m0.018s
#
# Using ruby 2.2.3p173 and RSpec 3.3.2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment