Specifying Merb Mailers

This helper is based on some other controller/view helpers I've been working on and planning on blogging soon, with a nod to the specs present in the merb-mailer library itself.

I'm still considering the idea of separate specs for UserMailer and its views, but I think the overhead is too much for mailers, compared to the benefits we get for regular controllers/views. I think this is a result of the way the send_mail helper functions.

in a controller

send_mail UserMailer, :hello, {
  :from => "greeter@example.com",
  :to => @person.email,
  :subject => "Greetings"
}, {
  :name => @person.name
}

The controller spec can simply stub/mock the send_mail call as appropriate.

spec/spec_helper.rb

Merb::Mailer.delivery_method = :test_send
def describe_mail(mailer, template, &block)
  describe "/#{mailer.to_s.downcase}/#{template}" do
    before :each do
      @mailer_class, @template = mailer, template
      @assigns = {}
    end

    def deliver(send_params={}, mail_params={})
      mail_params = {:from => "from@example.com", :to => "to@example.com", :subject => "Subject Line"}.merge(mail_params)
      @mailer_class.new(send_params).dispatch_and_deliver @template.to_sym, mail_params
      @mail = Merb::Mailer.deliveries.last
    end

    instance_eval &block
  end
end

spec/mailers/user_mailer_spec.rb

require File.join(File.dirname(__FILE__),'..','spec_helper')

describe_mail UserMailer, :hello do
  it "should say hello" do
    deliver :name => "Jamie"
    @mail.text.should == "Hello Jamie"
  end
end

I'm a big fan of custom rspec describers, as above. The fact that before and after blocks are transparently inherited is a huge win over test/unit, where you'd need to explicitly call super.

app/mailers/user_mailer.rb

class UserMailer < Merb::MailController
  def hello
    render_mail
  end  
end

app/mailers/views/user_mailer/hello.text.erb

Hello <%= params[:name] %>

Spec Timing

As an update to a previous article,

if $specs_timed.nil? && ENV.has_key?('SLOW')
  $specs_timed = true
  $timings = []

  Spec::Example::ExampleGroup.prepend_before do
    @start = Time.now
  end
  Spec::Example::ExampleGroup.append_after do
    elapsed = Time.now - @start
    if elapsed > ENV['SLOW'].to_f
      $timings << [elapsed, "#{self.class.description} #{description}"]
    end
  end

  at_exit do
    puts "\nSlow Specs:"
    $timings.sort{|a,b| a.first <=> b.first}.each do |time, name|
      puts " %7.4f #{name}" % time
    end
    puts "  None!" if $timings.empty?
  end
end

Then, simply run

rake SLOW=0.1

Two gotchas if you're using Rails though: instead of hooking S::E::ExampleGroup, you'll need to hook Spec::Rails::Example::RailsExampleGroup. Second, if you have any spec failures the timings don't seem to get output, since spec/rails aborts execution after failing.

Holmes on... software?

HGTV in Canada produces a tv show called Holmes on Homes that follows general contractor Mike Holmes as he visits failed renovations, provides commentary on the sorry situation of the work done, and then goes about fixing them.

I was recommended to the series by a friend of mine, who has suggested that the shows and situations very often have a correlation to the world of software development. After seeing the first four episodes, I decided he was right, that all the episodes I've seen have direct quotes that are applicable, and that I should start blogging them.

So, consider this the "front page" article on this, I'll fill in the individual episode links as I get around to them. The list of episodes is just the ones I have available to watch (on DVD or from HGTV) at the moment.

Season One

  1. Additional Grief
  2. Soggy Sorority
  3. Botched Basement
  4. Attica! Attica / Crappy Capping
  5. Flimsy Floor
  6. Kitchen Catastrophe
  7. Window Pain
  8. Faulty Showers
  9. Tiles and Tribulations
  10. Site Unseen
  11. Sweet Home Abandoned
  12. Whole House Disaster

