O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Building Mashup-Friendly Sites in Rails

by Jack Herrington
12/04/2007

Your web services interface, be it in whatever format—XML/RPC, SOAP, even JSON—is not mashup friendly. Let me ask you a question, what is the gold standard of mashupable widgets? For many that would have to be Google Maps, arguably the first truly mashupable widget that really took off. And why did it take off? Because I can mashup with Google Maps using just Notepad (or TextEdit, TextMate, whatever) and a browser. I get my developer key, copy in a little JavaScript code, and away I go.

So you are probably thinking to yourself, "Fine, I can use Ajax from my web page to get data from my web services interface and then put it into maps." Not true. Because of the security model in most of the browsers, a web page can only make an Ajax request to pages in the same domain. Have a look at Figure 1.

figure 1
Figure 1. Getting to the proxied service

Let's say you have a web page and a Rails service located somewhere else that you want to get access to. (Actually, it doesn't have to be Rails, it can be anything, but I'll use Rails as an example.) As you see in Figure 1, the page itself cannot directly make a call to get the pages from the Rails service because it's hosted in a different domain. To make the call the page will have to go back to a proxy service located on a server in the same domain and have it make the request, then return the data.

This is why the widget sites, like Microsoft Live or iGoogle, all have proxies built into them: so that people can access their SOAP, XML/RPC, REST, or JSON interfaces without having to build a mashup-friendly interface.

In this article I'm going to take a page from the Google Maps playbook and show you how to build a web services interface that is mashup friendly, where the only tools you need to get to the data on your Rails server is Notepad and a browser, just like you see in Figure 2.

figure 2
Figure 2. Building the easily mashupable interface

That way you can display data from your Rails service on blogs, and static HTML pages, and who knows whatever else from anywhere on the Web.

I've chosen Rails for this example for two reasons: first, it's cool and second, it's easy. What I'm doing here you could easily do in PHP, Java, .NET, or any web server technology. I'll start us out by creating the Rails site, and then I'll show you a progression of different transport mechanisms, starting with the familiar Ajax/XML pattern and ending with a mashup-friendly script tag transport mechanism.

Building the Rails Service

To demonstrate building a mashup-friendly web service I'll build a simple Rails application. The application will store a list of images along with description, title, and location in latitude and longitude. The Rails code to create the database is shown in Listing 1.

Listing 1. The database definition
class CreatePhotos <  ActiveRecord::Migration
   def self.up
     create_table :photos do |t|
       t.column  :title, :string, :null => false
       t.column  :description, :string, :null => false
       t.column  :url, :string, :null => false
       t.column  :latitude, :double, :null => false
       t.column  :longitude, :double, :null => false
     end
   end

  def self.down
   drop_table :photos
  end
end

Once the database is set up, we have to have a model to represent the individual photos. That model definition is shown in Listing 2.

Listing 2. The Photo Model
class Photo <  ActiveRecord::Base
end

There isn't much to it. I could add some constraints, but the application itself isn't the point of this article. The interesting stuff starts in the controller, which is shown in Listing 3.

Listing 3. The Photos Controller
class PhotosController  < ApplicationController
 scaffold :photo
 
 def xml
   render( :content_type => 'text/xml',
   :text => Photo.find(:all).to_xml() )
 end

 def json
   render( :content_type =>  'text/javascript',
   :text => Photo.find(:all).to_json() )
 end

 def jscallback
   callback = 'photos_callback'
   callback = params['cb'] if ( params['cb']  != nil )
   render( :content_type =>  'text/javascript',
   :text =>  "#{callback}(#{Photo.find(:all).to_json()});" )
 end
end

At the top is the usual Rails scaffolding, which I will use to add some images into the database. The list of images I've entered into the database is shown in Figure 3.

figure 3
Figure 3. The scaffolding with the list of images

Below the scaffolding invocation are the three key view methods; xml, json, and jscallback. The 'xml' method returns all of the records from the database encoded as XML. The 'json' method does the same thing, but returns the records encoded in JavaScript Object Notation (JSON) format by using the ever handy 'to_json' call.

The third method, 'jscallback', creates some JavaScript code that invokes a JavaScript function on the client and supplies as an argument the array of data from the database. By default, the name of the callback is 'photos_callback', but the caller can specify whatever function call name they like by adding the 'cb' argument to the URL.

