Here is a technique I’m experimenting with on gomore.dk. I suspect that I’m reinventing some pattern here. Please let me know in the comments if you recognize it.

When searching for rides on Gomore, users may specify from-location, to-location and a date. For the locations I use the Google Places API, giving the user access to the nice autocompleter commonly known from Google Maps. This also means that the chosen location is geocoded on the client side.

The date can be entered manually or chosen in a jQuery UI date picker.

Gomore search fields

This can go wrong in so many ways:

  • Location is not geocoded for some reason. Happens a lot with older browsers.
  • Browser screws up date format (we’ve seen a leading minus sign!)
  • Invalid dates, dates in the past etc.

The controller to handle these parameters was getting way too complicated from supporting all these cases. Also, testing controllers can be pretty painful, so I decided to pull this complexity out to a separate class, SearchParams. Not the best name, but the best I could come up with at the time.

I expect to use this pattern for several other actions, so I created a home for these classes: app/params

This class is reponsible for cleaning up all parameters to the search action. This means that it is tightly coupled to the this action, which is OK. If I need to reuse some of this (eg. the code to clean up dates), I can easily pull it out to a separate class which can be used from several Params classes.

Obviously the goal here is that the controller can assume that all parameters are OK and the complexity goes away.

Let’s see some code.

app/params/search_params.rb

class SearchParams
  def initialize(params)
    @params = params
    ...
  end

  # Fix invalid encodings sent from old browsers
  def force_encoding
    ...
  end

  # This method is a bit complex but very easy to unit test because it's in a
  # separate object. 
  def timestamp(options = {})
    default = options.fetch(:default, DateTime.now)
    now = options.fetch(:now, DateTime.now)

    begin
      res = DateTime.parse(cleaned_date_str)
    rescue
      res = default
    end

    if res.to_date > now.to_date
      res = res.beginning_of_day
    elsif res.to_date == now.to_date
      res = now
    end

    res = now if res < now

    res
    ...
  end

  def from
    params[:from]
  end

  def to
    params[:to]
  end

  # If the specified a location as a string without geocoding it, do so now and
  # update our internal state to reflect that parameters were modified. This
  # causes the `action` method to return :redirect instead of :proceed. 
  def geocode_if_necessary
    ...
  end
end

In the controller, I create a SearchParams object from the params. Then, two method calls make it very clear how the params are processed:

  • Force encoding
  • Geocode if necessary

More than this happens, but these are the steps that are not naturally linked to fetching a parameter value. For example, the #timestamp method handles dates in the past and other cases.

Next, we need to decide whether to render the search results or redirect. This decision is delegated to the search params object. If the parameters were OK to begin with, we proceed to render the search results. If not, we redirect using the corrected params. This prevents the user from ever seeing (and sharing) invalid search URLs.

app/controllers/rides_controller.rb

  def search
    search_params = SearchParams.new(params)
    search_params.force_encoding
    search_params.geocode_if_necessary

    case search_params.action
    when :redirect
      redirect_to search_path(search_params.params) and return
    when :proceed

      # Do nothing
    end

    @from_location = search_params.location(:from)
    @to_location = search_params.location(:to)
    @on = search_params.timestamp
    ...
  end

I’ve glossed over a lot of details in this writeup, but I hope you found it useful anyway. Let me know if something needs to be clarified.