Reputation: 5555
There has to be a built in way of doing this, right?
class Object
def send_chain(arr)
o=self
arr.each{|a| o=o.send(a) }
return o
end
end
Upvotes: 35
Views: 17829
Reputation: 8417
I built upon the previous responses and arrived at a module that can be mixed into class
es and Struct
s, plus some rspec
tests.
The rspec
tests show how to use the module.
sendchain.rb
:
# Supports one chain at a time
module SendChain
# This method can be called directly if no methods in the chain require arguments
# Does not use any external state
def send_chain(chain)
Array(chain).inject(self) { |o, a| o.send(*a) }
end
# Saves @chain structure containing :placeholders for arguments to be supplied later
# Call when a different chain with :placeholders is desired
def new_chain(chain)
abort "new_chain error: chain must be an array ('#{chain}' was an #{chain.class.name})" \
unless chain.instance_of?(Array)
@chain = chain
end
# Call after new_chain, to evaluate @chain with values
def substitute_and_send_chain_with(values)
send_chain substitute_chain_with values
end
# Call this method after calling new_chain to perform error checking and replace :placeholders with values.
# @chain is not modified.
# @return [Array] Modified chain
def substitute_chain_with(values)
values = [values] unless values.instance_of?(Array)
placeholder_count = @chain.flatten.count { |x| x == :placeholder }
if values.length != placeholder_count
abort "with_values error: number of values (#{values.length}) does not match the number of placeholders (#{placeholder_count})"
end
eval_chain @chain, values
end
private
# Replaces :placeholders with values
# Does not use any external state
# @return modified chain
def eval_chain(chain, values)
chain.map do |c|
case c
when :placeholder
values.pop
when Array
eval_chain c, values
else
c
end
end
end
end
send_chain_spec.rb
:
require 'spec_helper'
require_relative '../lib/util/send_chain'
LruFile = Struct.new(:url, :page) do
include SendChain
end
RSpec.describe(LruFile) do
lru_file = described_class.new 'abc', 'def'
it 'performs simple call if no arguments are required' do
# Equivalent to: lru_file.url.reverse
actual = lru_file.send_chain %i[url reverse]
expect(actual).to eq('cba')
end
it 'can accept a scalar argument in stages' do
lru_file.new_chain [:url, %i[end_with? placeholder]]
# Equivalent to: lru_file.url.end_with?('bc')
substituted_chain = lru_file.substitute_chain_with 'bc'
actual = lru_file.send_chain substituted_chain
expect(actual).to be true
end
it 'can accept a vector argument in stages' do
lru_file.new_chain [:url, %i[end_with? placeholder]]
# Next 2 lines are equivalent to: lru_file.url.end_with?('bc')
substituted_chain = lru_file.substitute_chain_with ['bc']
actual = lru_file.send_chain substituted_chain
expect(actual).to be true
end
it 'can accept a scalar argument in one stage' do
lru_file.new_chain [:url, %i[end_with? placeholder]]
# Equivalent to: lru_file.url.end_with?('bc')
actual = lru_file.substitute_and_send_chain_with 'bc'
expect(actual).to be true
end
it 'can accept an array argument in one stage' do
lru_file.new_chain [:url, %i[end_with? placeholder]]
# Equivalent to: lru_file.url.end_with?('bc')
actual = lru_file.substitute_and_send_chain_with ['bc']
expect(actual).to be true
end
it 'can reuse the chain with different values' do
lru_file.new_chain [:url, %i[end_with? placeholder]]
# Equivalent to: lru_file.url.end_with?('bc')
actual = lru_file.substitute_and_send_chain_with 'bc'
expect(actual).to be true
# Equivalent to: lru_file.url.end_with?('abc')
substituted_chain = lru_file.substitute_chain_with ['abc']
actual = lru_file.send_chain substituted_chain
expect(actual).to be true
# Equivalent to: lru_file.url.end_with?('de')
actual = lru_file.substitute_and_send_chain_with 'de'
expect(actual).to be false
end
end
Upvotes: 0
Reputation: 7062
How about this versatile solution without polluting the Object
class:
def chain_try(arr)
[arr].flatten.inject(self_or_instance, :try)
end
or
def chain_send(arr)
[arr].flatten.inject(self_or_instance, :send)
end
This way it can take a Symbol
, a String
or an Array
with a mix of both even.🤔
example usage:
chain_send([:method1, 'method2', :method3])
chain_send(:one_method)
chain_send('one_method')
Upvotes: 0
Reputation: 10796
Building upon previous answers, in case you need to pass arguments to each method, you can use this:
def send_chain(arr)
Array(arr).inject(self) { |o, a| o.send(*a) }
end
You can then use the method like this:
arr = [:to_i, [:+, 4], :to_s, [:*, 3]]
'1'.send_chain(arr) # => "555"
This method accepts single arguments as well.
Upvotes: 10
Reputation: 716
I just ran across this and it really begs for inject:
def send_chain(arr)
arr.inject(self) {|o, a| o.send(a) }
end
Upvotes: 70
Reputation: 14983
No, there isn't a built in way to do this. What you did is simple and concise enough, not to mention dangerous. Be careful when using it.
On another thought, this can be extended to accept arguments as well:
class Object
def send_chain(*args)
o=self
args.each do |arg|
case arg
when Symbol, String
o = o.send arg # send single symbol or string without arguments
when Array
o = o.send *arg # smash the inner array into method name + arguments
else
raise ArgumentError
end
end
return o
end
end
this would let you pass a method name with its arguments in an array, like;
test = MyObject.new
test.send_chain :a_method, [:a_method_with_args, an_argument, another_argument], :another_method
Upvotes: 8