JSON with Ruby on Rails on Google AppEngine

-

Getting Started

Here's a list of what you'll need to get started with the demo:

  • a Google AppEngine account
  • Ruby 1.8.6
  • Rails 2.3.5
  • Ruby on Rails AppEngine Gem

Instructions

The first steps that you'll want to follow are from John Woodell's Gist on Rails 2.3.5 on App Engine with DataMapper.

As you'll notice, the last couple of steps generate a RESTful resource called Contact and also it's associated DataMapper Model.

Adding REST

Now in order to add JSON to the mix, we'll need a couple more things:

We only have to make a few very small changes to the demo app that you got from the Gist to get JSON integrated. They're listed below, and also at this Gist.

Gemfile changes: adding the json-jruby and dm-serializer to the gems for bundler

[sourcecode language="ruby"]

Critical default settings:

disable_system_gems disable_rubygems bundle_path '.gems/bundler_gems'

List gems to bundle here:

gem 'rails_dm_datastore' gem 'rails', "2.3.5" gem "json-jruby" gem "dm-serializer" [/sourcecode]

config.ru changes: adding require 'dm-serializer' and 'json'

[sourcecode language="ruby"] require 'appengine-rack' require 'cgi' require 'json' require 'dm-core' require 'dm-serializer'

AppEngine::Rack.configure_app(

:application => 'iphone-json',
:precompilation_enabled => true,
:sessions_enabled => true,
:version => "1")

AppEngine::Rack.app.resource_files.exclude :rails_excludes ENV['RAILS_ENV'] = AppEngine::Rack.environment

deferred_dispatcher = AppEngine::Rack::DeferredDispatcher.new(

 :require => File.expand_path('../config/environment', __FILE__),
 :dispatch => 'ActionController::Dispatcher', :isolate => true)

map '/admin' do use AppEngine::Rack::AdminRequired run deferred_dispatcher end

map '/user' do use AppEngine::Rack::LoginRequired run deferred_dispatcher end

map '/' do run deferred_dispatcher end [/sourcecode]

and finally, the contacts_controller.rb needs the corresponding format.json render statements.

[sourcecode language="ruby"] class ContactsController < ApplicationController # GET /contacts # GET /contacts.xml # GET /contacts.json def index

@contacts = Contact.all

respond_to do |format|
  format.html # index.html.erb
  format.xml  { render :xml => @contacts }
  format.json { render :json => @contacts }
end

end

# GET /contacts/1 # GET /contacts/1.xml # GET /contacts/1.json def show

@contact = Contact.find(params[:id])

respond_to do |format|
  format.html # show.html.erb
  format.xml  { render :xml => @contact }
  format.json { render :json => @contact }
end

end

# GET /contacts/new # GET /contacts/new.xml # GET /contacts/new.json def new

@contact = Contact.new

respond_to do |format|
  format.html # new.html.erb
  format.xml  { render :xml => @contact }
  format.json { render :json => @contact }
end

end

# GET /contacts/1/edit def edit

@contact = Contact.find(params[:id])

end

# POST /contacts # POST /contacts.xml # POST /contacts.json def create

@contact = Contact.new(params[:contact])

respond_to do |format|
  if @contact.save
    flash[:notice] = 'Contact was successfully created.'
    format.html { redirect_to(@contact) }
    format.xml  { render :xml => @contact, :status => :created, :location => @contact }
    format.json { render :json => @contact, :status => :created, :location => @contact }
  else
    format.html { render :action => "new" }
    format.xml  { render :xml => @contact.errors, :status => :unprocessable_entity }
    format.json { render :json => @contact.errors, :status => :unprocessable_entity }
  end
end

end

# PUT /contacts/1 # PUT /contacts/1.xml # PUT /contacts/1.json def update

@contact = Contact.find(params[:id])

respond_to do |format|
  if @contact.update_attributes(params[:contact])
    flash[:notice] = 'Contact was successfully updated.'
    format.html { redirect_to(@contact) }
    format.xml  { head :ok }
    format.json { head :ok }
  else
    format.html { render :action => "edit" }
    format.xml  { render :xml => @contact.errors, :status => :unprocessable_entity }
    format.json { render :json => @contact.errors, :status => :unprocessable_entity }
  end
end

end

# DELETE /contacts/1 # DELETE /contacts/1.xml def destroy

@contact = Contact.find(params[:id])
@contact.destroy

respond_to do |format|
  format.html { redirect_to(contacts_url) }
  format.xml  { head :ok }
  format.json { head :ok }
end

end end [/sourcecode]

Final Notes

  1. You may have noticed that by default you get RESTful XML when you generated the resource. It doesn't work without the needed dm-serializer. Remember, we're using DataMapper here and not ActiveResource. If you try, you'll get this beautiful error.
    SEVERE: [1272067889120000] RuntimeError (Not all elements respond to to_xml)

  2. If you inspect your DataMapper model, you'll notice that every field is a required field. This is key for when you're testing your POST method.

  3. The last thing that is currently being looked into, is that the POST requires you to pass in the Class in lowercase for it to properly work. I'm not sure if this is a case-sensitive issue, or just purely needing to change the param to be ":Contact" instead of ":contact" in the controller.

Source Code

  • The fully working demo can be found running on Google AppEngine at: iphone-json.appspot.com
  • source is available on Github.
  • And there is an accompanying iPhone REST client also available on Github. (Android client is currently in development.)

Alternatively, if you just want to quickly test your newly created JSON RESTful resource, you can use the REST Client for Firefox or REST Client for Google Chrome.