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.
Our dynamically generated WSDL file
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.
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.
Comments
Leave a Reply
You must be logged in to post a comment.
