Thursday, August 30, 2007

Rails: Resource Links in Resource List

Navigating a REST application should not require a deep understanding of the namespace of the application's addressable resources. As much as possible, one REST query should lead to another.

The most obvious case of this is when one has a resource that is a list or collection (e.g. /customers) of the available resources (e.g. /customers/1), e.g.:

<name>The Customer</name>
<a href="myserver/customers/1.xml" />

And yet, it's surprisingly difficult to put these kind of links into Rails resources, particularly if you use the approach that resource_scaffold suggests.

Resource scaffold generates the XML representation of the collection as by calling the to_xml method that Rails adds to Array, which in turn calls to_xml on the model classes, something like this:

@customers = Customers.find(:all)
render :xml => @customers.to_xml

In order to add links to the XML representation, you'd need two pieces of information: the URL structure for resource and the id of the resource. The ActionController has the most understanding of the URL structure (as it can use
url_for and named resource helpers like customer_path), but the model class knows its id and is responsible for generating the XML.

This awkward split in the available information makes adding these links to the XML somewhat painful; perhaps no more painful than building the XML by hand would normally be, but it's a departure from the usual ease of accomplishing common tasks in Rails.

After trying a number of alternatives, we settled on approach. In the controller, we do:

# GET /customers.xml
def index
@customers = Customer.find(:all)
respond_to do |format|
format.xml do
render :xml => @customers.to_xml( :format => :summary, :base=>url_for(:controller=>'customers') )

# GET /customers/1.xml
def show
@customer = Customer.find(params[:id])
respond_to do |format|
format.xml { render :xml => @customer.to_xml( :format => :full ) }
rescue ActiveRecord::RecordNotFound
head 404

This relies on some model code, like this:

def to_xml( options = {} )
case options[:format]
when :summary
resource_url = proc { |options| options[:builder].a(:href=>"#{options[:base]}/#{id}.xml") }
super( options.merge!( :only => [:id, :name], :procs => [resource_url] ) )
when :full
super( options.merge!( :include => [ :main_contact_info, :billing_contact_info ],
:except => [ :main_contact_id, :billing_contact_id ] ) )
super( options )

It's still a little awkward, and I'm hopeful that we'll find a better alternative, but it does the job and is less ugly than some of the other alternatives we considered.

The only other option we looked at that seems reasonable is to generate all the XML within the controller, which means reproducing the work done by Array.to_xml, but is otherwise relatively sensible.

Friday, August 24, 2007

Rails Plugins: Globalize

Imagine that you've built yourself a small application in Ruby on Rails. Here's the good news: there are all sorts of ways to simply add significant functionality to your Rails applications through plugins. Let me introduce you to a few.

I've been investigating the Globalize and Acts As Audited (acts_as_audited) and Acts As Versioned (acts_as_versioned) plugins for Ruby on Rails over the past few days. I've got a lot to share, so I'll break it up into a few posts.

Let's start by imagining that you've built yourself a really, really simple model:
You've cut a customer model class with a unique identifier and a name. Now, let's add to that.

If your application needs to operate in a multilingual or global environment, you'll be pleased to discover that not only is Globalize a general-purpose internationalization and localization (i18n/l10n) mechanism for Ruby on Rails, but it can even translate your model classes.

If you're going to translate fields within your model, you should be aware that it has two storage models for the translated information. Historically, it did this by storing the translated text in a separate table, globalize_translations. I'll call this the external storage model.

Using our model from above, the name field corresponds to the name in the base language (whatever you decide that is; en-US is a common choice in this part of the world), and all other translations are stored in the globalize_translations table as follows:

When you load or save your model class within the context of a particular Locale, Globalize does its magic behind the scenes to make sure that the text is loaded from or saved to the appropriate spot -- base language text goes to the primary table, and text from any other language goes to the translated-text table.

As often seems to be true, this magic interferes with some of ActiveRecord's default mechanisms, and :select and :include clauses stop working as advertised. There are workarounds, apparently.

However, in recent versions of Globalize, you have another choice - you can store the translations directly in your model, as follows:

This interferes less with ActiveRecord, although it's better-suited to applications that need to work with a known number of languages up front, rather than an unknown and changing set of languages, as each language addition requires new columns for every translatable field. It has a few caveats of its own.

Thursday, August 23, 2007

Globalize 'Internal Storage', First Look

I took a moment this morning to examine the new internal storage model for Globalize, wherein translations are kept within the original table. It seems to have a few more caveats:

  • Rails doesn't instinctively understand that the translated column names are not otherwise normal attributes; that means that any "iterate over all fields" code may have the translated columns show up as well. (e.g. scaffold show html).
  • If you haven't inserted the translation column for a locale to which the user can set, rather than using the base language, the user will not see any value. So, for instance, I turned on internal-storage on an example class with a single translated 'name' field. I created an example record (e.g. name='Hello') and switched to an 'fr' locale; instead of getting the contents of the name field ('Hello'), I got an empty string ('').
It's an interesting choice, mostly because it breaks less of Rails than Globalize's other model. I prefer the 'external storage' approach in many cases, particularly if you're dealing with a product or SaaS application where the number of supported languages is not clearly known up-front.

