Forum: Ruby on Rails Meta Programming Help

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Nithin R. (Guest)
on 2006-03-03 23:30
(Received via mailing list)
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?
Justin B. (Guest)
on 2006-03-04 00:16
(Received via mailing list)
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 ;)

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
Nithin R. (Guest)
on 2006-03-04 04:36
(Received via mailing list)
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:
Justin B. (Guest)
on 2006-03-04 20:24
(Received via mailing list)
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.
Nithin R. (Guest)
on 2006-03-09 01:45
(Received via mailing list)
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
Justin B. (Guest)
on 2006-03-09 02:00
(Received via mailing list)
>
> 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 ;)
Nithin R. (Guest)
on 2006-03-09 19:01
(Received via mailing list)
> 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 ;)
>

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
Nithin R. (Guest)
on 2006-03-09 19:10
(Received via mailing list)
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.
This topic is locked and can not be replied to.