kreek
kreek

Reputation: 8834

Conditionally call chained method

A chained method should only be called under certain circumstances in the following code.

class Klass
  def foo
    puts 'foo'
    self
  end
  def bar
    puts 'bar'
    self
  end
end

klass = Klass.new
a = 2
id = klass.foo{conditionally chain bar if a == 2}.bar

Can you insert an expression or method between chained methods that conditionally continues or halts the method chain?

Upvotes: 3

Views: 3891

Answers (3)

Gorodscy Fernando
Gorodscy Fernando

Reputation: 191

You can use tap if the block you are chaining is mutable, i.e: It will change the value of self (as explained here) However, if you are chaining immutable blocks like QueryMethods, you can use try like:

data
  .try { |d| group ? d.group(group) : d }
  .try { |d| group ? d.order(order => :desc) : d }

try with nothing else but a block is essentially:

def try
  yield self
end

(reference: #try)

Upvotes: 1

Damiano Stoffie
Damiano Stoffie

Reputation: 897

This is simple and who will come after you will understand immediately:

klass = klass.foo
klass = klass.bar if a == 2
etc...

This works well if the chained methods take no arguments

klass.define_singleton_method :chain_if do |b, *s|
  return unless b
  klass = self
  s.each do |x|
    klass = klass.send x
  end
  klass
end

klass.foo.chain_if(true, :foo, :bar).chain_if(false, :bar)

Here some duplicated threads!

conditional chaining in ruby

Add method to an instanced object

Here I found another solution that I personally like:

my_object.tap{|o|o.method_a if a}.tap{|o|o.method_b if b}.tap{|o|o.method_c if c}

EDIT:

beware tap is defined as follows:

class Object
  def tap
    yield self
    self
  end
end

What you need might look like this, if the chained method returns a new immutable object:

class Object
  def tap_and_chain
    yield self
  end
end

Upvotes: 5

Cary Swoveland
Cary Swoveland

Reputation: 110675

def chain_maybe(klass, condition, *args)
  args[1..-1].reduce(klass.send(args.first)) { |r,m| (condition && r.send(m)) || r }
end

or:

def chain_maybe(klass, condition, *args)
  first, *others = args
  others = [] unless condition
  others.reduce(klass.send(first)) { |r,m| r.send(m) }
end

For:

class Klass
  def foo
    puts 'foo'
    self
  end

  def bar
    puts 'bar'
    self
  end

  def baz
    puts 'baz'
    self
  end
end

c = Klass.new

chain_maybe(c, true, :foo, :bar, :baz)
foo
bar
baz
  #=> #<Klass:0x007fccea8da388> 
chain_maybe(c, false, :foo, :bar, :baz)
foo
  #=> #<Klass:0x007fccea8da388> 
chain_maybe(c, true, :foo, :bar)
foo
bar
  #=> #<Klass:0x007fccea8da388>
chain_maybe(c, true, :bar, :baz)
bar
baz
  #=> #<Klass:0x007fccea8da388> 

If there is to be a condition for each argument, this can be generalized to:

def chain_maybe(klass, conditions, args)
  return nil if conditions.empty?
  first, *others = args.zip(conditions).select(&:last).map(&:first)
  others.reduce(klass.send(first)) { |r,m| r.send(m) }
end

args = [:foo, :bar, :baz]
chain_maybe(c, [true,  true,  true], args)
foo
bar
baz
  #=> #<Klass:0x007fccea8da388> 
chain_maybe(c, [false, true,  true], args)
bar
baz
  #=> #<Klass:0x007fccea8da388> 
chain_maybe(c, [true,  false, true], args)
foo
baz
  #=> #<Klass:0x007fccea8da388> 

Upvotes: 1

Related Questions