Meta Programming Help


#1

I have the following two methods:

def ProductFile::find_images(mode, prod_id)
# convert to symbol in case it is not (most commonly it may be a
String)
mode = mode.to_sym
case mode
when :all, :first
ProductFile::find(mode, :conditions => [“product_id = ? AND
file_type LIKE ?”, prod_id, “image%”])
end
end

def ProductFile::find_documents(mode, prod_id)
mode = mode.to_sym
case mode
when :all, :first
ProductFile::find(mode, :conditions => [“product_id = ? AND
(file_type LIKE ? or file_type LIKE ?)”, prod_id, “%pdf”, “%msword”])
end
end

Is it possible to create one meta-method that contains the stub for
these methods? I’m NOT looking for something like:

def ProductFile::find_by_type(type, mode, prod_id)
//code snip
end

Perhaps something using the define_method feature?


#2

Try this out. The “make_find :documents” and “make_find :images” calls
at
the bottom will create the class methods you want.

The sql used is a little different since I didn’t think you needed to
use
the parameters for the document types. If you want to extend it that
way,
feel free :wink:

It also could be a little shorter but I thought clarity was more
important.

Begin here

class ProductFile < ActiveRecord::Base

Makes a method named find_item, which will take a product_id and mode

argument. The method

will use the file_types list given to generate SQL which matches

against
those types and the product_id

Usage example:

make_find :documents, “pdf”, “msword”

make_find :images, “image%”

def self.make_find(item, *file_types)
sql = “product_id = ? "
if file_types.length > 0
sql << " AND (”
sql << file_types.inject("") do |str, file_type|
str << " OR " unless str.empty?
str << “file_type LIKE ‘#{file_type}’”
end
sql << “)”
end

We want the method created to ‘live’ in ProductFile as a class

method,

so we must actually create the method in the meta-class of

ProductFile.
If

we used ProductFile directly, we would create an instance method.

meta = class << self; self; end;

have to use send here because define_method is private

meta.send(:define_method, “find_#{item}”) do |mode, prod_id|

For debugging, outputs method definition when you run it

puts “mode: #{mode}, prod_id #{prod_id}”
puts “sql: #{sql}”
mode = mode.to_sym
case mode
when :all, :first
self.find(mode, :conditions => [sql, prod_id])
end
end
end

Create the two methods originally requested.

make_find :documents, “%pdf”, “%msword”
make_find :images, “image%”
end


#3

Thanks for the response. I’ve been struggling with the code for a
little while, and couldn’t get it to work.

When I go into the console and try this:
=> ProductFile.find_images(:first, 1)

I get:
=> NoMethodError: undefined method `make_find’ for ProductFile:Class

I think that this might be the issue:

make_find :documents, “%pdf”, “%msword”
make_find :images, “image%”

I wasn’t sure where to put these calls, so I put them in the
ProductFile class. Is this where it goes?

I would like to get some background into meta-programming. Are there
any resources you can recommend? I would especially like to understand
this:


#4

Copy all the code I sent into your ProductFile class (except the “class
ProductFile” bit, of course). That code should be able to be added to
the
class and work. Just make sure to remove your existing find_images and
find_documents methods.

As for the meta programming bit

meta = class << self; self; end;

That’s a little Ruby idiom that gets you the “class of the class.” It’s
a
tough concept to explain, but it’s critical to know when adding methods
to
classes or doing other metaprogramming tricks. Check out Dwemthy’s Array
at

http://poignantguide.net/dwemthy/

and http://www.ruby-garden.org for more info (just search for
“metaprogramming” or “singleton class”). Dwemthy’s Array is bizarre, to
say
the least, but if you spend some time with it it’s very rewarding.


#5

Ok. It works. I was under the (false) impression that I could put the
make_find calls anywhere. It turns out that, as in your code, I needed
to add them underneath the definition for make_find.

However, in the end, I chose to go with a different implementation. In
ProductFiles, I did the following:

def ProductFile::find_by_file_type(f_type, mode, prod_id)
f_type = f_type.to_sym
case f_type
when :images
sql_where = [“product_id = ? AND file_type LIKE ‘image%’”,
prod_id]
when :documents
sql_where = [“product_id = ? AND (file_type LIKE ‘%pdf’ OR
file_type LIKE ‘%msword’)”, prod_id]
end

unless sql_where.nil?
  mode = mode.to_sym
  case mode
  when :all, :first
    ProductFile::find(mode, :conditions => sql_where)
  end
end

end

and in my Product model, I did this:

def Product::make_find(name)
define_method(“find_#{name}”) do |mode|
mode.to_sym
case mode
when :all, :first
ProductFile.find_by_file_type(mode, name, self.id)
end
end
end

make_find “images”
make_find “documents”

I am tempted to use the missing_method method, but I believe that it
is used by ActiveRecord, which Product inherits from. I don’t want to
override the ActiveRecord definition, so I’m leaving it alone for now.
Is there a better way to do this?

Thanks!

  • Nithin

#6

def method_missing(sym, *args, &blk)
if sym.to_s == “make_images”

your code …

else

Call parent class (i.e. ActiveRecord)

super(sym, *args, &blk)
end
end

Thanks!

I also think you may be over-engineering this a bit - why not just call into
ProductFiles::find_by_file_type directly - but that’s just my opinion :wink:

You may be right, but unless I can do something like
ProductFiles::find_by_file_type(/image/) or
ProductFiles::find_by_file_type(/(msword|pdf)/), then it won’t work
for me.

  • Nithin

#7

I am tempted to use the missing_method method, but I believe that it
is used by ActiveRecord, which Product inherits from. I don’t want to
override the ActiveRecord definition, so I’m leaving it alone for now.
Is there a better way to do this?

If you define your own method_missing, you can call into ActiveRecords
just
by using the “super” keyword. For example,

def method_missing(sym, *args, &blk)
if sym.to_s == “make_images”

your code …

else

Call parent class (i.e. ActiveRecord)

super(sym, *args, &blk)
end
end

I also think you may be over-engineering this a bit - why not just call
into
ProductFiles::find_by_file_type directly - but that’s just my opinion :wink:


#8

I believe this is the case of a solution looking for a problem. I was
looking at the code for the file_column plugin and was confused /
awe-struck by all the meta-programming that was going on. So, in an
effort to learn, I decided to implement some the magic into my
project. Of course, I should probably KISS.