Season Two

  1. Terrible Terrace
  2. Drafty Ducting
  3. Ramp Revamp
  4. Flooded Foundation
  5. Garage Grievance
  6. Lamin-Ain't
  7. Roof Goof
  8. Floor Fiasco
  9. Doozy Jacuzzi
  10. No Grout About It
  11. Jacking the Box
  12. Access Denied
  13. Hell's Kitchen
  14. Holmes for the Holidays

Season Five

  • Holmes Inspection
  • Showing the Cracks
  • What a Mesh

Season Six

  • Due Date
  • Frozen Assets
  • Gone to Pot
  • Lack of Truss
  • Clean Slate
  • Completely Incomplete
  • Nashville Kitchen
  • Pasadena 911
  • Shaky Foundation
  • Stone Walled
  • Third Time Lucky

Season Seven

  • Hit the Deck
  • Rocky Reno
  • Paradise Island

Specials/Unaired

  • Lien on Me

RejectConf Talk - RCov Hack

It seems that there were people making both audio and video recordings of talks at RejectConf this year.

I gave a short talk on the RCov hack I've been working on (mentioned previously) which seemed to go well. The quality of the video isn't that great, but Geoff's recording of just the audio is good. It should go along well with the slides (pdf) if anyone's curious.

Rails and Seaside?

I'm coming late to the controversy, I know. I was talking with a co-worker about Rails and Seaside the other day, and after describing the Seaside structure and philosophy compared to Rails I got to thinking that there's really not as much overlap as some people think between the two.

Rails, at least since v1.2, has a focus on information. It says, I have a bunch of knowledge I'd like to share with the world. Working with routes makes accessing that information fairly uniform, and also allows for deep linking - a reference to that piece of information that won't change. It recognizes that while it's possible to provide access to this information with simple flat files, if you want to provide dynamic views, or frequently updating data, or even provide for display customizations, Rails has facilities for getting you most of the way there.

Seaside, on the other hand, has more of a focus on the application. It provides for a workflow, and pauses in that workflow every so often to display a web page to the user. It says, I want to let you get something done, here, go to it. It provides a framework that lets you write an application similar to a desktop application, but which uses a web browser for its UI and can provide a centralized storage system for the data it manipulates.

Just looking at these, it's easy to see where one framework shines and the other would require more work to get there.

Anything working with a data-centric view or large-scale multi-user behaviour could run very well in Rails. The Blog example is ubiquitous, but also a forum, or news site, or many other applications involving user feedback and the option to deep-link to pages.

Sites with a more workflow-driven, single-user view would do well by Seaside. For example, I think doing an internet banking front-end in Seaside would be excellent. One user working through steps for a number of actions (think of paying a bill - usually 3-4 page loads in sequence), without the need to reference any specific page in the system. Users log in, and can essentially ignore the URL in the address bar for the duration of their visit.

While both kinds of applications can (and have) been done with the other framework, it seems silly to bolt on extraneous features (like meaningful URLs in Seaside, or managing serious page flow in Rails) when you could switch and play to the strengths of the framework. Given the somewhat orthogonal strengths of Seaside and Rails, I can only see the increased choice they bring as a good thing.

RPlug 0.2

Rplug has a new release up, which should now be useful for the world at large, as it has gained support for projects in subversion.

The update process now preserves the .svn turds rather than breaking the working copy, which is possible now that SourceControl has taught svn (and svk) how the manifest command should be implemented (11 lines of ruby).

I should probably do a check after I've done the export and cull any now-empty directories from the plugin dir, but that'll come in time, I'm sure.

Problems with Hoe

RPlug and SourceControl now officially have Gems out. SourceControl is probably useless for anybody at the moment, but if you are working on a rails repository under SVK and want to manage SVN-backed plugins, RPlug should handle it just fine. Just gem install rplug -y. More compatability to come in the future.

[Updates below]

I've been having problems getting SourceControl deployed, turns out (unsurprisingly) to be user error - I'm new to this whole rubyforge/gem scene.

So, for the record, prior to releasing a gem using Hoe, one needs to get rubyforge configured. For me, this wound up being:

