Archive for December, 2006

A multitude of method_missing monstrosities

Friday, December 15th, 2006

I read a blog entry that dinged with_scope in controllers and especially in around_filters. My first reaction to reading this post was quite positive: “Here are some techniques to help move more logic to the model” which is high on my list of objectives. But I was dismayed to see the trendy fervor building up to hobble with_scope (mostly on other sites). Permit me to outline some excellent uses for with_scope in a controller….

First and most topically is the issue of nested RESTful resources. Imagine a MessagesController that deals with Messages. Messages can be viewed as a resource in their own right and RESTfully represented as http://somesite/messages. But they can also be considered child resources of two other resources, dogs and cats, and consequently messages can also be represented as http://somesite/dog/1/messages or http://somesite/cat/26/messages. In each action of my MessagesController, I need to check for the parameter ‘dog_id’ or ‘cat_id’ (or none) and ensure that proper restrictions are in place for every model query. I could perform this test in every action and call a custom model method that implements the appropriate scope. Or I could send a programatically constructed method name. But duplicating the test/construction in every action is not very DRY (my controller gets complicated with cats and dogs before I even start thinking about Messages) and my Message model now has some complex method_missing code, or unDRY method definitions. And please don’t suggest I pass a parameter to a custom finder -the M-C coupling will hurt badly when my second scenario is considered.

I would argue that the decoupled nature of with_scope in an around filter suits the explicit scoping implied by nested resources very nicely. It would be slick as snot if a REST resource definition could also support automatically defining a model scope for that resource. Something like

map.resources :cat {|r| r.messages, :scope_model => :message }

would automatically Message.with_scope(cat=cat_id) within the resource controller. OK, so this might be going a bit too far…

Secondly, with_scope has the nifty ability to merge independently defined scopes. This allows for a very clean decoupling of orthogonal concepts that would otherwise result in a multitude of custom finders with garishly coupled names. If I need need restrictions for authorization (get authorized data), and I need restrictions for currency (get current data) and I need restrictions for RESTful nesting (get data belonging to parent resource), and I need restrictions for active data (get data that has not been logically deleted) there might be hundreds of different custom model finders needed. Obviously method_missing could DRY that up quickly, but the method constructor would be horrendously complicated that could build Message.find_active_owner-authorized_current_dog and Message.find_deleted_view-authorized_old_cat. A better solution would be to decouple the overarching restrictions of authorization and explicit parent resources into around_filters leaving a custom finder to deal with currency, logical deletion and, of course, run-time user-defined filter conditions. Now my method_missing only has to build Message.find_active_current and Message.find_deleted_old.

To summarize, with_scope in a controller has two excellent applications: 1. The controller’s entry points are explicitly or consistently scoped, such as with nested RESTful resource controllers, and in many cases, authorization. 2. Numerous orthogonal restrictions that need to be combined, such as (logical deletion) AND (authorizations) AND (belongs_to_parent). In these cases, I believe controller-defined with_scope is The Right Stuff.

Technorati Tags:

YAML Fixtures for Serialized Objects

Friday, December 1st, 2006

I have always had problems trying to build sophisticated fixtures with YAML. But binary data fields (blobs), multi-line text and dynamically generated data are frequently required for ensuring a rich testing dataset. By scouring the web, I have been able to overcome most of the problems with point solutions. Yesterday, I ran into a problem that did not seem to be documented: building YAML fixtures for serialized objects.

First, recognize that it is merely a coincidence that Rails YAML-encodes a serialized attribute when storing it in the database as well as when building a test fixture. There is no direct transfer of the YAML-encoded fixture to the database field -Rails instantiates the object first. So in theory a serialized attribute is no more difficult than any other attribute to define in the fixture files. But, a serialized attribute is typically not a simple single-line YAML type. Instead, it requires a more exact knowledge of the YAML spec.

Like me, you may be thinking “why not just use the to_yaml method that is available in Ruby? Shouldn’t it generate the necessary YAML code for the serialized attribute?” To make a long story short: not quite. The problem lies in the indentation required to embed a YAML stream within another YAML-encoded object. YAML is highly sensitive to indentation: each attribute of an object must be indented at the same level. So in a typical RoR fixture file, the fixture object name starts in the leftmost column and all its attributes are typically indented two spaces, like this:

pie:
  id: 2
  sender_id: 1
  recipient_id: 2
  reference_type: "Place"
  reference_id: f81d4fae-7dec-11d0-a765-00a1c91e6bf6
  created_at: <%= 2.days.ago.to_s :db %>
  updated_at: <%= 1.days.ago.to_s :db %>

The serialized object is also an attribute of the fixture object so it too must be indented two spaces, like this:

pie:
  id: 2
  sender_id: 1
  recipient_id: 2
  reference_type: "Place"
  reference_id: f81d4fae-7dec-11d0-a765-00a1c91e6bf6
  created_at: <%= 2.days.ago.to_s :db %>
  updated_at: <%= 1.days.ago.to_s :db %>
  msg: <%= TMail::Mail.new.to_yaml %>

But the serialized object’s yaml representation (emitted by the to_yaml method) spans multiple lines -and each of its attributes must be indented four spaces underneath an appropriate header. The multi-line problem is easy to address as the YAML spec accommodates quoted multi-line strings like this:

  msg: |
<%= TMail::Mail.new.to_yaml %>

Unfortunately, that alone did not solve my indentation problem. The to_yaml method does not inherently know to indent four spaces -it only generates a two-space indentation. The result: confusion. I was temporarily elated to find that the to_yaml method appeared to support an :indent option that could be used to specify the number of spaces. But bluntly, it did not work. So I hacked the generated YAML a bit:

pie:
id: 2
  sender_id: 1
  recipient_id: 2
  reference_type: "Place"
  reference_id: f81d4fae-7dec-11d0-a765-00a1c91e6bf6
  created_at: <%= 2.days.ago.to_s :db %>
  updated_at: <%= 1.days.ago.to_s :db %>
  msg: |
    <%= TMail::Mail.new.to_yaml.gsub(/\n/,"\n    ") %>

Finally, Rails swallowed the fixture. Done.

REST and GET on Forms

Friday, December 1st, 2006

I have been converting a small Rails application to REST this past week and several issues have come to light surrounding HTML forms. I can’t seem to find a good solution to these problems. In my last entry, I tackled the issue of delivering of arbitrary forms to HTML clients. This entry focuses on the annoyances with using GET and Forms.

Unless you have been living in a cave for the past couple of years (Osama?), you have heard of REST. It encourages exploitation of the full spectrum of inherent capabilities of the HTTP protocol to provide resource-centric services over the web. Part of this exploitation centers on the HTTP verbs PUT, DELETE et. al. that are under-used. Another focus is on the use of GET in non-traditional contexts like HTML forms. The essence of this last recommendation is that GET should be used whenever there are no side-effects from the request. And querying usually falls into this category, so we should be using HTTP GET to submit HTML forms that are used to build queries. Sounds good, and it is trivial in Rails to implement with the :method=> :get syntax. But there are some ugly side effects to using GETs to send form data, most of which stem from the fact that a GET must URL-encode its parameters. Consequently:

  1. Hidden fields are represented in the URL
  2. Characters sets for form values are limited to URL-encodable values
  3. HTML Image Map coordinates are represented visibly in the URL -and this includes images used for buttons.

Some of these are aesthetically unpleasant, while #2 is downright restrictive.

Anybody got any solutions?

See this reference for more information.