Software Developer Patterns I: Enthusiast

In keeping with Christopher Alexander's seminal tome: A Pattern Language: Architects, Designers, Construction Workers, I have decided to document the pattern language of Architects, Developers, QA.

Although I cannot hope to document every pattern you see in your co-workers, I can, at least, start by codifying those patterns familiar to all. I do not claim to have invented these patterns, only to have begun the process of collecting them together into a common language to aid in communication.

Instead of grinding your teeth at your next design review, you can instead retort, "You were being a bit Enthusiast there, weren't you?" Your co-workers and the intended target, having read these patterns as well, will immediately understand what you're implying, and, chastened, acquiesce to your superior intellect.

Although developers are geeks, and therefore one-dimensional, maybe two-dimensional at best, you'll find that some of your co-workers exhibit not just one, but possibly two, or even three of these patterns. Some might even defy classification: occasionally, you may find that rather than treating your co-workers as the avatar of one of these patterns, you may have to treat him or her as an individual, with complex motivations and rationale. This is an edge case which can be resolved through good QA, or possibly an exception-handling routine.

The structure for each pattern will be as follows:

  • Name: The name of the pattern
  • Overview: An overview or description of said pattern, how it might be recognized.
  • Motivation: The primary motivation of the pattern-exhibitor.
  • Applicability: Where this pattern is useful, where it isn't.
  • Collaboration: Who this pattern can collaborate with in order to form a team.
  • Consequences: The likely outcome of employing this pattern.
  • Sample Conversation: An example of a conversation between an arbitrary observer and a pattern-exhibitor.
  • Known Examples: Well-known members of the community who exhibit this pattern regularly.
  • Related Patterns: Similar patterns; ways in which these are similar and different.
In keeping with the broadcast television model, I'm going to stretch these patterns out over a series of intermittent posts. If I thought you'd stick around in between, I'd play advertising, but I figure most of you have Tivos or other PVRs by now, and wouldn't watch them anyway. That said, I'm told that it makes economic sense to give a 'free sample' to get you hooked, so without further ado:

Enthusiast (Fan; Zealot; Cultist)
Enthusiasts feel strongly about their technology choices. They identify with them on a personal level. When push comes to shove, they will defend these technology decisions with the zeal of a cornered badger using all the techniques in their arsenal: ad hominem, straw man, yo mamma, whatever approach prevents them from having to face the deeper issues.

Enthusiasts often relate closely with technology because they have trouble relating to other humans and/or animals. Unable to form friendships and relationships, or even to own a pet, they transfer this emotional bond to objects with which they feel the most intimacy: a programming language, a hardware manufacturer, or something of that nature.

Enthusiasts make good bloggers, evangelists and marketers. Their enthusiasm is so strong it can be infectious, particularly when they're preaching to the already-converted. They are not well-suited to fielding criticism, performing quality-assurance or to lend an objective eye. They cannot be trusted to make high-level decisions that touch upon their areas of enthusiasm. A Lua enthusiast asked to choose a programming language for his or her project will stack the deck in favour of Lua.

Enthusiasts are a good choice to help a team succeed with a technology to which the team is already committed. If you're well down the path of implementing your system in Erlang, it's great to have an Erlang enthusiast around who can tell you about Erlang Conf East, or what's happening at the Erlang user group, or what libraries the Erlang community is talking about.

Enthusiasts pair well with Worker Bees, who may gain energy from the enthusiasm, and learn from the Enthusiast's constant stream of well-meaning knowledge. They can have productive discussions with Builders, but should probably not be kept in close quarters for too long. Rarely do an enthusiasts and Dependency Minimalists see eye-to-eye. They get along well with Architecture Astronauts, but nothing good can come from this union.

If an enthusiast is channeled well, they will act as a font of enthusiastic energy and useful knowledge to the team. A misdirected enthusiast with too much power will make arbitrary decisions that hurt the company and dismay the team.

Sample Conversations
"ASP.NET allows you to drag-and-drop UIs!"
"The iPhone API is composed of HTML, Javascript and Ajax."
"Macs aren't really that much more expensive, when you consider what you're getting."
"Smoking cuts ten years off your life."
"'Well you know. Smoking takes ten years off your life.' Well it's the ten worst years, isn't it folks? It's the ones at the end! It's the wheelchair kidney dialysis fucking years. You can have those years! We don't want 'em, alright!?"
"I'm thirty times more productive in Ruby on Rails than I ever was in Java."

Well-Known Examples
Apple customers. Ruby zealots. Bruce Tate.

Related Patterns
  • Early Adopter: Like an enthusiast, but the enthusiasm is applied broadly to 'all things new' and tails off as the pattern-exhibitor grows bored of each item.
  • Hawthorne Effectors: Not necessarily enthusiastic about technologies, but exhibit improvements in work when confronted with change.
  • Cargo Cultist: Adopting enthusiasm for practices or technologies without understanding the reasons behind the enthusiasm.

Wednesday, August 15, 2007

CodeGear: Red Diamond Non-Disclosable