It's the 'jscallback' method that makes the application mashup friendly. But in order to explain why, I need to start with the basic Ajax/XML pattern that we are familiar with.

Accessing the Server Using Ajax and XML

The first way I'm going to access the server is using Ajax calls to the XML view. To start off, I'll request the XML data from the server on the command line using curl. This is shown in Listing 4.

Listing 4. The XML Response
% curl  "http://0.0.0.0:3000/photos/xml"
<?xml  version="1.0" encoding="UTF-8"?>
  <photos>
    <photo>
      <description>Megan after winning 2nd  place in the pumpkin race.</description>
      <id  type="integer">1</id>
      <latitude  type="float">37.591753</latitude>
      <longitude  type="float">-122.048358</longitude>
      <title>Megan At The  Races</title>
      <url>http://0.0.0.0:3000/megan1.jpg</url>
     </photo>
 ...

The output of 'to_xml' is remarkably well formatted. There is a 'photos' tag that contains a set of 'photo' tags, each of which has sub-tags for the different attributes. Perfect.

To make use of that, I'll mash the data up with a Google Map. To start with I'll go to the Google Maps API page and request an API key. Then I'll write the view code shown in Listing 5.

Listing 5. The Ajax/XML map implementation
 <html>
 <head>
 <title>Map Ajax Example</title>
 <%= javascript_include_tag 'prototype'  %>
 <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=<Maps  Key>"
 type="text/javascript"></script>
 <script  type="text/javascript">
 function buildMarker(  title, url, description, latitude, longitude ) {
 var marker = new  GMarker(new GLatLng(latitude, longitude),
 { title: title,  clickable: true });

GEvent.addListener(marker,  "click", function() {
   var html =  '<strong>'+title+'</strong><br/><img  height="200" src="'+url+'" />';
   marker.openInfoWindowHtml( html );
   });

return marker;
   }

function load() {
   if (GBrowserIsCompatible()) {
   var map = new  GMap2(document.getElementById("map"));

    new Ajax.Request( '/photos/xml', {
   method: 'get',
   onSuccess: function(  transport ) {
   var photoTags =  transport.responseXML.getElementsByTagName( 'photo' );

for( var b = 0; b <  photoTags.length; b++ ) {
   var title =  photoTags[b].getElementsByTagName('title')[0].firstChild.nodeValue;
   var url =  photoTags[b].getElementsByTagName('url')[0].firstChild.nodeValue;
   var description = photoTags[b].getElementsByTagName('description')[0].firstChild.nodeValue;
   var latitude = parseFloat(  photoTags[b].getElementsByTagName('latitude')[0].firstChild.nodeValue );
   var longitude = parseFloat(  photoTags[b].getElementsByTagName('longitude')[0].firstChild.nodeValue );

 if ( b == 0 )
   map.setCenter(new GLatLng(latitude,  longitude), 13);
   map.addOverlay( buildMarker( title, url,  description, latitude, longitude ) );
   } } } );
   }
   }
   </script>
   </head>
   <body onload="load()"  onunload="GUnload()">
   <div id="map"  style="width: 95%; height: 95%"></div>
   </body>
   </html>

The first couple of lines at the top are pretty important. The 'javascript_include_tag' invocation includes the Prototype library into the page. I'll use the Ajax.Request function from the Prototype library to get the XML from the server. The next script tag is the Google Maps script tag, which brings in the map objects.

The two functions that are specified after this are 'buildMarker' and 'load'. The 'load' function creates the map and starts the Ajax request. The 'buildMarker' function is used by the Ajax request handler to add markers to the map for each of the images. These markers will then pop up an information window with the image when you click on them.

The completed page is shown in Figure 4.

figure 4
Figure 4. The map mashup

Here I have clicked on the picture of my daughter that I took at her first race. She came in second place! A natural born runner.

Now, back to the code for a second, if you take a look at the code in the function within Ajax.Request you can see where I break up the XML and get out the various fields for each marker. It's a pain to do that sort of stuff. It would be far easier if the server just returned some JSON data that I could convert into an array quickly.

The JSON Version of the Mapper

