Google Data on Rails

Eric Bidelman, Google Data APIs Team
February 2009

Introduction

"Where's Ruby on the list of client libraries?"

Motivated by the ferocious appetite of our developers and the enduring popularity of Ruby on Rails (RoR), my colleague Jeff Fisher has forged a Ruby utility library from the fiery depths of Mount Doom. Mind you, it's not a full-blown client library, but it does handle the fundamentals like authentication and basic XML manipulation. It also requires you to work directly with the Atom feed using the REXML module and XPath.

Audience

This article is intended for developers interested in accessing the Google Data APIs using Ruby, specifically Ruby on Rails. It assumes the reader has some familiarity with the Ruby programming language and the Rails web-development framework. I focus on the Documents List API for most of the samples, but the same concepts can be applied to any of the Data APIs.

Getting Started

Requirements

Installing the Google Data Ruby Utility Library

To obtain the library, you can either download the library source directly from project hosting or install the gem:

sudo gem install gdata

Tip: For good measure, run gem list --local to verify that the gem was installed properly.

Authentication

ClientLogin

ClientLogin allows your application to programmatically log in users to their Google or G Suite account. Upon validating the user's credentials, Google issues an Auth token to be referenced in subsequent API requests. The token remains valid for a set length of time, defined by whichever Google service you're working with. For security reasons and to provide your users the best experience, you should only use ClientLogin when developing installed, desktop applications. For web applications, using AuthSub or OAuth is preferred.

The Ruby library has a client class for each of the APIs. For example, use the following code snippet to log in user@gmail.com to the Documents List Data API:

client = GData::Client::DocList.new
client.clientlogin('user@gmail.com', 'pa$$word')

The YouTube Data API would be:

client = GData::Client::YouTube.new
client.clientlogin('user@gmail.com', 'pa$$word')

See the full list of implemented service classes. If a service doesn't have a client class, use the GData::Client::Base class. As an example, the following code forces users to log in with a G Suite account.

client_login_handler = GData::Auth::ClientLogin.new('writely', :account_type => 'HOSTED')
token = client_login_handler.get_token('user@example.com', 'pa$$word', 'google-RailsArticleSample-v1')
client = GData::Client::Base.new(:auth_handler => client_login_handler)

Note: By default, the library uses HOSTED_OR_GOOGLE for the accountType. Possible values are HOSTED_OR_GOOGLE, HOSTED, or GOOGLE.

One of the downsides of using ClientLogin is that your application can be sent CAPTCHA challenges on failed login attempts. If that happens, you can handle the error by calling the clientlogin() method with its additional parameters: client.clientlogin(username, password, captcha_token, captcha_answer). Refer to the full Authentication for Installed Applications documentation for more information on dealing with CAPTCHAs.

AuthSub

Generating the AuthSubRequest URL

scope = 'http://www.google.com/calendar/feeds/'
next_url = 'http://example.com/change/to/your/app'
secure = false  # set secure = true for signed AuthSub requests
sess = true
authsub_link = GData::Auth::AuthSub.get_url(next_url, scope, secure, sess)

The previous block of code creates the following URL in authsub_link:

https://www.google.com/accounts/AuthSubRequest?next=http%3A%2F%2Fexample.com%2Fchange%2Fto%2Fyour%2Fapp&scope=http%3A%2F%2Fwww.google.com%2Fcalendar%2Ffeeds%2F&session=1&secure=0

You can also use the authsub_url method of the client object. Each service class has set a default authsub_scope attribute so there's no need to specify your own.

client = GData::Client::DocList.new
next_url = 'http://example.com/change/to/your/app'
secure = false  # set secure = true for signed AuthSub requests
sess = true
domain = 'example.com'  # force users to login to a G Suite hosted domain
authsub_link = client.authsub_url(next_url, secure, sess, domain)

The previous block of code creates the following URL:

https://www.google.com/accounts/AuthSubRequest?next=http%3A%2F%2Fexample.com%2Fchange%2Fto%2Fyour%2Fapp&scope=http%3A%2F%2Fdocs.google.com%2Ffeeds%2F&session=1&secure=0&hd=example.com

Upgrading a single-use token to a session token

AuthSub will redirect the user back to http://example.com/change/to/your/app?token=SINGLE_USE_TOKEN once they have granted access to their data. Notice that the URL is just our next_url with the single-use token appended as a query parameter.

Next, exchange the single-use token for a long-lived session token:

client.authsub_token = params[:token] # extract the single-use token from the URL query params
session[:token] = client.auth_handler.upgrade()
client.authsub_token = session[:token] if session[:token]

Secure AuthSub is very similar. The only addition is to set your private key before upgrading the token:

PRIVATE_KEY = '/path/to/private_key.pem'

client.authsub_token = params[:token]
client.authsub_private_key = PRIVATE_KEY
session[:token] = client.auth_handler.upgrade()
client.authsub_token = session[:token] if session[:token]

Note: To use secure tokens, make sure to set secure=true when requesting a single-use token. See Generating the AuthSubRequest URL above.

Token management

