Home > Articles > Web Development

Flex with RESTful Services

📄 Contents

  1. Creating the Stock Portfolio Rails Application
  2. Accessing Our RESTful Application with Flex
  3. Summary
  • Print
  • + Share This
Learn how to access a RESTful Rails application and how to consume nested resources with custom verbs, which are common in RESTful applications.
This chapter is from the book

More frequently than not, Rails applications are written using a RESTful approach, which provides a coherent way of organizing your controller actions for applications that serve HTML pages. It also has the added benefit of exposing your application as a service that can be accessed via an API. This capability is important because it enables our Flex application to communicate with the Rails application by passing XML data using the Flex HTTPService components. In Chapter 2, Passing Data with XML, we looked at the underlying mechanisms of using the HTTPService component and the implications of using XML. In this chapter, we will look at the larger picture of how to access a RESTful Rails application and how to consume nested resources with custom verbs, which are common in RESTful applications. You should still be able to follow this chapter, even if you are not familiar with RESTs. This chapter will guide you through building the "Stock Portfolio" application, which will introduce REST concepts, such as CRUD verbs and nested resources, and use custom REST actions. You will see how to code this application from both a Rails and a Flex perspective.

Let's jump right into it.

Creating the Stock Portfolio Rails Application

The Stock Portfolio application is an online trading application that allows you to buy and sell stock. Of course, this sample application will walk you through what a RESTful Rails application is, even though it doesn't include many aspects that a real-world trading application needs. The data we want to manage is the following: An account holds positions in stock, for example, 50 shares of Google and 20 shares of Adobe. Each position has many movements created when the stock is bought or sold. To get started, let's create a new Rails application:

$ rails rails
$ cd rails

Now you can create the Account, Position, and Movements "resources" as follows:

$ ./script/generate scaffold Account name:string
$ ./script/generate scaffold Position account_id:integer quantity:integer ticker:string name:string
$ ./script/generate scaffold Movement price:float date:datetime quantity:integer position_id:integer operation:string

In Rails terms, a resource is data exposed by your Rails application following a convention to access and manipulate the data via HTTP requests. From a code point of view, this translates to a controller that can be invoked to create, read, update, and delete the data, and the controller will access the active record of concern to perform the requested action. To access the controller methods, define in the routes configuration file the exposed resources; this definition will dictate which URL can be used to access these resources. We will do this step by step hereafter. Again, when we mention a resource, think of it as combination of the URLs to manipulate the data, the controller that exposes the data, and the active record used to store the data.

The script/generate command is a facility to create the files we need as a starting point. We need to apply several changes to the generated code to get a fully functional application. If you look at the script/generate commands above, we specified the Account, Position, and Movement resources, their attributes, and how the resources are linked to each other. The Movement resource has a position_id column that links the movements to the positions, and the Position resource has an account_id column that links the positions to the accounts. The script/generate command does not add the code either to associate the active records or to constrain the controllers. Let's do that now. You can add it to the Account, Position, and Movement active records and add the has_many and belongs_to associations as follows:

class Account < ActiveRecord::Base
  has_many :positions, :dependent => :destroy
end

class Position < ActiveRecord::Base
  belongs_to :account
  has_many :movements, :dependent => :destroy
end

class Movement < ActiveRecord::Base
  belongs_to :position
end

This code will give you fully associated active records. Assuming you have some data in your database, you could, for example, find all the movements of the first position of the first account using the following Rails statement:

Account.first.positions.first.movements

Changing the active records was the easy part. The controllers will require more work because to respect and constrain the resource nesting, we want to ensure that the positions controller only returns positions for the specified account, and the movements controller only returns movements for the specified position. In other words, we want to have movements nested in positions and positions nested in accounts. Change the config/routes.rb file to the following:

ActionController::Routing::Routes.draw do |map|
  map.resources :accounts do |account|
    account.resources :positions do |position|
      position.resources :movements
    end
  end
end