Thankfully, we have a controller method that will return JSON. I'll test it once again using curl on the command line. The output is in Listing 6.

Listing 6. The JSON output
 % curl  "http://0.0.0.0:3000/photos/json"
 [{attributes: {latitude:  "37.591753", title: "Megan At The Races", url:  "http://0.0.0.0:3000/megan1.jpg", id: "1", description:  "Megan after winning 2nd place in the pumpkin race.", longitude:  "-122.048358"}}, ...

Very cool. Now I can change just the load function from the previous example to use JSON instead of XML. This change is shown in Listing 7.

Listing 7. The JSON loader
 function load() {
 if (GBrowserIsCompatible()) {
 var map = new  GMap2(document.getElementById("map"));

    new Ajax.Request( '/photos/json', {
   method: 'get',
   onSuccess: function(  transport ) {
   var photos = eval(  transport.responseText );

for( var b = 0; b <  photos.length; b++ ) {

 if ( b == 0 )
   map.setCenter(new  GLatLng(photos[b].attributes.latitude,
   photos[b].attributes.longitude), 13);

 map.addOverlay( buildMarker(  photos[b].attributes.title, 
   photos[b].attributes.url, photos[b].attributes.description,
   photos[b].attributes.latitude, photos[b].attributes.longitude )  );
   } } } );
   }
   }

All right, now this is much easier. Instead of doing all of the XML stuff I just 'eval' the responseText and that gives me back an array of photos which I can then iterate to create markers.

Another way to do this is to have the returned JavaScript call a function directly. I do that by using the 'jscallback' method on the controller. You can see the output of that in Listing 8.

Listing 8. The JavaScript callback output
 % curl  "http://0.0.0.0:3000/photos/jscallback"
 photos_callback([{attributes:  {latitude: "37.591753", title: "Megan At The Races", ...

Now my 'load' function becomes really tiny because the bulk of the photo handling code moves into a new function called 'photos_callback'. This new version of the code is shown in Listing 9.

Listing 9. The Photos Callback
 var map = null;

function  photos_callback( photos ) {
   for( var b = 0; b < photos.length; b++ ) {

   if ( b == 0 )
   map.setCenter(new GLatLng(  photos[b].attributes.latitude,
   photos[b].attributes.longitude ), 13);

   map.addOverlay( buildMarker(  photos[b].attributes.title, 
   photos[b].attributes.url,  photos[b].attributes.description,
   photos[b].attributes.latitude,  photos[b].attributes.longitude ) );
   }
   }

function load() {
   if (GBrowserIsCompatible()) {
   map = new  GMap2(document.getElementById("map"));

    new Ajax.Request( '/photos/jscallback', {
   method: 'get',
   onSuccess: function( transport ) {  eval( transport.responseText ); }
   } );
   }
   }

Now comes the fun part. What if I didn't want to use the Ajax.Request method at all? What if I couldn't use the Ajax.Request because the data that I wanted is on a server in a different domain? Well, it turns out there is a solution for that. Something that pre-dates Ajax altogether

Using Dynamic Script Tags

The solution is to dynamically add a script tag to the DOM that has the URL of the service as the 'src' of the tag. The only thing I need to change is the 'load' code and that's shown in Listing 10.

Listing 10. Using a script tag instead of Ajax
 function load() {
 if (GBrowserIsCompatible()) {
 map = new  GMap2(document.getElementById("map"));

    var elScript =  document.createElement('script');
   elScript.src =  'http://0.0.0.0:3000/photos/jscallback';
   document.body.appendChild( elScript );
   }
   }

Here is the trick, Ajax can only access servers located in the same domain. But 'script' tags can point anywhere. So I can now put this code on any page in any domain and it will still work (given that the URL is something it can get to and not 0.0.0.0:3000).

But I don't really want my users having to know all of this script tag creation stuff. So I am going to, like Google, hide all of the code in a library to make it easier for people to use the code. This simple library is shown in Listing 11.

Listing 11. The photo library
 var g_photos_cb = [];

function  photos_callback( photos ) {
   while( g_photos_cb.length > 0 ) {
   var cb = g_photos_cb.pop();
   cb( photos );
   }
   }
   function photos_get( cb  ) {
   g_photos_cb.push( cb );
   var elScript =  document.createElement('script');
   elScript.src =  'http://0.0.0.0:3000/photos/jscallback';
   document.body.appendChild( elScript );
   }

The user of the library calls 'photos_get' and gives it a function that will take the photos data and do something with it. The method is pretty simple; it just takes the callback and adds it to an array of callbacks, then does the same script tag creation as I did before. The callback then invokes the different user-defined callbacks with the photos data.

To use it, I will change the load method one last time and remove the original photos_callback from the code. This updated code is shown in Listing 12.

Listing 12. The photo library in use
 function load() {
 if (GBrowserIsCompatible()) {
 map = new GMap2(document.getElementById("map"));
 
 photos_get( function( photos ) {
 for( var b = 0; b < photos.length; b++  ) {

       if ( b == 0 )
   map.setCenter(new GLatLng(  photos[b].attributes.latitude,
   photos[b].attributes.longitude ),  13);

       map.addOverlay( buildMarker(  photos[b].attributes.title, 
   photos[b].attributes.url,  photos[b].attributes.description,
   photos[b].attributes.latitude,  photos[b].attributes.longitude ) );
   }
   } );
   }
   }

Now there is one problem with this library. Internet Explorer has a nasty habit of blowing up on pages where you add tags to the body tag before the page has been completely loaded. To avoid that I'll change the code a little bit to harden it up for IE. This final version of the mashup friendly photos library is shown in Listing 13.

Listing 13. A hardened photos library
 var g_photos_cb = [];
 var g_photos_loaded =  false;

window.addEventListener(  'load', photos_onload );

function photos_onload()  {
   photos_process_requests();
   g_photos_loaded = true;
   }

function  photos_process_requests() {
   if ( g_photos_cb.length > 0 )
   {
   var elScript =  document.createElement('script');
   elScript.src =  'http://0.0.0.0:3000/photos/jscallback?d='+(new Date().valueOf());
   document.body.appendChild( elScript );
   }
   }

function  photos_callback( photos ) {
   while( g_photos_cb.length > 0 ) {
   var cb = g_photos_cb.pop();
   cb( photos );
   }
   }

function photos_get( cb  ) {
   g_photos_cb.push( cb );
   if ( g_photos_loaded ) photos_process_requests();
   }

The code latches on to the 'onload' message on the window. Then it adds script tags only once the onload message has been sent. If the onload message has already been sent, then it just sends the request right on to the server.

This completed photos library is a very mashup-friendly web services interface. Anyone with Notepad and a browser can now write JavaScript code that can access data on the Rails server from anywhere in the world.

Some Tips and Suggestion

I've written a couple of these interfaces, so I have a few things I'd like to recommend to save you a little pain. The first is to write your interface to basic Dynamic HTML without using any libraries like Prototype, Dojo, Yahoo! UI, or any of the others. Those libraries can be incompatible with each other and you don't want to exclude anyone from using your interface.

The other important thing is to figure out whether or not you even need this type of interface. This type of API is ideal when your target customers are bloggers, HTML or JavaScript coders, or folks who may not control their server environment. It's not a good general purpose API to replace conventional SOAP, XML/RPC, REST, or JSON APIs. Those APIs are far better when your customers are writing server code.

Another thing to keep in mind is that this interface is good for creating read-only APIs. It's not particularly easy to write writeable APIs as 'script' tags only GET, they don't POST. Nor is it particularly straightforward to authenticate using 'script' tags, though it is possible.

Conclusions

Hopefully the example I've walked you through here has shown you how to build an API that works well for getting data to page-level coders for mashups. If it inspires you to build a mashup-friendly API, let me know by posting to the comments section at the end of the article. I'd love to see what you come up with, and you may just get a few more people using your API.

Jack Herrington is an engineer, author and presenter who lives and works in the Bay Area. His mission is to expose his fellow engineers to new technologies. That covers a broad spectrum, from demonstrating programs that write other programs in the book Code Generation in Action. Providing techniques for building customer centered web sites in PHP Hacks. All the way writing a how-to on audio blogging called Podcasting Hacks.


Return to the O'Reilly Ruby.

Copyright © 2009 O'Reilly Media, Inc.