Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method chaining in ruby

I want to build an API client that has an interface similar to rails active record. I want the consumers to be able to chain methods and after the last method is chained, the client requests a url based on the methods called. So it's method chaining with some lazy evaluation. I looked into Active Record but this is very complicated (spawning proceses, etc).

Here is a toy example of the sort of thing I am talking about. You can chain as many 'bar' methods together as you like before calling 'get', like this:

puts Foo.bar.bar.get # => 'bar,bar'
puts Foo.bar.bar.bar.get # => 'bar,bar,bar'

I have successfully implemented this, but I would rather not need to call the 'get' method. So what I want is this:

puts Foo.bar.bar # => 'bar,bar' 

But my current implementation does this:

puts Foo.bar.bar #=> [:bar, :bar]

I have thought of overriding array methods like each and to_s but I am sure there is a better solution.

How would I chain the methods and know which was the last one so I could return something like the string returned in the get method?

Here is my current implementation:

#!/usr/bin/env ruby

class Bar
  def get(args)
    # does a request to an API and returns things but this will do for now.
    args.join(',') 
  end
end

class Foo < Array
  def self.bar
    @q = new
    @q << :bar
    @q
  end

  def bar
    self << :bar
    self
  end

  def get
    Bar.new.get(self)
  end
end

Also see: Ruby Challenge - Method chaining and Lazy Evaluation

like image 842
Rimian Avatar asked Feb 13 '23 02:02

Rimian


1 Answers

How it works with activerecord is that the relation is a wrapper around the array, delegating any undefined method to this internal array (called target). So what you need is to start with a BasicObject instead of Object:

class Foo < BasicObject

then you need to create internal variable, to which you will delegate all the methods:

  def method_missing(*args, &block)
    reload! unless loaded?
    @target.send(*args, &block)
  end

  def reload!
    # your logic to populate target, e.g:
    @target = @counter
    @loaded = true
  end

  def loaded?
    !!@loaded
  end

To chain methods, your methods need to return new instance of your class, e.g:

def initialize(counter=0)
  @counter = counter
end

def bar
  _class.new(@counter + 1)
end

private

# BasicObject does not define class method. If you want to wrap your target 
# completely (like ActiveRecord does before rails 4), you want to delegate it 
# to @target as well. Still you need to access the instance class to create 
# new instances. That's the way (if there are any suggestion how to improve it,
# please comment!)
def _class
  (class << self; self end).superclass
end

Now you can check it in action:

p Foo.new.bar.bar.bar      #=> 3
(f = Foo.new) && nil       # '&& nil' added to prevent execution of inspect             
                           # object in the console , as it will force @target 
                           # to be loaded

f.loaded?                  #=> false
puts f                     #=> 0
f.loaded?                  #=> true
like image 135
BroiSatse Avatar answered Feb 24 '23 06:02

BroiSatse