- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
DISPATCHING MODES 589
Using these lets external developers work with the native structured types for their platform when accessing our web services.
So what gets put into the structured type seen by remote callers? For ActionWebService::Struct, all the members defined with member do.
class Person < ActionWebService::Struct member :id, :int
member :name, :string end
An ActiveRecord::Base derivative exposes the columns defined in its corresponding database table.
25.3Dispatching Modes
Remote callers send their invocation requests to endpoint URLs. (Section 25.6,
External Client Applications (XML-RPC), on page 596, has the formats of endpoint URLs.) Dispatching is the process by which AWS maps these incoming requests to methods in objects that implement the services.
The default dispatching mode is direct dispatching and requires no additional configuration to set up. This is the mode we used for the example on page 584.
Direct Dispatching
With direct dispatching, the API definition is attached directly to the controller, and the API method implementations are placed in the controller as public instance methods.
The advantage of this approach is its simplicity. The drawback is that only one API definition can be attached to the controller; therefore, we can have only one API implementation for a unique endpoint URL. It also blurs the separation of model and controller code. It is shown in Figure 25.2.
NewPost(post)
Remote Caller
Controller
new_post(post)
Figure 25.2: Overview of Direct Dispatching
Report erratum
USING ALTERNATE DISPATCHING 590
Layered Dispatching
Layered dispatching allows us to implement multiple APIs with one controller, with one unique endpoint URL for all the APIs. This works well for overlapping XML-RPC–based APIs (such as the various blogging APIs), which have desktop client applications supporting only one endpoint URL. This is shown in Figure 25.3.
Delegated Dispatching
Delegated dispatching is identical to layered dispatching except that it uses a unique endpoint URL per contained API. Instead of embedding API identifiers in the method invocation messages, remote callers send the messages for a specific API to its associated endpoint URI.
We use the web_service_dispatching_mode method in a controller to select that controller’s dispatching mode.
Download e1/ws/dispatching_mode.rb
class RpcController < ActionController::Base web_service_dispatching_mode :layered
end
The valid modes are :direct, :layered, and :delegated.
25.4Using Alternate Dispatching
Because we’ve already used direct dispatching in our first example web service, let’s implement the same web service in one of the other modes.
Remote Caller
FindProductById(id)
ProductService
find_product_by_id(id)
Controller
OrderService
is_order_shipped(id)
IsOrderShipped(id)
Remote Caller
Figure 25.3: Overview of Layered Dispatching
Report erratum
USING ALTERNATE DISPATCHING 591
Layered Dispatching from a Remote Caller’s Perspective
Method invocation requests from remote callers differentiate between the APIs by sending an identifier indicating which to API the method call should go.
In the case of XML-RPC, remote callers use the standard XML-RPC serviceName.methodName convention, with serviceName being the identifier. For example, an XML-RPC method with a name in the XML-RPC message of blogger.newPost would be sent to a newPost method in whichever object is declared to implement the blogger service.
In the case of SOAP, this information is encoded in the SOAPAction HTTP header as declared by the generated WSDL. This has the implication that remote callers behind a proxy stripping off this HTTP header will not be able to call web services that use layered dispatching.
Layered Dispatching Mode
Since layered dispatching implements multiple APIs with one controller, it needs to create mappings for incoming method calls to the objects implementing them. We do this mapping using the web_service declaration in the controller.
Download e1/depot_ws/app/controllers/layered_backend_controller.rb
class LayeredBackendController < ApplicationController web_service_dispatching_mode :layered web_service_scaffold :invoke
web_service :product, ProductService.new web_service(:order) { OrderService.new }
end
You’ll notice that we no longer attach the API definition to the controller, because it no longer contains the API methods. Also notice the two different ways we called web_service.
The first call to web_service passed it a ProductService instance directly. This is sufficient if our web service doesn’t need to have anything to do with the controller. Because the instance is created at class definition time, though, it has no access to the instance variables of the controller, so it effectively operates in isolation from it.
The second call to web_service passes a block parameter. This has the effect of deferring OrderService instantiation to request time. The block we give it will be evaluated in controller instance context, so it will have access to all the instance variables and methods of the controller. This can be useful if we need to use helper methods such as url_for in our web service methods.
Report erratum
METHOD INVOCATION INTERCEPTION 592
Here’s the rest of our code. First, here’s the implementation of our productsearching service.
Download e1/depot_ws/app/apis/product_service.rb
class ProductService < ActionWebService::Base web_service_api ProductApi
def find_all_products
Product.find(:all).map{ |product| product.id } end
def find_product_by_id(id) Product.find(id)
end end
And here’s the implementation of the API to determine whether a product has been shipped.
Download e1/depot_ws/app/apis/order_service.rb
class OrderApi < ActionWebService::API::Base api_method :is_order_shipped,
:expects => [{:orderid => :int}], :returns => [:bool]
end
class OrderService < ActionWebService::Base web_service_api OrderApi
def is_order_shipped(orderid)
raise "No such order" unless order = Order.find_by_id(orderid) !order.shipped_at.nil?
end end
Implementing Delegated Dispatching
The implementation for delegated dispatching is identical to layered dispatching, except that we pass :delegated to web_service_dispatching_mode rather than :layered.
25.5Method Invocation Interception
To avoid duplicating the same code in multiple methods, AWS allows us to perform invocation interception, allowing us to register callbacks that will be invoked before and/or after the web service request.
AWS interception works similarly to Action Pack filters but includes additional information about the web service request that is not available through Action Pack filters, such as the method name and its decoded parameters.
Report erratum
METHOD INVOCATION INTERCEPTION 593
For example, if we wanted to allow only remote callers with an acceptable API key to access our product searching web service, we could add an extra parameter to each method call.
Download e1/depot_ws/app/apis/product_auth_api.rb
class ProductAuthApi < ActionWebService::API::Base
api_method :find_all_products,
:expects => [{:key=>:string}], :returns => [[:int]]
api_method :find_product_by_id,
:expects => [{:key=>:string}, {:id=>:int}], :returns => [Product]
end
And then create an invocation interceptor that validates this parameter without putting the code in every method.
Download e1/depot_ws/app/controllers/backend_auth_controller.rb
class BackendAuthController < ApplicationController wsdl_service_name 'Backend'
web_service_api ProductAuthApi web_service_scaffold :invoke
before_invocation :authenticate
def find_all_products(key) Product.find(:all).map{ |product| product.id }
end
def find_product_by_id(key, id) Product.find(id)
end
protected
def authenticate(name, args)
raise "Not authenticated" unless args[0] == 'secret' end
end
Like with Action Pack, if a before interceptor returns false, the method is never invoked, and an appropriate error message is sent back to the caller as an exception. If a before interceptor raises an exception, invocation of the web service method will also be aborted.
AWS interceptors are defined using before_invocation and after_invocation.
before_invocation(interceptor, options={}) after_invocation(interceptor, options={})
Report erratum