Routes tells our application what URL to accept and how to route the incoming requests to the appropriate controller actions. By replacing three independent routes with nested routes, we indicate that, for example, the positions cannot be accessed outside the scope of an account. What URLs does the route file define now? From the command line, type the following rake command to find out:

$ rake routes | grep -v -E "(format|new|edit)"

The rake routes command gives you the list of all URLs as defined by your routes configuration file. We just pipe it into the grep command to remove from the list any extra URLs we don't want at this stage. For the account resource, we now have the URLs shown in Table 3.1.

Table 3.1. URLs for the Account Resource

HTTP Verb URL Controller
GET /accounts {:action=>"index", :controller=>"accounts"}
POST /accounts {:action=>"create", :controller=>"accounts"}
GET /accounts/:id {:action=>"show", :controller=>"accounts"}
PUT /accounts/:id {:action=>"update", :controller=>"accounts"}
DELETE /accounts/:id {:action=>"destroy", :controller=>"accounts"}

To access the positions, we need to prefix the URL with the account ID that nests the positions (see Table 3.2).

Table 3.2. Account IDs Added as Prefixes to the URLs

HTTP Verb URL Controller
GET
/accounts/:account_id/
positions
{:action=>"index",
:controller=>"positions"}
POST
/accounts/:account_id/
positions
{:action=>"create",
:controller=>"positions"}
GET
/accounts/:account_id/
positions/:id
{:action=>"show",
:controller=>"positions"}
PUT
/accounts/:account_id/
positions/:id
{:action=>"update",
:controller=>"positions"}
DELETE
/accounts/:account_id/
positions/:id
{:action=>"destroy",
:controller=>"positions"}

Finally, we need to prefix the URL with the account and position that nests the movements (see Table 3.3).

Table 3.3. URL Prefixes to Nest the Movements

HTTP Verb URL Controller
GET
/accounts/:account_id/
positions/:position_id/movements
{:action=>"index",
:controller=>"movements"}
POST
/accounts/:account_id/
positions/:position_id/movements
{:action=>"create",
:controller=>"movements"}
GET
/accounts/:account_id/
positions/:position_id/movements/:id
{:action=>"show",
:controller=>"movements"}
PUT
/accounts/:account_id/
positions/:position_id/movements/:id
{:action=>"update",
:controller=>"movements"}
DELETE
/accounts/:account_id/
positions/:position_id/movements/:id
{:action=>"destroy",
:controller=>"movements"}

List all the movements of the first position of the first account, for example, by using the following URL: http://localhost:3000/accounts/1/positions/1/movements.

Defining the routes makes sure the application supports the nested URLs. However, we now need to modify the controllers to enforce implementation of this nesting, so we'll add such constraints to all the controllers. But first, let's remove the HTML support from our controllers because, in our case, we want the Rails application to only serve XML, and we don't need to worry about supporting an HTML user interface. Let's simply remove the respond_to block from our controllers and keep the code used in the format.xml block. For example, we change the index method from the following:

class AccountsController < ApplicationController
  def index
    @accounts = Account.find(:all)

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

to the following:

class AccountsController < ApplicationController
  def index
    @accounts = Account.find(:all)
    render :xml => @accounts
  end
end

You can effectively consider the respond_to as a big switch in all your controller methods that provide support for the different types of invocations, such as rendering either HTML or XML. To constrain the positions controller, we will add before_filter, which will find the account from the request parameters and only query the positions of that account. Change the index method from the following implementation:

class PositionsController < ApplicationController
  def index
    @positions = Position.find(:all)

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

to this one:

class PositionsController < ApplicationController
  before_filter :get_account
  def index
    @positions = @account.positions.find(:all)
    render :xml => @positions.to_xml(:dasherize=>false)
  end
  protected
  def get_account
    @account = Account.find(params[:account_id])
  end
end

The Position.find(:all) was changed to @account.positions.find(:all). This change ensures that only the positions for the specific account instance are returned.

The before filter loads that account for each method. We also are modifying the format of the returned XML to use underscores instead of dashes in the XML element names to better accommodate Flex, as explained in Chapter 2. When requesting the http://localhost:3000/accounts/1/positions URL, the controller now returns an XML list of all the positions with an ID of 1 that belong to the account. Now we do the same with the movements controller and scope the movements to a specific account and position, as follows:

class MovementsController < ApplicationController
  before_filter :get_account_and_position
  def index
    @movements = @position.movements.find(:all)
    render :xml => @movements.to_xml(:dasherize => false)
  end
  protected
  def get_account_and_position
    @account = Account.find(params[:account_id])
    @position = @account.positions.find(params[:position_id])
  end
end

So when requesting the http://localhost:3000/accounts/1/positions/1/movements URL, the controller returns an XML list of all the movements of the given position from the given account. First the account is retrieved, and then the positions from that account are queried, enforcing the scope of both the account and the position. Don't directly query the positions by using Position.find(params[:position_id]) or a similar statement because the users could tamper with the URL and query the positions of a different account.

Before changing the rest of the methods, let's do some planning and see how we will use all the different controllers. Table 3.4 gives an overview of all the actions for our three controllers.

Table 3.4. Overview of Actions of the Three Controllers

Controller Method Accounts Controller Positions Controller Movements Controller
Index All accounts All positions for account All movements for position in account
Show Not used Not used Not used
New Not used Not used Not used
Edit Not used Not used Not used
Create Creates an account Buy existing stock Not used
Update Updates an account Not used Not used
Destroy Deletes the account Sell stock Not used
Customer verbs None Buy None

For our application, several nonrelevant methods don't apply when rendering XML that would apply when supporting an HTML user interface. For example, the controller doesn't need to generate an edit form because the Flex application maps the XML to a form. In the same way, we don't need the new action, which returns an empty HTML entry form. Additionally, as in our case, since the index method returns all the attributes of each node, we don't really need the show method because the client application would already have that data. We don't use the show, new, and edit methods for all three controllers, so we can delete them.

For the positions controller, we won't update a position; we will simply buy new stock and sell existing stock, meaning we are not using the update method. We also differentiate buying new stock and buying existing stock, because for existing stock, we know the ID of the position and find the object using an active record search. But, for a new stock position, we pass the stock ticker and we create the new position, which may not save and validate if the ticker is invalid. Therefore, to support these two different usage patterns, we decided to use two different actions: we use the create action for existing stock, and we add the custom buy verb to the positions controller to buy new stock.

The movements controller doesn't enable any updates since movements are generated when buying and selling positions, so only the index method is significant. Providing such a mapping table of the verbs serves as a good overview of the work you will do next. First, you can remove all unused methods. As you already implemented the index methods earlier in the chapter, we are left with seven methods, three for the accounts controller and four for the positions controller. Let's dive into it. For the accounts controller, in the create, update, and destroy methods, we simply remove the respond_to blocks and keep only the XML rendering.

class AccountsController < ApplicationController
      def create
        @account = Account.new(params[:account])
        if @account.save
          render :xml => @account, :status => :created, :location => @account
        else
          render :xml => @account.errors, :status => :unprocessable_entity
        end
      end

      def update
        @account = Account.find(params[:id])
        if @account.update_attributes(params[:account])
          head :ok
        else
          render :xml => @account.errors, :status => :unprocessable_entity
        end
      end
      def destroy
        @account = Account.find(params[:id])
        @account.destroy
        head :ok
      end
end

We saw earlier that the positions controller index method was relying on the @account variable set by the get_account before_filter to only access positions for the specified account. To enforce the scoping to a given account, the remaining methods of the positions controller will also use the @account active record to issue the find instead of directly using the Position.find method. Let's go ahead and update the create and destroy methods and add a buy method, as follows:

 class PositionsController < ApplicationController
  def create
    @position = @account.positions.find(params[:position][:id])
    if @position.buy(params[:position][:quantity].to_i)
      render :xml => @position, :status => :created,
             :location => [@account, @position]
    else
      render :xml => @position.errors, :status => :unprocessable_entity
    end
  end

  def destroy
    @position = @account.positions.find(params[:position][:id])
    if @position.sell(params[:position][:quantity].to_i)
      render :xml => @position, :status => :created,
             :location => [@account, @position]
    else
      render :xml => @position.errors, :status => :unprocessable_entity
    end
  end

   def buy
    @position = @account.buy(params[:position][:ticker],
                             params[:position][:quantity].to_i)
    if @position.errors.empty?
      head :ok
    else
      render :xml => @position.errors, :status => :unprocessable_entity
    end