$ rubyforge setup
$ rubyforge config rplug
$ rubyforge config sourcecontrol

After all that, SourceControl is deploying just fine.

I'm presuming that the initial problem was that the gem (and internal file structure) is source_control, but due to limitations on rubyforge the project name is sourcecontrol - somewhere along the way that confusion stopped it from working.

Today, I went mucking around with the packages for it, removed the old one named 'sourcecontrol' and added 'source_control' - removing ~/.rubyforge/auto-config.yml and re-running the rubyforge setup/config picked up the new package id, and everything seems to run just fine now.

RPlug Up and Running

Well, it's got the basic functionality it needs, so I'm about to put out a 0.1.0 gem for RPlug. It has a dependency on SourceControl, which I think only deserves a 0.0.5 release because it only does the bare minimum to support RPlug at the moment.

Both projects are entirely up in subversion if anyone wants to check them out, but they're not quite ready for public consumption at the moment.

Example usage and output follows.

% rplug install exception_logger http://svn.techno-weenie.net/projects/plugins/exception_logger svn
Recorded exception_logger, run 'rplug update' to pull the latest revision

% rplug update
Working in project dir /home/jamie/dev/redvase
Updating exception_logger...
  upgrading to revision 2733
  updating local repository
  Done.
Updating mocha...
  Done.
Updating helper_test...
  Done.
Updating arts...
  Done.
Updating liquid...
  Done.

% rplug status
Working in project dir /home/jamie/dev/redvase
Managing the following plugins:
  arts, revision 70
  exception_logger, revision 2
  helper_test, revision 85
  liquid, revision 140
  mocha, revision 99
Not Managing the following plugins:
  test_timer

% rplug update -p exception_logger -r 2563
Working in project dir /home/jamie/dev/redvase
Updating exception_logger...
  upgrading to revision 2563
  updating local repository
  Done.

For those new to the blog, I'm currently reinventing a few wheels here - RPlug is a replacement for Piston that stores meta-info in config/plugins.yml rather than the version control system, and which does not tie itself directly to Subversion even when given a compatible system (like SVK). It does this by using SourceControl (itself intended as a replacement for RSCM) to handle the interface to the SCM system.

Test Timing

Since Geoff's gem wasn't working for me, I whipped up a test timing utility based off of it.

Rather than hook into Test::Unit::TestSuite, I'm hooking into TestCase, and providing a global report via an at_exit hook. Just add the following file to your lib folder, require it from test_helper, and most of the time it will just sit there, quietly doing nothing. Call it into action by setting the environment variable TEST_TIMER with a float, and it will output the elapsed time of any test taking longer than that.

Example run:

# TEST_TIMER=0.25 rake test:units TEST=test/unit/creative_test.rb
/usr/bin/rake:17:Warning: require_gem is obsolete.  Use gem instead.
(in /home/jamie/dev/redvase)
/usr/bin/ruby1.8 -Ilib:test "/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loader.rb" "test/unit/creative_test.rb"
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loader
Started
......................................................................................
Finished in 10.116575 seconds.

86 tests, 164 assertions, 0 failures, 0 errors

Test Benchmark Results
  0.2927 CreativeTest#test_delayed_click_count_with_third_party_stats
  0.2982 CreativeTest#test_impression_count_for_date_range_with_third_party_stats_offset
  0.3240 CreativeTest#test_global_creative_stats_should_return_correct_default_values
  6.2505 CreativeTest#test_click_count

Source file, lib/test_timer.rb

if ENV.has_key? 'TEST_TIMER' and
   !Test::Unit::TestCase.method_defined? :untimed_run

  class Test::Unit::TestCase
    cattr_reader :benchmark_data
    @@benchmark_data = {}
    alias untimed_run run

    def run(result, &progress_block)
      start = Time.now
      untimed_run(result, &progress_block)
      finish = Time.now
      elapsed = finish - start
      if elapsed > ENV['TEST_TIMER'].to_f
        name =~ /(.*)((.*))/
        @@benchmark_data["#{$2}##{$1}"] = elapsed
      end
    end
  end

  # at_exit hooks run in reverse order, so in order to run after
  # Test::Unit's hook, we need to nest at_exit calls.
  at_exit do
    at_exit do
      results = Test::Unit::TestCase.benchmark_data
      unless results.empty?
        puts "\nTest Benchmark Results"
        results.sort{|a,b| a.last <=> b.last }.each do |key,value|
          puts " %7.4f #{key}" % value
        end
      end
    end
  end
