Zando
Zando

Reputation: 5555

Ruby: How to chain multiple method calls together with "send"

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

Answers (5)

Mike Slinn
Mike Slinn

Reputation: 8417

I built upon the previous responses and arrived at a module that can be mixed into classes and Structs, 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

Khalil Gharbaoui
Khalil Gharbaoui

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

dgilperez
dgilperez

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

Joe
Joe

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

edgerunner
edgerunner

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

Related Questions