-
-
Save bogdanRada/2567625052fd3b4312863d42e7d74d46 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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