Casteurr
Casteurr

Reputation: 956

Ruby define_method

I have the following test which I must pass:

def test_can_find_by_arbitrary_fields
  assert @library.respond_to? :find_by_artist
  assert [email protected]_to?(:find_by_bitrate)
  @library.add_song({ :artist => 'Green Day',
                     :name => 'American Idiot',
                     :bitrate => 192 })
  assert @library.respond_to?(:find_by_bitrate)
end

and I am not sure how I can do it.

I tried doing:

def respond_to?(method)
     if self.public_methods.include? method
         true
     elsif (method == :find_by_bitrate)
         define_method :find_by_bitrate, ->(default = nrb)  { @songs.select |a| a[:bitrate] == nrb }       
         false
     else
         false
end

but it says "define_method is undefined". Are there any ways I can define the find_by_bitrate method?

Upvotes: 1

Views: 827

Answers (3)

Dave Newton
Dave Newton

Reputation: 160170

You may define methods the first time they're called in method_missing.

Whether or not you should is open to some debate, but it's a better option than respond_to?.

class Foo
  def method_missing(sym)
    puts "Method missing; defining."
    self.class.send(:define_method, sym) do
      puts "Called #{sym}."
    end
  end
end

Sanity check:

f = Foo.new
=> #<Foo:0x007fa6aa09d3c0>
f.wat
=> Method wat missing; defining.
f.wat
=> Called wat.
f2 = Foo.new
=> Called wat.

Upvotes: 3

dbenhur
dbenhur

Reputation: 20408

There's a lot of info missing to properly answer this. The test implies that find_by_artist is always defined even when @library is empty, but that there are dynamic methods available on other attributes (eg: bitrate) that are valid only when library contains a record with such a method.

One should not redefine respond_to? in any case. There is an explicit hook method for answering respond_to? for dynamic methods: Object#respond_to_missing?.

So a simple way to make your test pass is to be sure the @library object has a concrete method #find_by_artist and a respond to hook that checks whether any of it's elements a have the requested attribute. If I assume @library is a collection object Library which keeps an enumeration of songs in @songs

class Library
  def find_by_artist artist
    @songs.select { |song| song['artist'] == artist }
  end

  def method_missing meth, arg
    m = /^find_by_(.+)$/.match meth.to_s
    return super unless attr = m && m[1]

    @songs.select { |song| song[attr] ==  arg }
  end

  def respond_to_missing? meth, include_private
    m = /^find_by_(.+)$/.match meth.to_s
    return super unless attr = m && m[1]

    @songs.any? { |song| song.has_key? attr }
  end
end

This has a performance problem in that respond_to? now incurs a search of all the songs. One could optimize by keeping a set of the union of all attributes contained in @songs and updating it in methods which add/update/delete elements in the collection.

Upvotes: 1

szeryf
szeryf

Reputation: 3387

I don't think you should be redefining respond_to? method. The point of the test is (probably) that the @library object should have a find_by_artist method defined and no find_by_bitrate until you add a song with a bitrate. I.e. the add_song method should define method find_by_bitrate when it sees a song with a bitrate (?).

Also, define_method is a private method of Class. Above, you're trying to call it from an instance method. See "Ruby: define_method vs. def", there's more on this stuff.

Upvotes: 1

Related Questions