- Chapter 3: Flex with RESTful Services
- Creating the Stock Portfolio Rails Application
- Accessing our RESTful Application with Flex
- Summary
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 =>''bu'') 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''yahoofinanc''
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 =>''sel'') 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.
© Copyright Pearson Education. All rights reserved.