3.2. SOAP and
XML-RPC Web Service Servers
Rails comes with a component called
ActionWebService (often referred to as AWS) that makes hosting SOAP
and XML-RPC web services simple and efficient. ActionWebService
allows you to make SOAP and XML-RPC methods available by binding
your web service to controllers in your Rails application. It takes
care of almost all the technical details for you: parsing the XML
request, creating the XML response, and even creating the
appropriate WSDL file for your SOAP service. This all means you're
free to focus on your business logic, without having to worry about
all the protocol specifics.
To update your version of ActionWebService, or
to install it independently of Rails, use the gem
command:
gem install actionwebservice
AWS supports many of the common Rails tools to
speed up the development cycle. It includes a
web_service_scaffold method and a script to generate the
base files, code, and some functional tests:
script/generate web_service YOURSERVICE YOURMETHOD1 YOURMETHOD2
All of this makes building web services with AWS
very easythough we will skip the use of these convenience tools in
order to give a more detailed explanation of
the steps involved in creating your servers.
There are only three simple steps you need to follow:
-
Determine which dispatching mode fits your needs
(Direct, Delegated, or Layered).
-
Create your Application Programming Interface
(API), providing the details about the methods you'll make
available.
-
Create your methods (either in controllers for
direct dispatching or in models for layered or delegated
modes).
Before we talk too much about the details and
options of each step, let's build a very basic service so we have a
point of reference. We'll create a web service with a single method
called dogreeting. This method expects a username of type
string as a parameter and returns a greeting in the form
of a string. We start by defining our API in the file app/apis/greeting_api.rb. This file lists the
methods our services makes available:
class GreetingApi < ActionWebService::API::Base
api_method :dogreeting,
:expects => [{:username => :string}],
:returns => [{:greeting => :string}]
end
Note: You could also use the command
line (ruby script/generate webservice greeting dogreeting)
to generate the base files and then edit them to match our
example.
Next we write a controller that implements the
methods defined in the API. Rails automatically looks for a
GreetingApi implemented by the GreetingController. So
we'll save the following file as app/controllers/greeting_controller.rb:
class GreetingController < ApplicationController
def dogreeting(username)
"Hello #{username}"
end
end
That's all there is to it! We've now built a
very simple, direct-dispatching web service with Rails!
ActionWebService generates a WSDL file for the service
automatically; SOAP clients can download this WSDL file from
http://localhost:3000/greeting/wsdl.
SOAP clients can call methods using http://localhost:3000/greeting/api as the
endpoint URI and urn:ActionWebService as the namespace. XML-RPC
clients can access the service with the URI http://localhost:3000/greeting/api.
Now it's time to test our service to make sure
it's really working as expected. Save the following code as
test/functional/greeting_api_test.rb:
require File.dirname(__FILE__) + '/../test_helper'
require 'greeting_controller'
class GreetingController; def rescue_action(e) raise e end; end
class GreetingControllerApiTest < Test::Unit::TestCase
def setup
@controller = GreetingController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_dogreeting
result = invoke :dogreeting, "Kevin" # this is the real test call
assert_equal "Hello Kevin", result
end
end
Make sure that you have Webrick running in one
command window, and then execute the above test in a separate
command window with the command ruby
test\functional\gretting_api_test.rb. If everything goes as
planned, you should get results similar to:
Loaded suite greeting_api_test
Started
.
Finished in 0.75 seconds.
1 tests, 1 assertions, 0 failures, 0 errors.
Note: The following code can also be
used to test your service. Save the XML-RPC and SOAP clients as
local Ruby programs and run them from the command line:
This simple example takes advantage of a number
of "magical" things Rails does, such as setting the dispatching
mode and automatically associating the API to the controller. So
before starting another example, let's talk about the some of these
magical things, and what our other options for them are.
The first step in building a web service with
Railsafter going through the design phase, of courseis to determine
your dispatching mode. AWS offers three dispatching options,
:direct, :delegated, or :layered. The
dispatching mode controls the routing of your web service method
invocations (from clients) to your Ruby methods. The options differ
in where you implement your methods, and in the address your
clients use to access your web service. To specify the dispatching
mode, the controller for your service calls the
web_service_dispatching_mode method with the argument
:direct, :delegated, or :layered.
The default mode is :direct
(web_service_dispatching_mode:direct). With direct
dispatching, you just have one controller and one API. You code
everything like any other Rails application, except that your web
service methods won't have RHTML views. Direct mode's disadvantage
is that you can associate only one API file with a controller. This
limitation becomes a problem if you want multiple points of entry
to your web service or if you want to combine existing services
into one larger web service.
For example, let's say you develop a
:direct web service that lets users search the data at
yourdomain.com/data/getuserdata
and yourdomain.com/data/getproductdata. Now let's
say that you decide to break these out into two additional access
points: yourdomain.com/user/getdata and yourdomain.com/product/getdata. Direct
dispatching would require two controllers (one for user and one for product). These two controllers would have
essentially the same code. While this might be acceptable in
certain situations, it's definitely not the Rails way and goes
against the "don't repeat yourself" (DRY) philosophy.
The other two dispatching options are
:delegated and :layered. The code for these two
modes is identical. You define all your web service methods in a
model, associate the model with your API, and then reference the
models from any controllers you like. To solve the user and product problem, you would create separate
product and user models. Each model would then contain its own
getdata method and would be associated with an API file
defining its related geTData web service input and output
parameters. Finally, each controller would use the
web_service method to declare which methods are available
to the outside worldfor example web_service :getdata,
Product.new.
When you use the :delegated or
:layered dispatching modes, your code is slightly harder
to follow: you define the methods for each API in a separate model,
rather than directly in the controller. However,
:delegated or :layered is the best way to go for
any large or complex web service; they are more flexible in the
long run. :direct looks good as long as the service
remains simple and has relatively few features, but when have
real-world applications stayed simple or gone according to
design?
Since our Rails code is the same for both
:layered and :delegated dispatching, you are
probably wondering why there are two options at all. The answer is
in the endpoint URL that clients use to access the web service.
With :delegated, clients use a distinct URL for each
method in the APIfor example, yourdomain.com/product/getdata and
yourdomain.com/product/getprice.
With :layered, clients use the same URL for all attached
methods and rely on AWS to route the request based on information
passed in its headerfor example, yourdomain.com/product/api. If a SOAP client
uses a WSDL file to define their method calls, there really is no
difference between the :delegated and :layered
dispatching modes.
Regardless of the dispatching mode, you always
develop Rails web services by defining an API that lists the
methods and data types the web service will provide. AWS uses this
API to route requests and generate errors if parameters are missing
or a connection problem arises. The api_method method
defines the methods that the services makes available. It can have
the optional :expects and :returns parameters to
define web service signatures. If you omit :expects, the
web service will generate errors for any clients that attempt to
pass parameters during the method call. If you omit
:returns, any calls to the method will not receive a
result.
Being that Ruby is a loosely typed language,
:expects and :returns may feel very strange, but
they're necessary because web services are strongly typed
creatures. Specifying data types for parameters and return values
makes sense when you consider that your web service will
communicate with clients written in many languages, including
statically typed languages, such as Java and C#. Without defining
types, you could potentially send these clients an object they
weren't expecting or couldn't handle, causing the client to fail or
deliver inconsistent and unreliable results.
In the call to api_method, the
:expects and :returns symbols are used as Hash
keys that point to an array of parameter types or an array of
return-value types. (Like Ruby methods, web service methods support
multiple return values.) The elements of these arrays are symbols
representing standard Ruby types (:string, :int,
:bool, :float, :time,
:datetime, :date, and :base64), or the
names of ActiveRecord::Base or
ActionWebService::Struct classes (for example,
Greeting or Account), or a single element array
to represent arrays of objects (for example, [:string] to
represent an array of Strings or [Account] to
represent an array of ActionWebService::Struct Account
objects). Here's how to specify the signature of a method named
dogreeting that expects a string as a parameter and
returns a complex data structure:
api_method :dogreeting, :expects => [:string], :returns => [Greeting]
If you want to provide more documentation, you
can provide a one-item Hash with the name of the parameter as the
key and one of the previously mentioned types for the valuefor
example, :expects => [{:username => :string}].
Providing a parameter name doesn't change the implementation of
your web services, but it makes it possible for ActionWebService to
generate a more descriptive WSDL file. And a more descriptive WSDL
file may make it easier for other developers (or you) to write
clients for the web service.
Although api_method can describe some
very complex signatures, the underlying idea isn't that difficult.
However, the ActiveRecord::Base and
ActionWebService::Struct types may throw you for a little
bit of a loop, so I'll expand a little bit on those.
If you use the ActiveRecord::Base type,
AWS returns a record in the table as a complex web service type.
Ruby's implementation of this type is a Hash in which the column
names are the keys. So, if you have a table where the columns are
first_name, last_name, age, and
birth_date, ActiveRecord::Base would treat rows
in this table as if you had listed them explicitly. For
example:
[{:first_name => :string},
{:last_name => :string},
{:age => :int},
{:birth_date => :date}].
You can use the
ActionWebService::Struct type in much the same way; it's
also an array of one-item Hash data types in which the member names
are the keys. You would use it in situations in which you don't
have a database in back of the web service but want the service to
return an arbitrary object from your application much like a
database would. The member method defines members of the
Struct:
# in our API, we can now say :expects => [Greeting]
class Greeting < ActionWebService::Struct
member :username, :string
member :password, :string
member :logtime, :datetime
end
The first parameter of the member
method serves two purposes: it defines a read/write attribute of
the Greeting class, which you use to store or retrieve
data from a Greeting object (just like any other
attribute), and it is used to build a more descriptive WSDL file
for clients of your web service. The second argument is the Web
Services data type used to transfer the attribute from client to
server, or vice versa. Again, the Struct type is useful
when you have a large or complex set of data, much like you would
find in a database table but without the database.
ActiveRecord::Base and
ActionWebService::Struct are simplified ways to define
complex data types for your web service (there's no reason you
can't do everything yourself, by populating an array of Hash types
with your data). They save you from having to deal with the details
of defining the Hash and then mapping your data into it. In the
next example, we'll use an ActionWebService::Struct type
to show how easy they really are to use in code.
|