The Rails controller code can be used in either the Rails 2.3 or Rails 3 API. It maps the incoming requests to the appropriate model logic for storing and retrieving votes:
class VotesController < ApplicationController # create a new vote
# /api/v1/votes/entries/:entry_id/users/:user_id def create begin json = Yajl::Parser.parse(request.body.read) vote = Vote.create_or_update( :user_id => params["user_id"], :entry_id => params["entry_id"], :value => json["value"])
ptg
if vote.valid?
render :json => vote.to_json else
render :json => vote.errors.to_json, :status => 400 end
rescue => e
# log it and return an error
render :json => e.message.to_json, :status => 500 end
end
# GET /api/v1/votes/users/:user_id/up def entry_ids_voted_up_for_user
page = (params[:page] || 1).to_i per_page = (params[:per_page] || 25).to_i user_id = params[:user_id]
count = Vote.up.user_id(user_id).count
entry_ids = Vote.voted_up_for_user_id(user_id, page, per_page) data = { :total => count, :entries => entry_ids } data[:previous_page] = user_up_votes_url( :user_id => user_id, :page => page - 1,
:per_page => per_page) if page > 1 data[:next_page] = user_up_votes_url(
:user_id => user_id, :page => page + 1,
:per_page => per_page) if (page*per_page) < count render :json => data.to_json
ptg
# GET /api/v1/votes/users/:user_id/down def entry_ids_voted_down_for_user
page = (params[:page] || 1).to_i per_page = (params[:per_page] || 25).to_i user_id = params[:user_id]
count = Vote.down.user_id(user_id).count
entry_ids = Vote.voted_down_for_user_id(user_id, page, per_page) data = { :total => count, :entries => entry_ids } data[:previous_page] = user_down_votes_url( :user_id => user_id, :page => page - 1,
:per_page => per_page) if page > 1 data[:next_page] = user_down_votes_url(
:user_id => user_id, :page => page + 1,
:per_page => per_page) if (page*per_page) < count render :json => data.to_json
end
# GET /api/v1/votes/entries/totals?ids=1,2 def totals_for_entries
entry_ids = params["ids"].split(",")
data = entry_ids.inject({}) do |result, entry_id| result.merge!(entry_id => {
:up => Vote.up.entry_id(entry_id).count, :down => Vote.down.entry_id(entry_id).count })
ptg
render :json => data.to_json end
# GET /api/v1/votes/users/:user_id def votes_for_users
user_id = params["user_id"]
entry_ids = params["ids"].split(",")
data = entry_ids.inject({}) do |result, entry_id| vote = Vote.find_by_user_id_and_entry_id(user_id, entry_id) if vote result.merge!(entry_id => vote.value) else result end end
render :json => data.to_json end # GET /api/v1/votes/users/:user_id/totals def totals_for_user user_id = params["user_id"] data = { :up => Vote.up.user_id(user_id).count, :down => Vote.down.user_id(user_id).count }
render :json => data.to_json end
end
The create method is the action that creates the vote. If there are validation
errors, they are converted to JSON and returned with an HTTP response code of 400. There is also a general catchall for unexpected errors that returns with the exception
ptg message and an HTTP response code of 500. If all goes well, the API returns a
response code of 200 with the vote JSON in the body. To parse the JSON in the request body, the example uses the yajl-ruby library, but you can also use the JSON gem.
JSON Parsing with YAJL Ruby
There are many libraries for parsing JSON. The most com- monly used is Florian Frank’s JSON library (http://flori .github.com/json/). However, yajl-ruby (http://github .com/brianmario/yajl-ruby) is a more recently developed library that offers incredible performance and a streaming parser. It is a Ruby library that provides bindings to the C JSON library named YAJL (http://lloyd.github.com/yajl/).
Next are the methods for returning user up and down votes. The example keeps all the response assembly code in the controller to keep things visible. As a result, the controller actions have quite a bit of code in them for handling pagination. Another option for organizing this kind of logic is to use a presenter pattern. The important part of these methods is that links are included for previous and next pages, if needed. It’s a good idea to include some kind of pagination like this in any API call that can return a list. The API would also want to add limits on the page size later.
The Presenter Pattern
The presenter pattern offers an additional layer of abstrac- tion outside the standard MVC style. This additional abstraction is helpful when managing views based on state. In the case of the vote API, the state and alternate views are based on pagination. For more information on the presen- ter pattern, see http://blog.jayfields.com/2007/03/rails- presenter-pattern.html.
Finally, APIs return the vote information for a specific list of entry IDs. This example uses the GET method with the single parameter ids. For the response, it
ptg builds up a hash of entry ID to the corresponding vote information. Once the hash is
built, it is returned as JSON in the response.
The overall structure of this controller jumps out because it doesn’t map to the standard Rails RESTful CRUD style of development. These calls are fairly specific, and there are far more GETs than CREATEs. For the most part, the API is simply an
HTTP-facing interface for the model object. The only things that lie outside the model have to do with parsing requests and managing URIs.
For the simple needs of the vote service, the entire Rails framework is overkill. It doesn’t need any of the view or helper constructs. Further, the way it uses controllers seems a bit off. It doesn’t fit with the regular Rails paradigm. Let’s take a look at some offerings that are a bit more service focused or modular.
Sinatra
Sinatra is a lightweight web services framework built on top of Rack. It is designed for small, simple web applications and services, so it is a perfect fit for your vote service. You can use the same model as you did with your Rails implementation.
Sinatra doesn’t come with any built-in generators, so you have to generate your own application structure. However, the number of directories and the amount of support code needed is minimal. The example has to be able to run ActiveRecord migrations, load an environment, and support the Rack application loading style. The directory structure should like this:
votes-service config database.yml config.ru db migrate 20090920213313_create_votes.rb models vote.rb Rakefile service.rb
The setup and required files are very simple. Included are config, db, and models
ptg example. The HTTP services interface is defined in service.rb. Finally, the rackup
command and other web servers use the config.ru file to load the application.
With the basic structure created, it’s time to look at the implementation of the service in service.rb: require 'rubygems' require 'yajl' require 'active_record' require 'action_pack' require 'will_paginate' require 'sinatra'
# this line required to enable the pagination WillPaginate.enable_activerecord
require 'models/vote.rb'
class Service < Sinatra::Base configure do
env = ENV["SINATRA_ENV"] || "development"
databases = YAML.load_file("config/database.yml") ActiveRecord::Base.establish_connection(databases[env]) end
mime :json, "application/json" before do
content_type :json end
# create or update a vote
put '/api/v1/votes/entries/:entry_id/users/:user_id' do begin json = Yajl::Parser.parse(request.body.read) vote = Vote.create_or_update( :user_id => params["user_id"], :entry_id => params["entry_id"], :value => json["value"])
ptg if vote.valid? return vote.to_json else error 400, vote.errors.to_json end rescue => e
# log it and return an error error 500, e.message.to_json end
end
# return the entry ids the user voted up on get '/api/v1/votes/users/:user_id/up' do
page = (params[:page] || 1).to_i per_page = (params[:per_page] || 25).to_i user_id = params[:user_id]
count = Vote.up.user_id(user_id).count
entry_ids = Vote.voted_up_for_user_id(user_id, page, per_page) data = { :total => count, :entries => entry_ids } if page > 1 data[:previous_page] = "/api/v1/votes/users/#{user_id}/up?page=" + "#{page - 1}&per_page=#{per_page}" end if (page*per_page) < count data[:next_page] = "/api/v1/votes/users/#{user_id}/up?page=" + "#{page + 1}&per_page=#{per_page}" end
ptg
data.to_json end
# return the entry ids the user voted down on get '/api/v1/votes/users/:user_id/down' do
page = (params[:page] || 1).to_i per_page = (params[:per_page] || 25).to_i user_id = params[:user_id]
count = Vote.down.user_id(user_id).count
entry_ids = Vote.voted_down_for_user_id(user_id, page, per_page) data = { :total => count, :entries => entry_ids } if page > 1 data[:previous_page] = "/api/v1/votes/users/#{user_id}/down?page=" + "#{page - 1}&per_page=#{per_page}" end if (page*per_page) < count data[:next_page] = "/api/v1/votes/users/#{user_id}/down?page=" + "#{page + 1}&per_page=#{per_page}" end data.to_json end
# return the vote totals for a specific list of entries get '/api/v1/votes/entries/totals' do
ptg
data = entry_ids.inject({}) do |result, entry_id| result.merge!(entry_id => { :up => Vote.up.entry_id(entry_id).count, :down => Vote.down.entry_id(entry_id).count }) end data.to_json end
# return the users' vote for a specific list of entries get '/api/v1/votes/users/:user_id' do
user_id = params["user_id"] entry_ids = params["ids"].split(",")
data = entry_ids.inject({}) do |result, entry_id| vote = Vote.find_by_user_id_and_entry_id(user_id, entry_id) if vote result.merge!(entry_id => vote.value) else result end end data.to_json end
# return the total number of up and down votes for user get '/api/v1/votes/users/:user_id/totals' do user_id = params["user_id"] data = { :up => Vote.up.user_id(user_id).count, :down => Vote.down.user_id(user_id).count }
ptg
data.to_json end
end
First, the required libraries for Service are loaded. Inside the Service class itself
is a configure block that gets called when the service is initially loaded. It is here that
the ActiveRecord connection is set up. The MIME type JSON is then defined so it can be set on every response coming from this service in the before block.
The rest of service.rb is the full API interface. Unlike with Rails, with Sinatra,
routes are defined with the implementations of their handlers. For example, line 20 in this example defines the URI for creating a vote. It should be a PUT call that matches
the pattern /api/v1/votes/entries/:entry_id/users/:user_id.
There are two major differences between this code and the Rails code. First, the Sinatra service has dropped all the extra baggage around the service. Everything is con- tained in a single file. Second, the routes reside with the implementation of what those routes do. For a small service like the vote service, this single file can be referenced to get an idea of what the public interface looks like. It’s a single point of reference for the service interface.
Rack
This section looks at using the raw Rack interface for implementing the vote service. Rack was designed to wrap HTTP requests and responses in a very simple interface that is modular and versatile. It is on top of this simple unified API that Rails, Sina- tra, and many other frameworks are built. In fact, you could build your own simple service framework on top of Rack. The following example works with Rack directly.
As with Sinatra, with Rack there is a simple directory structure to the service. The entire service can be contained within a single file. For pure Rack applications, everything can be contained in a service.ru file. The rackup file becomes the actual
service: require 'rubygems' require 'yajl' require 'active_record' require 'action_pack' require 'will_paginate' WillPaginate.enable_activerecord
ptg require 'models/vote.rb' module Rack class VotesService def initialize(environment) dbs = YAML.load_file("config/database.yml") ActiveRecord::Base.establish_connection (dbs[environment]) end
# every request will enter here def call(env)
request = Rack::Request.new(env) path = request.path_info
begin
# return the vote totals for a specific list of entries if path == "/api/v1/votes/entries/totals"
ids = ids_from_params(request.params) return get_entry_totals(ids)
# create or update a vote
elsif path.start_with?("/api/v1/votes/entries") && path.end_with?("vote") && request.put?
entry_id, user_id = entry_id_and_user_id_from_path (path)
value = Yajl::Parser.parse(request.body.read) return process_vote(entry_id, user_id, value) # it's a request to get information for a user elsif path.start_with? "/api/v1/votes/users"
# get the users votes on specific entries if path.end_with? "votes"
ids = ids_from_params(request.params)
return get_user_votes(user_id_from_path(path), ids)
# get the entry ids a user voted down on elsif path.end_with? "down"
return get_down_votes(user_id_from_path(path), request.params)
ptg
# get the entry ids a user voted up on elsif path.end_with? "up"
return get_up_votes(user_id_from_path(path), request.params)
# get the up and down totals for a user elsif path.end_with? "totals"
return get_user_totals(user_id_from_path(path)) end
end rescue => e
# log it and return an error
return [500,{ 'Content-Type' => 'application/json' }, e.message.to_json]
end
[404, { 'Content-Type' => 'application/json' }, "Not Found".to_json]
end
def process_vote(entry_id, user_id, value) vote = Vote.create_or_update( :user_id => user_id, :entry_id => entry_id, :value => value) if vote.valid? [200, { 'Content-Type' => 'application/json' }, vote.to_json] else [400, { 'Content-Type' => 'application/json' }, vote.errors.to_json] end end def get_entry_totals(entry_ids)
ptg result.merge!(entry_id => { :up => Vote.up.entry_id(entry_id).count, :down => Vote.down.entry_id(entry_id).count }) end [200, {'Content-Type'=>'application/json'}, data.to_json] end
def get_up_votes(user_id, params)
page = (params[:page] || 1).to_i per_page = (params[:per_page] || 25).to_i count = Vote.up.user_id(user_id).count
entry_ids = Vote.voted_up_for_user_id(user_id, page, per_page) data = { :total => count, :entries => entry_ids } if page > 1 data[:previous_page] = "/api/v1/votes/users/#{user_id}/up?page=" + "#{page - 1}&per_page=#{per_page}" end if (page*per_page) < count data[:next_page] = "/api/v1/votes/users/#{user_id}/up?page=" + "#{page + 1}&per_page=#{per_page}" end [200, {'Content-Type'=>'application/json'}, data.to_json] end
ptg
def get_down_votes(user_id, params) page = (params[:page] || 1).to_i per_page = (params[:per_page] || 25).to_i count = Vote.down.user_id(user_id).count
entry_ids = Vote.voted_down_for_user_id(user_id, page, per_page) data = { :total => count, :entries => entry_ids } if page > 1 data[:previous_page] = "/api/v1/votes/users/#{user_id}/down?page=" + "#{page - 1}&per_page=#{per_page}" end if (page*per_page) < count data[:next_page] = "/api/v1/votes/users/#{user_id}/down?page=" + "#{page + 1}&per_page=#{per_page}" end [200, {'Content-Type'=>'application/json'}, data.to_json] end def get_user_totals(user_id) data = { :up => Vote.up.user_id(user_id).count, :down => Vote.down.user_id(user_id).count } [200, {'Content-Type'=>'application/json'}, data.to_json] end
ptg
def get_user_votes(user_id, entry_ids)
data = entry_ids.inject({}) do |result, entry_id| vote = Vote.find_by_user_id_and_entry_id(user_id, entry_id) if vote result.merge!(entry_id => vote.value) else result end end [200, {'Content-Type'=>'application/json'}, data.to_json] end def user_id_from_path(path) path.match(/.*users\/(.*)\/.*/)[1] end def entry_id_and_user_id_from_path(path) matches = path.match(/.*entries\/(.*)\/users\/ (.*)\/vote/) [matches[1], matches[2]] end def ids_from_params(params) params.has_key?("ids") ? params["ids"].split(",") : [] end end end
environment = ENV["RACK_ENV"] || "development" service = Rack::VotesService.new(environment) run service
The beginning of the rackup file looks very similar to the Sinatra service, with the
same required libraries. The VotesService class is inside the Rack module, but this is
ptg class is the call method. Every request that comes into Rack will invoke a call and pass
in the env variable.
The env variable consists of the regular CGI environment variables that hold
information about the request. Those variables can be accessed through the env hash,
but it’s easier to work with the Rack Request wrapper than the environment variables
directly. Request takes the env variables and wraps them in a friendlier interface. The
rest of the call method is a series of statements that determine which request is being made to the API.
To make things a little more readable, the implementation of each API entry point is its own method. This means that the entire body of call methods is devoted to resolving a request to a Ruby method. The implementations of these methods are almost identical to the implementations in the Sinatra example.
In normal circumstances, you probably wouldn’t want to write Rack code like this. The code inside the call method highlights some of the common things you want out of a service framework: mapping a request URI and HTTP method to a Ruby method (like routes in Rails) and pulling out values from the request path and query string. A more advanced service interface might route based not only on URI and HTTP method but on headers as well. For example, if you were using headers to spec- ify the API version, it would be helpful to route based on that information. The raw Rack implementation shows what common elements Sinatra and Rails provide.
Conclusion
This chapter takes a quick tour of a few of the options for implementing services in Ruby. However, it only scratches the surface. There are many frameworks out there, including Wave, Mack, and several others. However, the three options reviewed in this chapter give a good idea of different ways for structuring the code for a service.
While Rails is probably the most familiar option, it includes extra code and scaf- folding that are unnecessary. Sinatra provides a simple, clean interface for mapping requests to blocks of code for their implementation. The common element among most frameworks is Rack. However, while you can write a raw Rack-based service, its implementation is quite messy.
Ultimately, the choice of which framework to use comes down to aesthetics. Most frameworks can accomplish the job. While requests may run a little more slowly through the Rails framework, the service application layer is horizontally scalable. However, services should strive for simplicity. This means keeping the amount of code to a minimum and striving for readability and maintainability throughout.
ptg
Connecting to Services
Connecting to services is a fairly easy task, but there are some things to consider when running in a production environment. This chapter discusses blocking I/O, paral- lelism, and how to tackle these problems in Ruby 1.8, 1.9, and JRuby. It also covers how to gather performance statistics to ensure that service owners are meeting require- ments. Finally, examples show how to test and mock service calls and methods for run- ning in development mode.