end

Upgrading to 1.2, Part 1: Deprecations

So I've been spending some time lately working on upgrading the existing codebase for some projects at work such that they'll work in Rails 1.2 once it's released. Sadly, the upgrade process is not without its rough edges, and after two days of poking at it (it being a 5800 LOC app with 8800 LOC of tests) I'm still not completely done - the test run does not pass cleanly.

However, I have managed to get rid of most of the niggly deprecation warnings, so the output of the rake run is down to 450k from a high of about 2.2mb. Fun times. The changes required to silence most of the warnings are...

ActiveRecord

find_all and find_first are deprecated. Use find(:all) and find(:first) instead.

If you have a has_many which is :dependent, make sure you're specifying :destroy or :delete_all, rather than true. If you've got true you probably want to replace it with :destroy. Slower, but safer.

Routes

Just a short note here, :requirements regexps no longer accept anchors. We have something along these lines:

map.connect ':controller/:action/:foo', :requirements => { :foo => /^(bar|baz)$/ }

Such that that route only fires if the third url part is exactly bar or baz. To silence 1.2, just remove the ^ and $ anchors.

Controllers/Views

@params, @session, @request, and @flash are deprecated in controllers and views, use the version without the @. Note, don't try and change this globally in your tests, or all hell will break loose. Oddly, assigns(:flash) in a test seems to trigger the warning for accessing @flash.

If you want to have your link_to go by post instead of get, use :method => :post instead of :post => true.

(Update: This will not throw errors, but won't work in 1.1.6, so wait until you're actually running 1.2 to do this change.)

Rendering with a string (render 'template') is no longer allowed. The deprecation warning says to render :file => 'template' instead, but if you want your code to continue to work in Rails 1.1.6 you'll need to add a :use_full_path => true to the call.

start_form_tag and end_form_tag are now deprecated. The suggested replacement is to pass a block to a form_tag call, but that does not work at all in Rails 1.1.6. My preferred fix is to use a bare form_tag to start the form, and a hard-coded to finish it. Same goes with remote_form_tag for those AJAXy forms. That shuts up all the deprecation warnings, and allows for a fairly simple multiline regexp to blockify them up in the future. I was looking at something like <%= ?(remote_)?form_tag([^%]) ?%>(.?) and replacing with _<% $1form_tag$2 do %>$3<% end %>

We've got a few places where we're redirecting to a named route: redirect_to :login_url. This calls url_for, which is deprecated. I think the correct solution is to just drop the colon and redirect_to login_url, but this doesn't work in 1.1.6 and I haven't quite tested it yet.

Tests

Lastly, a change which I completely disagree with, assert_template_has and friends are now deprecated. Use assert(@response.has_session_object?(key)) instead, my ass. This changes removes a useful failure message like <:login> is not a template object and brings me back to the glory days of is not true. I know I'll be rewriting those as custom assertions for my test_helper, thank you very much.

To Be Continued

Like I said, I'm only half done this migration, but when I get the rest of it sorted, I'll be posting a follow-up right here. See you then.

How to crash Ruby

  class BrokenError < StandardError
    def backtrace
      raise(StandardError.new)
    end
  end

  begin
    raise BrokenError.new
  rescue e
    puts 'rescued'
  end

Because of the exception in the backtrace generation, processing just dies. If you have an at_exit block, it will still be run, so I suppose I'm not really crashing the ruby interpreter, I suppose, but it comes close.

Found this one out migrating a rails app from 1.1.6 to 1.2. Instead of doing this:

  render 'controller/action'