end

For the buy method, we simply use the ticker and invoke the buy method from the account active record:

class Account < ActiveRecord::Base
  has_many :positions, :dependent => :destroy
  def buy(ticker, quantity)
    ticker.upcase!
    position = positions.find_or_initialize_by_ticker(ticker)
    position.buy(quantity)
    position.save
    position
  end
end

The Account#buy method in turn calls the position buy method, which in turn creates a movement for the buy operation.

 class Position < ActiveRecord::Base
  belongs_to :account
  has_many :movements, :dependent => :destroy

 def buy(quantity)
    self.quantity ||= 0
    self.quantity = self.quantity + quantity;
    movements.build(:quantity => quantity, :price => quote.lastTrade,
                    :operation => 'buy')
    save
  end
end

Now let's extend the position active record to add a validation that will be triggered when saving the position. The first validation we add is the following:

validates_uniqueness_of :ticker, :scope => :account_id

This check simply ensures that one account cannot have more than one position with the same name. We verify that the ticker really exists by using the yahoofinance gem. Install it first:

$ sudo gem install yahoofinance

To make this gem available to our application we can create the following Rails initializer under config/initializers/yahoofinance.rb that requires the gem:

require 'yahoofinance'

That's it. Now we can write a before_validation_on_create handler that will load the given stock information from Yahoo Finance, and then we add a validation for the name of the stock, which is set by the handler only if the stock exists.

class Position < ActiveRecord::Base
  validates_uniqueness_of :ticker, :scope => :account_id
  validates_presence_of :name,
                        :message => "Stock not found on Yahoo Finance."
  before_validation_on_create :update_stock_information

      protected

      def quote
        @quote ||= YahooFinance::get_standard_quotes(ticker)[ticker]
      end

      def update_stock_information
          self.name = @quote.name if quote.valid?
        end
      end
end

When referring to the quote method, the instance variable @quote is returned if it exists, or if it doesn't exist, the stock information is retrieved from Yahoo Finance using the class provided by this gem:

YahooFinance::get_standard_quotes(ticker)

The get_standard_quotes method can take one or several comma-separated stock symbols as a parameter, and it returns a hash, with the keys being the ticker and the values being a StandardQuote, a class from the YahooFinance module that contains financial information related to the ticker, such as the name, the last trading price, and so on. If the ticker doesn't exist, then the name of the stock is not set and the save of the position doesn't validate.

The sell method of the positions controller is similar to the buy method, but less complex. Let's take a look:

 class Position < ActiveRecord::Base
  def sell(quantity)
    self.quantity = self.quantity - quantity
    movements.build(:quantity => quantity, :price => quote.lastTrade,
                    :operation => 'sell')
    save
  end
end

Similar to the buy method, the sell method updates the quantity and creates a sell movement, recording the price of the stock when the operation occurs. There is one last thing: we need to add the custom buy verb to our routes. Do this by adding the :collection parameter to the positions resource.

ActionController::Routing::Routes.draw do |map|

map.resources :accounts do |account|
  account.resources :positions, :collection => {:buy => :post} do |position|
    position.resources :movements
  end
end

This indicates that no ID for the position is specified when creating the URL, thus invoking the buy verb on the positions collection. The URL would look something like this:

/accounts/1/positions/buy

If you wanted to add a custom verb that applies not only to the collection of the positions, but also to a specific position, thus requiring the position ID in the URL, you could have used the :member parameter to the positions resource.

Our application starts to be functional. By now, you certainly did a migration and started playing with your active records from the console. If not, play around a little, then keep reading because we are about to start the Flex part of our application.

  • + Share This
  • 🔖 Save To Your Account