Friday, January 16, 2009

Ack in Project Skipping Rake

Ack in Project is probably the TextMate bundle I use the most (after the Ruby bundle, I suppose, which I constantly use without even thinking about it). If you haven't already, you should install it immediately for very fast project searches (and recursive searches of just the selected directory, which on its own is hugely useful).

What it took me a while to realize, though, is that my project's Rake files weren't being searched. It turns out this is because Ack will by default search every file of a known type and ignore everything else. Ack knows Ruby as .rb, .rhtml, .rjs, .rxml and .erb, but that's all by default. (You can see what extensions are associated with what types from the command line with ack --help types. This assumes you've got ack installed on your path somewhere.)

To teach Ack about new extensions or entirely new types, create an .ackrc file in your home directory and fill it with things like this:

# Add the .rake extension to the existing ruby type.
--type-add
ruby=.rake

# Create a new type for Clojure source files.
--type-set
clojure=.clj

(type-set can also be used to completely replace a built in type definition. Read about this and all sorts of other ack options from the command line with ack --man.)

In addition to getting .ackrc in place, you also have to tell the TextMate bundle that you want to use it. From inside TextMate do an Ack in Project, hit the "Advanced Options" twisty, and check "load defaults from .ackrc." You're all set.

It may have occurred to you that your top-level Rakefile doesn't have an extension. Likewise for the executables in your Rails projects' script directory. And your Capfile.

Damn.

Luckily, Ack knows unix-ey people like dropping extensions from executables and using a shebang line to tell the shell how to run them. So any file with an unrecognized extension (or none at all) will be checked for a shebang line that might qualify it for the perl, ruby, php, python, shell or xml types!

So that takes care of RAILS_ROOT/script/*. As far as Rakefile and Capfile ... well, the shebang line would really be a lie, since they're not executable without the supporting Rake and Capistrano libraries loaded, but the workaround of adding the shebang will get them into your Ack results. I leave it to you to decide whether the lying shebang or excluded files is a lesser evil, and promise to let you know if I find a better way to get Ack to consider them Ruby.

Thanks for reading!

Sunday, January 04, 2009

Show Me Your Meta

I'm working on a presentation on Ruby metaprogramming, and I'd love to have more examples from the outside world. If there's any piece of metaprogramming you've seen in open-source code that you thought was particularly clever, readable, convoluted, or gratuitous, please comment or email with links to the source (or at least a pointer in the general direction).

If you have some non-public-source example you think is interesting, please feel free to email me along with information about how I can use the example (and whether you want "credit" for supplying it).

Thanks!

Friday, November 07, 2008

ConstantizeAttribute to support Renum on Rails

The most obvious shortcoming of Renum is that there hasn't been any clean way to use enumerated values as ActiveRecord attribute values. I've finally fixed that.

There were a couple of Rails features that seemed like they might be helpful but turned out not to be.

Rails' built-in serialize macro class method uses YAML to store items, which is fine for arrays and hashes but hideous for anything else (if you ever look at your database directly). Though YAML serialization worked on Renum-created enumerated values, deserializing a value created a new instance. That instance might turn out to work fine for your needs, but it wouldn't actually be the instance it ought to be (i.e., the one the constant points to), so it might surprise and disappoint you in subtle ways. The other huge downside to serializing the instance (and its instance variables) is that a change in the encapsulated contents of the enumerated value could break things.

Rails' built-in composed_of macro class method could have been made to work, but it wouldn't have been pretty. The default handling would have required Renum to redefine new in generated enumeration classes to do a lookup instead of allocating and initializing an instance. The latest releases of Rails provide :constructor and :converter options that would have allowed me to avoid messing with new, but it still would've been ugly, not to mention requiring a very recent version of Rails.

What I finally realized is that the beauty of constants is that I didn't need to write a lookup mechanism: Ruby already does that. All I needed to do was store the name of the constant when writing the attribute and constantize it when reading to get the proper value. I also realized that approach is in no way tied to Renum: It would also allow classes and modules to be attribute values, which could be helpful if, for example, you have a module to mix in or a service class to call based on some reference data.

So I wrote a tiny little Rails plugin called ConstantizeAttribute that does this for you. This example pretty much says it all:

# ... your migration ...
create_table :robots do |t|
  t.column :behavior_module, :string
end

# ... your model ...
class Robot < ActiveRecord::Base
  constantize_attribute :behavior_module
end

# ... some classes or modules you want to store as attribute values ...
module RobotBehaviors
  module Handy
    def self.encounter
      "Is there anything I can help you with?"
    end
  end

  module Evil
    def self.encounter
      "I will destroy all humans."
    end
  end
end

# ... so now,
robby = Robot.create :behavior_module => RobotBehaviors::Evil

# Now "RobotBehaviors::Evil" is in the behavior_module column.

robby.behavior_module.encounter # => "I will destroy all humans."

robby.update_attribute :behavior_module, RobotBehaviors::Handy

# Now "RobotBehaviors::Handy" is in the behavior_module column.

robby = Robot.find :first

robby.behavior_module.encounter # => "Is there anything I can help you with?"

Install ConstantizeAttribute with

script/plugin install git://github.com/duelinmarkers/constantize_attribute.git

if your Rails is recent enough to install from git or grab a copy of the repo manually and drop the plugin in place. (There's no install script to worry about.)

The cool thing about this is that it works with old version of Renum without any changes, and it all happens in only about 15 lines of code.