ActiveRecord association extensions and method_missing

The semantics of method calls in Ruby are simple:

  1. Call the named method; or
  2. If no method exists, call #method_missing() instead.

Normally #send() obeys these rules as well. ActiveRecord association proxies mangle #send()‘s semantics, however, violating the POLS and potentially breaking your code in the process.

Let’s say we have some monsters:

Cookie monster, being a personable kinda guy, is friends with all the others:

Now lets say we want to filter Cookie’s friends by color. Not that we’re prejudiced or anything; we just want to teach the nice kids about GREEN and BLUE.

We could define filters for each color manually:

Note that we’ve extended the friends association with ByColor.

But that’s a repetitive way of defining ByColor. Let’s say, for the sake of example, that we decide to use #method_missing() instead. In this implementation, any undefined zero-arg method call on the association will be treated as a color filter:

Let’s check that our filters still work:

And just to be thorough, let’s try using #send() to do the same thing:

But instead of getting Oscar and Clancy again, we get an exception!

I’m not going to dig deep into the ActiveRecord association proxy gymnastics that lead to this error. Suffice to say, ActiveRecord overrides #send(), and as a result our #method_missing() is never tried.

How often is this really going to matter? Well, chances are this difference will bite you when you try to test your code. RSpec’s matchers make heavy use of #send(), under the (reasonable) assumption that it will behave identically to calling the method directly. So where you’re likely to run into this is in specs that seemingly behave differently than your production code. That’s where I ran into it.

The solution is to always define a #send() when defining #method_missing() in an association proxy:

This will ensure that #method_missing() is tried before giving up on a method. Note, the code above is a particularly dumb implementation of #send(), but it’s sufficient to get our example working again. Your #send() may need to be smarter.

Oh and while you’re at it, you should probably define your own #respond_to? for maximum consistency. Make sure it falls back to super if it doesn’t find the method.