The Rails application I’m currently working on accepts URLs like:

GET /submarks
GET /submarks?lat=:latitude&lng=:longitude
GET /submarks?lat=:latitude&lng=:longitude&user=:user_id
GET /submarks?lat=:latitude&lng=:longitude&channel=:channel_id
GET /submarks?lat=:latitude&lng=:longitude&channel=:channel_id&user=:user_id

All query parameters are optional; only lat and lng have to appear together. On the Rails end, I have a Submark model with the attributes lat, lng, user_id and channel_id.

So how do you implement find call on the Submark model without a long, ugly if-elsif-else statement? First of all, if you only have = as a comparator you can use a hash for the :conditions parameter of the find method:

conditions = {}
conditions[:user_id] => params[:user] if params[:user]
conditions[:channel_id] => params[:channel] if params[:channel]
Submark.find(:all, :conditions => conditions)

But what if you want OR instead of AND in the WHERE clause? Or what if you want to do something like lat < 16? Currently, you have revert to ugly if-elsif-else statements like:

if params[:user] and params[:channel].nil?
  Submark.find(:all, :conditions => ["user_id = ?", params[:user]])
elsif params[:user].nil? and params[:channel]
  Submark.find(:all, :conditions => ["user_id = ?", params[:user]])
elsif params[:user] and params[:channel]
  Submark.find(:all, :conditions => ["user_id = ? AND channel_id = ?", params[:user], params[:channel]])

A good solution for this problem is the ez_where plugin by Ezra Zygmuntowicz and Fabien Franzen. It includes a new find_where method in your ActiveRecord models which takes a code block building the conditions with a quite nifty syntax.

But I didn’t want to depend on an external plugin, mainly because, as we’re working with edge Rails, it’s not unlikely that ez_where will break sometime during development.

Instead, I’ve implemented a little helper, create_conditions, which is able to construct a conditions array in the format the usual find method likes. It also works by utilizing a code block. You get a Conditions class as parameter to your code block, which has the methods AND and OR.

This is how you would use create_conditions in your controller:

conditions = create_conditions do | c |
  c.and ["channel_id = ?", params[:channel]] if params[:channel]
  c.and ["user_id = ?", params[:user]] if params[:user]
  c.or "distance < 10"
Submark.find(:all, :conditions => conditions)

The result of create_conditions looks like:

  ["channel_id = ? AND user_id = ? OR distance < 10", 1, 42]

As you can see, you can use both String- and Array-based conditions. Hash-based conditions are not supported.

Get the code here.