Well, in order to download a Beta for CodeGear's Ruby IDE (based on DLTK, code-named Red Diamond), I have to agree not to disclose information about it, including its existence and features. So instead of telling you about it, and how much it adds on top of DLTK, I will, instead, pretend it doesn't exist. If you ask me to suggest an IDE for Ruby code, I will not mention this one, even if it's better. That's what they want.

Wednesday, August 8, 2007

Eclipse: Go to Implementation

I've been pairing with one of my colleagues at The FeedRoom today (hi, Ivan!), and he passed on a plugin I've failed to notice before: Implementors.

Basically, it allows you to jump to the implementation of a method; really handy when you've got Interface/Implementation splits in your code and you know a particular method falls into that bucket, and you want to go to its implementation rather than the interface your code points to.

If there's more than one implementation, it brings up a context-menu of all the implementations it can find. If the interface in question is, say, java.util.List, this can take a while, so be patient.

Really handy; I imagine I'll be using that a lot.

Ruby on Rails on Bamboo: Unit Tests

In my previous post about Continuously Integrating Ruby on Rails with Bamboo, I walked through a simple setup for a Ruby on Rails project in Bamboo. That setup is a good starting point, but it's missing useful elements, like understanding the unit tests within the project, and test coverage:

Unit Testing Ruby on Rails with Bamboo
Let's start with unit tests. Bamboo understands the JUnit XML format, which Ruby doesn't normally emit (what with not using JUnit and all). Fortunately, someone's written a Rails plugin called ci_reporter that emits these files for us.

$ gem install ci_reporter
Bulk updating Gem source index for:
Successfully installed ci_reporter-1.3.3
Installing ri documentation for ci_reporter-1.3.3...
Installing RDoc documentation for ci_reporter-1.3.3...
$ script/plugin install
+ ./ci_reporter/tasks/ci_reporter.rake
$ svn add vendor/plugins/ci_reporter/
A vendor/plugins/ci_reporter
A vendor/plugins/ci_reporter/tasks
A vendor/plugins/ci_reporter/tasks/ci_reporter.rake
$ svn ci -m "Added CI reporter to project."
Adding vendor/plugins/ci_reporter
Adding vendor/plugins/ci_reporter/tasks
Adding vendor/plugins/ci_reporter/tasks/ci_reporter.rake
Transmitting file data .
Committed revision 411.

You'll probably have to ensure the ci_reporter gem is installed on your instance of Bamboo as well. Once this is complete, you'll want to modify your Plan:

And build the project:

See? Tests! Bamboo can find the XML files generated in test/reports, and use them to understand the testing of your Ruby on Rails application. Now, in a Java/JUnit project, Bamboo does a fine job of turning the test methods into readable names, but for now that's not true with Test::Unit test names, but they're working on it.

Next step: Coverage. But that'll be another day.

Tuesday, August 7, 2007

Hpricot Limitations

Well, after fighting with Cygwin and Hpricot for a few days, I've given in and dropped Cygwin from my stack for the Ruby/Windows testing for now.

Within five minutes of doing that, I was able to get something done with Hpricot, and bump into its limitations within a minute or so after that.

For example:

irb(main):001:0> require 'rubygems'
=> false

irb(main):002:0> require 'hpricot'
=> true

irb(main):003:0> require 'open-uri'
=> true

irb(main):004:0> doc = Hpricot(open("
=> #<Hpricot::Doc {doctype "<!DOCTYPE html\n" " PUBLIC \"-//W3C//DTD XHTML 1.


"> "" </a>} "\n " </p>} "\n" </div>} "\n\n\n\n "} </bod
y>} "\n" </html>} "\n\n">

irb(main):005:0> (doc/"//img")
=> #<Hpricot::Elements[{emptyelem <img src="/hpricot/chrome/site/images/hpricot-
small.png" alt="hpricot">}, {emptyelem <img src="/hpricot/chrome/common/trac_log
o_mini.png" height="30" alt="Trac Powered" width="107">}]>

irb(main):006:0> (doc/"//img[@alt='hpricot'")
=> #<Hpricot::Elements[{emptyelem <img src="/hpricot/chrome/site/images/hpricot-
small.png" alt="hpricot">}, {emptyelem <img src="/hpricot/chrome/common/trac_log
o_mini.png" height="30" alt="Trac Powered" width="107">}]>

irb(main):007:0> (doc/"//img[@alt='hpricot']")
=> #<Hpricot::Elements[{emptyelem <img src="/hpricot/chrome/site/images/hpricot-
small.png" alt="hpricot">}]>

irb(main):008:0> (doc/"//a/img[@alt='hpricot']")
=> #<Hpricot::Elements[{emptyelem <img src="/hpricot/chrome/site/images/hpricot-
small.png" alt="hpricot">}]>

irb(main):009:0> (doc/"//a[img/@alt='hpricot']")
=> #<Hpricot::Elements[]>

There's nothing wrong with the second query; it's a valid XPath expression, it's just not supported by jQuery, which means it isn't supported by Hpricot. Too bad; I guess i'll have to work around the syntax limitations.