AuthSub provides two additional handlers, AuthSubTokenInfo and AuthSubRevokeToken for managing tokens. AuthSubTokenInfo is useful for checking the validity of a token. AuthSubRevokeToken gives users the option of discontining access to their data. Your app should use AuthSubRevokeToken as a best practice. Both methods are supported in the Ruby library.

To query a token's metadata:

client.auth_handler.info

To revoke a session token:

client.auth_handler.revoke

See the full AuthSub Authentication for Web Applications documentation for the full scoop on AuthSub.

OAuth

At the time of writing this article, OAuth has not been added to the GData::Auth module.

Using OAuth in the utility library should be relatively straightforward when using the Rails oauth-plugin or Ruby oauth gem. In either case, you'll want to create a GData::HTTP::Request object and pass it the Authorization header generated by each library.

Accessing feeds

GET (fetching data)

Once you've setup a client object, use its get() method to query a Google Data feed. XPath can be used to retrieve specific Atom elements. Here is an example of retrieving a user's Google Documents:

feed = client.get('http://docs.google.com/feeds/documents/private/full').to_xml

feed.elements.each('entry') do |entry|
  puts 'title: ' + entry.elements['title'].text
  puts 'type: ' + entry.elements['category'].attribute('label').value
  puts 'updated: ' + entry.elements['updated'].text
  puts 'id: ' + entry.elements['id'].text
  
  # Extract the href value from each <atom:link>
  links = {}
  entry.elements.each('link') do |link|
    links[link.attribute('rel').value] = link.attribute('href').value
  end
  puts links.to_s
end

POST (creating new data)

Use a client's post() method to create new data on the server. The following example will add new_writer@example.com as a collaborator to the document with id: doc_id.

# Return documents the authenticated user owns
feed = client.get('http://docs.google.com/feeds/documents/private/full/-/mine').to_xml
entry = feed.elements['entry']  # first <atom:entry>

acl_entry = <<-EOF
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gAcl='http://schemas.google.com/acl/2007'>
  <category scheme='http://schemas.google.com/g/2005#kind'
    term='http://schemas.google.com/acl/2007#accessRule'/>
  <gAcl:role value='writer'/>
  <gAcl:scope type='user' value='new_writer@example.com'/>
</entry>
EOF

# Regex the document id out from the full <atom:id>.
# http://docs.google.com/feeds/documents/private/full/document%3Adfrk14g25fdsdwf -> document%3Adfrk14g25fdsdwf
doc_id = entry.elements['id'].text[/full\/(.*%3[aA].*)$/, 1]
response = client.post("http://docs.google.com/feeds/acl/private/full/#{doc_id}", acl_entry)

PUT (updating data)

To update data on the server, use a client's put() method. The following example will update a document's title. It assumes you have a feed from a previous query.

entry = feed.elements['entry'] # first <atom:entry>

# Update the document's title
entry.elements['title'].text = 'Updated title'
entry.add_namespace('http://www.w3.org/2005/Atom')
entry.add_namespace('gd','http://schemas.google.com/g/2005')

edit_uri = entry.elements["link[@rel='edit']"].attributes['href']
response = client.put(edit_uri, entry.to_s)

DELETE

To delete an <atom:entry> or other data from the server, use the delete() method. The following example will delete a document. The code assumes you have a document entry from a previous query.

entry = feed.elements['entry'] # first <atom:entry>
edit_uri = entry.elements["link[@rel='edit']"].attributes['href']
client.headers['If-Match'] = entry.attribute('etag').value  # make sure we don't nuke another client's updates
client.delete(edit_uri)

Creating a new Rails application

Usually the first exercise in creating a new Rails app involves running the scaffold generators to create your MVC files. After that, it's running rake db:migrate to set up your database tables. However, since our application will be querying the Google Documents List API for data, we have little need for generic scaffolding or databases. Instead, create a new application and simple controller:

rails doclist
cd doclist
ruby script/generate controller doclist

and make the following changes to config/environment.rb:

config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
config.gem 'gdata', :lib => 'gdata'

The first line unhooks ActiveRecord from the application. The second line loads the gdata gem at startup.

Lastly, I chose to connect the default route ('/') to the documents action in DoclistController. Add this line to config/routes.rb:

map.root :controller => 'doclist', :action => 'all'

Start a controller

Since we didn't generate scaffolding, manually add an action called 'all' to the DoclistController in app/controllers/doclist_controller.rb.

class DoclistController < ApplicationController
  def all
    @foo = 'I pity the foo!'
  end
end

and create all.html.erb under app/views/doclist/:

<%= @foo %>

Fire up the web server & start development

You should now be able to start the default web server by invoking ruby script/server. If all is well, pointing your browser to http://localhost:3000/ should display 'I pity the foo!'.

Tip: Don't forget to remove or rename public/index.html.

Once you have things working, take a look at my final DoclistController and ApplicationController for the meat of the DocList Manager project. You'll also want to look at ContactsController, which handles the calls to the Google Contacts API.

Conclusion

The hardest part of creating a Google Data Rails app is configuring Rails! However, a close second is deploying your application. For that, I highly recommend mod_rails for Apache. It's super easy to setup, install, and run. You'll be up and running in no time!

Resources

Appendix

Examples

The DocList Manager is a full Ruby on Rails sample demonstrating the topics discussed in this article. The full source code is available from project hosting.