Rails tip: Side effect filters
Some bugs are easy to overlook. One that has a habit of catching me out is a Rails filter that returns false occasionally when it’s being evaluated purely for its side effects. Here’s how I’ve started working round the issue:
def side_effect_filter
return if some_conditions_not_met?
...
ensure
return true
endWhat happens here is that the ensure catches any return and returns true instead. The catch is that if something throws an uncaught exception anywhere, it too gets caught by the ensure and true is returned. Which may not be what you were looking for. Here’s how to fix that issue:
def side_effect_filter
error = nil
return if some_conditions_not_met?
...
rescue Exception => error
ensure
raise error if error
return true
endThis catches the exception in a rescue and stashes it in the error variable, then the ensure checks to see if an exception was thrown and rethrows it, otherwise, it just returns true. Which is bulletproof, but ugly. Let’s wrap the ugliness up in a method:
def self.side_effect(method, &block)
def_method(method) do
error = nil
begin
instance_eval(&block)
rescue LocalJumpError # catches an explicit return
rescue Exception => error
ensure
raise error if error
return true
end
end
end
side_effect :side_effect_filter do
return if some_conditions_not_met?
...
endAgain, not pretty inside, but all we actually care about anywhere else is that the interface is good and does what it’s supposed to do. Encapsulated ugliness has its own beauty. Especially if you get the interface right.
Homework
This should pluginize quite nicely, just install the method in ActionController::Base and ActiveRecord::Base and you have a very useful tool, but I’m still not sure that the method name is right, so I’m holding off on it. If someone were to come up with a bulletproof name and release a plugin, that would be wonderful though.
Updates
Fixed a scoping issue in the encapsulated version of the code. Replaced yield with instance_eval(&block)
My first 'acts_as' plugin 7
So, you’ve upgraded to Rails 1.2.1 and you’re working on a tool to maintain a database of all the tunes you have in your various songbooks and (eventually) your record collection. You start with:
$ ./script/generate rspec_resource MusicBook title:string author_id:integer \
abstract:text
$ ./script/generate rspec_resource Tune title:string composer_id:integer \
abc:text book_id:integer
You decide to come back to composers and authors later, so you set up your models1:
MusicBook.has_many :tunes
Tune.belongs_to :music_book
And your routes:
map.resource :music_books do |book|
book.resource :tunes
end
Problems start here
Being a cautious sort, before you start adding behaviour, you fire up a development server and go and check things with the browser. The /music_books/ stuff works fine, but once you start looking at /music_books/1/tunes things start to get weird; all of a sudden your links aren’t making sense.
The problem is, that your scaffolding is calling named routes with something like: edit_tune_url @tune, when, because of the nesting of your resources, they should really be calling edit_tune_url @tune.book, @tune. But how to fix things?
Well, you could go through all your controllers and views, replacing all the calls to named_routes with the right version. But, if you’re anything like me, you’ll get bored stiff of the repetition after you’ve fixed up the first file. And that’s before you start fixing up your controllers to do
@tune = Book.find(params[:book_id]).tunes.find(params[:id])
So, I’m going to suggest that a better bet is to install the acts_as_resource plugin I’m in the process of writing.
Making the model pull its weight
Once you’ve got acts_as_resource installed, you can just do:
Book.acts_as_resource
Tune.acts_as_resource :parent => :book
And your models will magically acquire a resource_chain which returns exactly the list of objects that your named routes need. I’m currently investigating the innards of named routes, but the plan is that you’ll never have to call resource_chain yourself, the named_route helper will do it for you and use the resulting list to build the url.
Your model class, meanwhile, gets a handy
Tune.find_resource(params)
which will find your resource, verifying that it can be reached through the chain of resources specified in your params hash, which means we can fix up the scaffolding generator to use find_resource (or, more likely, a helper method that will set appropriately named instance variables in the controller).
If I understand what simply_helpful is up to, the named_routes hack should play well with that too, which means that form_for @tune will get its url right without you having to remember to call form_for @tune.book, @tune.
Release date?
I’m not quite ready to release yet, I’m busy wrapping my head around how named routes work so I can fix ‘em up to use resource_chain. find_resource is written though.
It’s amazing how much leverage you can get, simply by adding one class attribute and a support method to your model…
Expect a release some time next week. However, if you’re desperate for a look, drop me a line and I’ll send you my local snapshot.
1 Look, vertical space is precious. Pretend this is in the usual blocky type stuff you usually find in the files you’ll find in app/models/whatever.rb
A sketch of declarative ActiveRecord Migrations
Writing migrations can get pretty tedious when you’re being scrupulous about writing both the up and the down side of the migration. Okay, so the Textmate ninjas amongst you can use scarily clever snippets to populate the down migration while you write the up method, but I can’t be the only Mac user who still prefers Emacs. And not everyone gets to run on Macs either.
So, inspired by something Jamis Buck wrote about designing a DSL, I’ve been sketching out a DSL for describing the easy parts of a migration in declarative style. None of this is implemented yet, but I’m pretty sure that it’s relatively simple to implement for a decent Ruby metaprogrammer. I’m brain dumping it here so I can come back to it later, or, you never know, someone might have implemented it by the time I revisit…
The sketch
class AdjustContentTable < ActiveRecord::Migration
class Content < ActiveRecord::Base
include ActiveRecord::Migration::Delta
delta do
+(:extended, :text)
-(:excerpt, :text)
+index :text_filter_id
end
end
end
callbacks_for(:up) do
before_any_changes
Content.before_changes { ... }
Content.before_additions {...}
Content.after_additions {...}
Content.before_removals {...}
Content.after_removals {...}
Content.after_changes {...}
after_any_changes
end
callbacks_for(:down) do
...
end
endImplementation Pointers
By making delta do its work in a block, it’s possible to instance_eval the block using a Delta object that has appropriate implementations of + and -.
callbacks_for[:up] puts its block into, say @callbacks[:up] and the block is yielded to to set up the callbacks by the default implementation of up. The default up probably looks something like:
def self.up
@callbacks[:up].call
apply_all_deltas
endapply_all_deltas – the per class apply_delta is reasonably simple:
def self.apply_delta(direction)
callback(:before_changes)
apply_additions(direction)
apply_removals(direction)
callback(:after_changes)
end
def self.apply_additions(direction)
if has_additions?(direction)
callback(:before_additions)
eval additions_for(direction).to_ruby
callback(:after_additions)
end
end
...Thinking about it, apply_all_deltas is going to take a certain amount of grovelling about in the namespace to work out what classes need to have their deltas applied, but even with just per class delta declarations, this should be a useful thing to implement.
Braindump ends
Sorry this doesn’t come with a handy dandy Rails plugin with it all implemented; your contributions in this area would be very gratefully received.
Testing Your Assumptions
One of the joys of writing applications using Ruby on Rails is the way the framework is constantly evolving better ways of doing stuff. It’s one of the dangers too.
Each release of Rails brings new and groovy features to the table, so it’s nice to stay close to the edge. However, when you do that, a change in the framework can bring your whole app crashing down because a key assumption you made turns out not to be true any more.
This is especially common when the assumed behaviour is undocumented, or a surprising consequence of behaviour that is documented.
When this happens, your test suite can have failures everywhere; it’s hard to work out precisely what the underlying problem is.
So, what to do?
Easy. Write tests that express your assumptions. It’s always good to make assumptions explicit. It’s better still to make them executable. Whenever you use something that’s a little bit surprising or hard to find in the docs, or when you do something that works around a bug, write a test.
Then, when the framework changes and tests are failing all over the place, test your assumptions. Hopefully there’ll be one or two failing tests there that point you to a solution to your problem. If there aren’t, then at least you know what’s not causing the problem. And, when you do find the underlying problem, you can add the new assumption to your tests for later.
I’ve just been working on Typo and found (rather usefully as it happens) that, if you add counter caching to a model, the model that holds the cache field will call all its save/update hooks whenever the cache field is updated.
Which is a little odd; it could be argued that simply adding what should be an ‘invisible’ cache field to a model shouldn’t go changing its behaviour in this way. So, I’ve created an assumptions_test.rb file and written the behaviour up as a test. Now, if David Heinemeier Hansson decides that this is undesirable behaviour and changes it, we’ll be able to see what’s going on.
The idea (as is so often the case) comes from Kent Beck. In this case, from Test Driven Development. One more book by Beck that deserves to be on every programmer’s bookshelf.
EuroOSCON here I come
I just got word from O’Reilly, I’ve got press accreditation for European Open Source Conference in Amsterdam next month. So I’ll be interviewing various Perl 6 luminaries and hopefully summarizing any Perl 6 hackathon activities, doing photojournalistic stuff and generally enjoying the ‘hallway track’.
I expect I’ll also try and have a word with the Ruby people as well. The last OSCON I went to was a couple of years ago in Portland Oregon. It’ll be interesting to see how the European and US versions compare.