the deprecation warning suggests the following:

  render :file => 'controller/action'

Unfortunately, this causes the error if you're still trying to run in 1.1.6. A more complete fix is to make sure to add usefullpath to the render call to prevent an older TemplateError from horking, like so:

  render :file => 'controller/action', :use_full_path => true

Camping for Railers

Well, I managed to get the weight-tracking app functional (graph and all) in about 220 lines, just tweaking the look now. It's a single-script Camping app using Gruff for graphing, with SQLite for data storage. Not exactly the most efficient app (I'm cheating by using a lot of mostly-null records in the database) but it gets the job done. There were a few things that got me stuck for a bit that weren't obviously mentioned in the camping docs, so I thought I'd put them down here.

If you're planning on letting Camping handle migrations for you (class Weight::Models::CreateEntries < V 0.1) be sure to require 'camping/db', which is what defines the V method. The equivalent of Rails' /params/ method for get and post variables is /input/ in Camping. Found that one by accident on a JRuby tutorial, of all things.

Not exactly a camping thing, but if you want a non 4:3 ratio gruff graph, send a string '1000x350' or similar.

That being all that I can think of browsing over the source, I think I'm safe recommending camping for quick prototyping. The best part is that if you want to switch over to a full-fledged rails app, you can just copy/paste the models and migrations (Rails and Camping both use ActiveRecord) and if you want to use markaby for your rails views, you can copy them over as well. A little more work organizing the views and correcting urls and such, but mostly painless. Just don't forget to do the testing ;)

Recursiveness

One of the big things I learned at University was that while "Recursion is a Wonderful Thing" (Thank you, Dr. Roelants), sometimes the performance can really hurt. Those times, it can pay to spend the effort turning that recursive function into a simple loop. Sure, it might not be as clean, or as elegant, or as natural to understand, but we're looking at performance here, right?

Ryan Davis recently posted about using RubyInline to optimize a recursive factorial method. He ended with a caveat that sometimes you need to look at other things than just moving the code into C for speed. His idea was to cache the data as it goes along. There are times when that won't help you in the log run (for example, generating a stats graph where caching as you draw helps, but the cached values will be stale the next time you need to do it) but changing it around to iterative can sometimes give you a further speedup.

def fib_iter(n)
  return 1 if n < 3  
  f = f1 = 1
  (2..n).each do
    f, f1 = (f+f1), f
  end
  f
end

