Ruby, Software, Web

Serve JSON Remotely with Rails and JSONP

Have you ever found yourself needing to query data from another domain in Javascript? Because data in Javascript is usually fetched via AJAX, you will find yourself inside the Javascript sandbox of the Same Origin Policy (SOP). This leads you to immediately finding a way to circumvent this security via something like a proxy server. Fortunately for us, it is well known that the script tag’s “src” attribute is not limited to this restriction. Using this behavior, we can easily query data from across domains without the use of a proxy. The concept that I will be showing below is JSONP proposed by Bob Ippolito

To start with, lets take a look at how JSONP works. As a great example, go to this URL and look at the very beginning of the JSON response. It leads right into the JSON. Now, lets go to the same URL, with a callback variable in the query string. Try this link. Now you can see that the JSON result is wrapped inside of “test(…)”. This is padding the JSON result, hence the name. Because this is wrapped inside a function call, when the data is returned to our requesting resource, it will be passed to a local function with the same name – in this case “test”. So, we can write something like this:

function test(data) {
  console.log(data);
}

This would echo the result of the request out to our Javascript console.

To see a fully working example, lets include our Javascript library on the requesting end. If you are using JQuery, this functionality is built in, using the “callback=?” functionality, and an inline method. Congratulations – you are one of the cool people and you are done for a second. If you are using Prototype.js, you will need to extend its functionality with a community contributed patch, such as this one. Just include this in a separate file, or merge and minify, etc. Whatever makes you happy.

Lets talk about the server for a minute. If you are running Rails, lets setup a sample controller method that we can access. For example, I have the following:

# app/controllers/users_controller.rb
def show
  respond_to do |format|
    format.json do 
      render :inline => "#{params[:callback]('testing JSONP')" 
    end
  end
end

You can see that this action, show will respond to a Javascript request, and pad the result with the name of our callback variable. More on this in a second.

Now, lets fire up the server, and test out or new code. On the requesting end, do the following:

getJSON('http://path/to/server/users/show?callback=?',
  function(data) {
    console.log(data);
  }
);

We have taken the Javascript function from earlier, and made it inline, with our getJSON call. Change your URL path to reflect your Rails server. You should get a response like below if you go to the Rails server directly in your browser:

container('testing JSONP')

Now, within your requesting page, when you reload you should see the response ‘testing JSONP’ in your console log. Congratulations, you have just served JSON cross-domain without going to jail for breaking the SOP law.

It turns out that Rails handles this beautifully. Per the documentation for render:

Sometimes the result isn’t handled directly by a script (such as when the request comes from a SCRIPT tag), so the :callback option is provided for these cases.    
# Renders ‘show({“name”: “David”})’    
render :json => {:name => “David”}.to_json, :callback => ‘show’

We can refactor our controller to look like this instead:

# app/controllers/users_controller.rb
def show
  respond_to do |format|
    format.json do 
      render :inline => 'testing JSONP', :callback => params[:callback]
    end
  end
end

Rail’s render will pad the JSON response if a callback variable is present, or just serve the JSON response otherwise. Happy cross-site scripting!

Computers

ActiveRecord’s Secret find_by_sql Results

Ruby on Rails logo Well, its not exactly a secret. It sure isn’t well documented however. Recently, I wanted to return a query that spanned multiple database tables. I decided to go with find_by_sql because of the mind-blowing idiocy with which this legacy database was structured. I will take a watered down version of what I was attempting to do to demonstrate how we can expose some “hidden” functionality of ActiveRecord’s find_by_sql method.

 
Channel table:
------------------------------------
id | title  | description        | user_id
------------------------------------
1  | first | the first channel | 1

User table:
-----------
id | name
-----------
1  | ben

After I constructed my find_by_sql query, it looked something like this:

Channel.find_by_sql("SELECT a.*, b.name
FROM channel a, user b
WHERE a.user_id = b.id")

This query selects all columns from table a (channel), and a single column from table b (user). This is pretty standard, as many queries need to gather values from multiple table columns in a single SELECT operation.

Running this query, you will receive an array of Channel instances with all the attributes filled in for the channel model. Missing however, will be the attributes from any table other than “Channel”:

Channel.find_by_sql("SELECT a.*, b.name
FROM channel a, user b
WHERE a.user_id = b.id")
=> "[#]"

Notice how the “name” column from table b (user) is not present in the display? You can even query this attribute directly:

c = Channel.find_by_sql("SELECT a.*, b.name
FROM channel a, user b
WHERE a.user_id = b.id")
=> "[#]"
c[0].name
=> NoMethodError: undefined method 'name' for #

We could create an attr_accessor for the Channel class, and this would resolve the NoMethodError, but it still won’t be populated for our Channel instance after a find_by_sql.

After some digging around in the source code, and online, I came across this posting, which made the brilliant suggestion of looking in channel.attributes. This method will list an array of attributes that ActiveRecord knows about. Take a look at channel.attributes.keys:

c.attributes.keys
=> ["id", "title", "description", "user_id", "name"]

There it is! Our “missing” name attribute from the SELECT query has been located. Accessing the value for this attribute is trivial:

c.attributes["name"]
=> "ben"

We can do this with as many “extra” columns as we want. If two column names conflict (say channels had a column “name”, and users also had a column “name”), the database will return “name”, and “name_1” respectively. This is a really powerful feature of ActiveRecord that will encourage people to stick with the ORM, since they can still write SQL in a pinch.

Bonus: Customizing .to_json to include find_by_sql attributes

In the preceding example, the attribute “name” would not be included in the output of a “.to_json” call, as in the following example:

c.attributes.keys
=> ["id", "title", "description", "user_id", "name"]
c.to_json
=> "{"channel":{"id":1,"title":"first","description":"The first channel"}}"

This is where we can customize what is included in our JSON output. This article showed me that you can use the :methods argument with to_json to explicitly include any custom attributes, such as those that are attr_accessor objects in your class. When passing in the :methods argument, I must specify which attributes to include:

c.attributes.keys
c.to_json
=> "{"channel":{"id":1,"title":"first","description":"The first channel"}}"
c.to_json :methods => :name
=> "{"channel":{"id":1,"title":"first","description":"The first channel","name":"ben"}}"

Good job Rails team! No ugly hacks, or overrides needed today.