The benchmarking speaks for itself. (Same parameters as Ryan's benching, 10,000 runs doing fib(15)):

                      user     system      total        real
fib-ruby         21.180000   3.640000  24.820000 ( 24.989140)
fib-hash-reset    0.510000   0.070000   0.580000 (  0.609976)
fib-cache-reset   0.510000   0.050000   0.560000 (  0.570715)
fib-iter          0.160000   0.020000   0.180000 (  0.209565)
fib-hash          0.020000   0.000000   0.020000 (  0.034616)
fib-cached        0.020000   0.010000   0.030000 (  0.035222)

Benchmarks for fib-ruby and fib-cached come from Ryan's post. fib-iter and fib-hash are mine.

The two "-reset" methods are indicative of times when global caching won't help you, which is still a significant speedup over the uncached versions. (For fib(15), uncached will need ~610 method calls, compared to ~15) The iterative method is about 1/3 their speed, but when you can globally cache you can get huge gains - if I increased the number of runs in the benchmark, the discrepancy between fib-iter and fib-cached would increase even more.

So once again, it seems that there's a different best solution for two different problems.

And the fib-hash benchmark? It's not significantly faster than Ryan's fib-cached method, but it bumps the fib logic from a method that uses a hash into the hash itself. It's a neat trick I picked up a while ago, but probably too ugly to make significant use of unless your benchmarking tells you otherwise - it's really hard to read at first glance:

def hashfib(n)
  return 1 if n <= 1
  h = Hash.new{|h,k| h[k] = h[k-1] + h[k-2] }
  h[1] = 1
  h[2] = 1
  h[n]
end

The cached version uses @@h instead of h, and ||=s it.

Dynamic ActiveRecord Attributes

Chris Abad wrote yesterday about his experience with dynamic attributes, and I thought I'd share mine.

I'm doing something similar to collect data POSTed to a form, but my data is slightly more structured than Chris'. I have a few fields that I expect to be populated most of the time, and the possiblity of arbitrary fields being set as well. My models look like this:

create_table "leads", :force => true do |t|
  t.column "email", :string, :default => "", :null => false
  t.column "firstname", :string
  t.column "lastname", :string
  t.column "ip", :string, :default => "", :null => false
end
create_table "lead_infos", :force => true do |t|
  t.column "lead_id", :integer, :default => 0, :null => false
  t.column "name", :string, :default => "", :null => false
  t.column "value", :string, :default => "", :null => false
end

Lead, of course, has_many :lead_infos. Thus, I can assume that most leads will have an email, first and last name, and an IP address. The name fields are optional, but common enough to warrant being in the main table (also makes for easier lookups and duplicate checking). Other things, like address, city, zip, etc. I want to hang on to if provided, so I store them as a LeadInfo.

I'm in the same boat as Chris though, as I want to provide uniform access to the data points in a Lead, as well as its LeadInfos, using the 'name' field as a key. Chris added an after_find hook that moved all the correct data in, but since I'm such a fan of metaprogramming, I decided that I would use method_missing like so:

class Lead < ActiveRecord::Base
  has_many :lead_infos, :dependent => true

  def method_missing(methodname, *args)
    begin
      super
    rescue NameError
      name = methodname.to_s.chomp('=')
      if (methodname.to_s =~ /=$/)
        LeadInfo.create(:name => name, :value => args.first, :lead_id => self.id)
        self
      else
        LeadInfo.find(:first, :conditions => ['name = ?', name]) or raise
        lead_infos.find_by_name(name).value rescue ''
      end
    end
  end

  ...
end

Walking through this, I first make sure to call super so that ActiveRecord's method_missing gets run first. If it can't find anything to do, then it is the Lead's turn to try.

We start by stripping a trailing = if it exists to get the correct name to use. If the = existed, it's being used as a setter, so we just create the new LeadInfo record based off the current Lead, and return self so that we can chain calls if we desire.

Otherwise, it's a getter, so I first verify that there is at least one LeadInfo with the supplied name - if not, then something is very wrong and we want to re-raise the NameError. Elsewise we so a search for the given value and return it, or an empty string if it is not set (the empty string catch-all is specific for the things I'm doing with this bit of hackery, so might not be applicable to everybody).

The performance difference between my code and Chris' is that I take 2 DB queries for each access to the LeadInfo data, but only when you ask for it. I'm not caching the result (which would take some strain away) because my use of it is solely one access each time I load the Lead from the DB, so caching wouldn't help me. On the other hand, if I do a grand find of a bunch of leads (and in a few places in my code that's quite a lot) I'm not getting hit with extra db hits that I'm not going to use most of the time.

There's benefits to both what I've done and what Chris has done, so anyone reading these can take their pick :)

Comments

Nice write up. I was going to go the MethodMissing route if it weren't for my requirement to have all the attributes insterted into the objects attributes hash. I think you're write about the catch-all being specific to you. Most people would probably want to re-raise the error and deal with that appropriately elsewhere.

  • Chris Abad, at 18:45, Sep 13 2006

Rapid Prototyping with OpenStruct

I was doing a bit of data processing the other night. A little copying here, a bit of typing there, formatting into YAML, then loaded into a Ruby script. Loop through the hashes YAML loaded, and try to make some sense out of it.

I'm happy to say that I wound up doing the most comfortable thing for munging the data, and it turned out pretty well: OpenStruct. For those who don't know about it (require 'ostruct'), OpenStruct is exactly as the name says. It's a struct, in that it just holds data, but it is open for extending after you've created it. One can almost treat it like a Hash, but with method calls instead of indexing. (In fact, this week's RubyQuiz was converting YAML-loaded Hashes to OpenStructs)

What I was doing was looping through the Hashes, and creating OpenStructs on the fly to hold the data. At the same time, I was back-referring to previous OpenStructs and appending data to them. I didn't think much of it until I thought to myself that I needed to do some calculations on the data, and the most logical spot for it was in one of my OpenStruct objects.

I was disappointed for a moment because I knew the methods didn't fit in OpenStruct itself, when I realized that it was just time to refactor a bit - take the OpenStructs that were holding the data, promote them to instances of a concrete class, and fit the logic in there.

A quick class def, a handful of attr_accessors, rename the OpenStruct instantiation to my new class, and I was off again, none worse for the wear. Ahh, duck typing, I couldn't have done it without you.

Fun with Routes

Rails' routing framework is a pretty capable beast, but it does sometimes still need a bit of help doing more exotic things.

I ran across an example from someone a few months back (either on the mailing list or in a blog post, I can't find any trace of it now) that was using multiple routes to match the same thing - a GUID that could belong to one of many different models. This was done with an overloaded Regexp subclass that pattern matched the GUID, and then looked it up to see if it existed for that model. I wanted to do something similar to that for a project I'm working on, and since I couldn't find an example to copy off of, I went delving inside routing.rb on my own.

The long and short of it is that the following code seems to be a workable solution for me, while being generic enough (the only real custom line is the last one inside the module def) for anyone to incorporate into their code.

module ActionController::Routing
  module ConditionConstants; end

  class CustomCondition < Regexp
    def initialize(name, &match)
      super '^$' # pretend we're a regular empty string regexp
      @name, @match = name, match
    end
    def =~(other); @match.call(other); end
    def inspect;   @name;              end
  end

  def self.custom_condition(name, &block)
    ConditionConstants.const_set(name, CustomCondition.new(name, &block))
  end

  custom_condition('ProjectCondition'){|other| Project.find_by_url(other) }
end

The only other thing to do is include ActionController::Routing::ConditionConstants inside the block attached to draw so your connect calls can see the constants being defined - AC::Routing seems to have no problem seeing them, even though they're inside a module of their own.

Anyone interested in the hows and whys, feel free to read on...

The goal then, is to come up with an object that can perform an arbitrary condition for use in routes. My usage is a simple lookup in one of my ActiveRecord models, but really you could do anything you wanted. So let's take a look through the generation of a route.

First off, you create routes using the draw method of ActionController::Routing::Routes, which accepts a block detailing the routes you want to connect. Routes is actually not a class with a class method draw, but rather is an instance of AC::Routing::RouteSet. draw yields the RouteSet to the content block, so let's take a look at it for a moment.

The RouteSet#connect method takes a bunch of arguments, and passes them directly into the constructor of AC::Routing::Route. Route accepts two parameters: a path, and an options hash. The path can be either a string (which is split on '/') or an array. The options hash is populated with either defaults or conditions for the various parts of the path.

While you can explicitly define :defaults and :conditions with their own subhashes, but Route is smart enough to do some thinking for you: if the value for a given key is_a?(Regexp), it is treated as a condition, otherwise it is considered a default. Therefore, this is the first test that our custom condition must pass. Fortunately, it's trivial to write an is_a? method on whatever class we end up writing that returns true for Regexp, so it's not a big stumbling block.

Now, I have to admit I snuck a bit ahead of the game here - when Route was doing data massaging on the path, it is creating AC::Routing::Components to store each part of the path. There are actually four different subclasses of components, each created by the base Component class. The one we're interested in is DynamicComponent, which is created when the path looks like a symbol. ControllerComponent matches on the explicit symbol :controller so that it can deal with modules, so we don't need to worry about it.

This comes in later on in RouteSet#draw. After creating the various Routes, it calls methods named write_generation and write_recognition. write_generation sets up rules for turning a params hash into an actual URL. write_recognition does the other way, which is what we want.

write_recognition then, assembles the recognition rules for each of its Routes, which through a roundabout way calls the same on each Component. The DynamicComponent we were looking at then calls the class method Routing.test_condition with its condition. This leads us to our second constraint on our custom class - test_condition runs the condition through a case statement, putting classes on the whens. The when Regexp condition behaves the same as if Regexp === condition, which only evaluates true when condition is an instance of Regexp or a subclass.

This is significantly more difficult to fake than being able to redefine is_a?, and I really didn't want to come up with a solution that required hacking into Regexp to get anything done. The next two lines after the when (that's 38 and 39, for those playing at home with Rails 1.1.2) add two other wrinkles in the behaviour.

The first is that if the Regexp instance is not bookended by beginning-of-string and end-of-string matchers, a new instance is created that is wrapped so. However, this isn't as bad as it looks at first. Since we're not actually using the Regexp source for any pattern matching, we can satisfy this criteria by setting the pattern to /^$/, which matches an empty string.

The second is significantly trickier, in that when our matcher is inspected, it needs to output as a string the ruby code used to create it. This is fine for normal regular expressions, as /^$/.inspect does actually print out "/^$/", but if we want our matcher to be highly dynamic, it effectively rules out using blocks or procs directly as we would expect. Additionally, the output of the inspect call is then directly sent an =~ call with the part of the path it is trying to match.

My first thought on this was to use classes, since I could create an instance that would pass the "is a subclass of Regexp" test, output the name of the class, and use a class method to do the actual matching (coming later), but that was looking much too ugly. Then I realized that the reason I was drawn to classes is that it is a constant that knows how to look itself up. If I were to teach a Regexp subclass the name I'm about to assign to it, it will know how to reference itself directly, and storing it in a constant means I should be able to get easy access to it by the time we get that deep into the code.

Thus began my CustomCondition class.

I first set up an empty module named ConditionConstants that I would use to house my constants.

Next up was the class - a subclass of Regexp to pass the is_a? and === tests, but with an overoaded initialize. This first set up the Regexp base to use the empty string regex listed above to prevent Routing from stepping over the constant. initialize also accepted the name of the constant it is about to be stored in, and a block of code to execute later on.

CustomCondition#inspect did the obvious, and output the name we were to be remembering.

CustomCondition#=~ simply called the stored block with the argument we are trying to match, allowing the creation of the object to completely drive its purpose.

Lastly, since all the work was being done in the ActionController::Routing module, I created a class method that would do the creation and assignment for me so that I was doing less direct repetition.

Once that was all in place, I simply called my helper method with the name of the constant, and a block encapsulating the behaviour. Done and done.

Driving

Great thought that came to me in the shower this morning: Agile software development is like driving.

Your team is driving the project to its destination. It's travelling down a multi-lane highway. Some team members like driving in the fast lane, others take it a little more deliberate and cautious and go in the slow lane. But they're all heading to the same place.

The team members like to mix it up a little, swapping cars and who drives, so that everyone can get a feel for the entire fleet.

The important thing is that everyone is in constant communication, even while they're driving. If we find that we need to change destinations, it's not a problem for everyone to make the right exit - even the guy in the fast lane - because there's always someone in each car with the map. This is why we work in pairs: driving with a map in front of you is a hell of a lot harder than having a co-pilot/navigator.

Swimming

Out to lunch with the department today, bemoaning the state of Computer Science education. The general consensus is that the local universities seem to be teaching Java in an SWF setting - Structs With Functions. This leads to a seriously flawed notion of what OO programming is, and can seriously warp the still-forming mind of a new coder.

Because I am wont to do so (and have been doing so for that last two weeks or so, much to everyone's chagrin), I took this a step further. Really, if you're writing Java then what you have are methods, not functions. Even if you use them like that. So rather than have SWF, you have SWM, or Swim.

This is followed by much entertainment exploring the analogy - things start off easy, but as you do more of it it's harder to keep afloat, there's a possibility you might just wash out, etc. Then Jon comes up with the whopper:

"So that's why they call it the waterfall method of software development"