Putting It All Together: an Expanded Server Example
Now that we’ve covered the various dispatching modes and discussed how to define a web service API with appropriate signatures, we’re ready to dive into a more realistic example. We’ll develop the basics of a football statistics web service, starting with two methods: Listgames and Getgamestats. The first method requires an integer parameter that represents a year; it returns a list of games in that year for which statistics are available. The second method requires two parameters, both strings: a username and gamename. If the username is valid, this method returns the actual statistics for a specific game.
We’ll start by defining the web service API in app/apis/stats_api.rb:
class StatsApi < ActionWebService::API::Base
api_method :listgames,
:expects => [{:year => :int}],
:returns => [[:string]]
api_method :getgamestats,
:expects => [{:username => :string}, {:gamename => :string}],
:returns => [[Footballstats]]
end
The StatsApi class reflects the service’s design requirements. It defines two methods. The first, listgames, requires an integer argument; it returns an array of strings to our clients. The second requires two string arguments and returns an array of Footballstats, which we will define as an ActionWebService::Struct type.
The next step is to set up a controller in the file app/controllers/stats_controller.rb:
class StatsController < ApplicationController
wsdl_service_name 'Stats'
wsdl_namespace 'urn:sportsxml'
web_service_dispatching_mode :delegated
web_service :footballstats, Getstats.new
web_service_scaffold :test
def about
# information about our service
# accessed through reg. web browser
# http://localhost:3000/stats/about
end
end
The controller sets up a name for the web service by calling wsdl_service_name ‘Stats’, and defines a namespace by calling wsdl_namespace ‘urn:sportsxml’, so that the automatically generated WSDL file has useful and unique values that our clients can quickly understand. We also set the dispatching mode to :delegated so we can use distinct endpoint URLs for each method and can have the flexibility to mix in other web services or APIs in the future.
Because we are not using the :direct dispatching mode, we are required to use the web_service method to specify the web service models that this controller exposes to the clients. The first parameter of the web_service method, :footballstats, is a symbol used to reference the web service. This symbol is used as part of the endpoint URI for XML-RPC and some SOAP calls. The second parameter is an instance of the model itself: Getstats.new, in this case. (We’ll define the Getstats class shortly). Rather than providing an instance of the model, you can reference the model in a block and pass that block to the web_service method: web_service :footballstats {Getstats.new }. If you use the block form, the Model is instantiated at runtime and has access to the instance variables and methodsincluding helper methodsof the controller.
There are two additional things we do in our controller. First, we define a basic method about that we intend to use to provide documentation and other information to users via our web site. Second, we include a call to web_service_scaffold:test, which allows us to do some quick additional testing of our service via a web browser at the address http://localhost:3000/stats/test. Using the built-in scaffold feature gives us some quick feedback about our services, but it should not be considered a complete test. It’s still highly recommended that you take advantage of the functional tests Rails supports, as well as the additional client code tests mentioned at the end of this section.
Next, we define the Getstats class, saved as app/models/getstats.rb:
class Getstats < ActionWebService::Base
web_service_api StatsApi
before_invocation :checkusername, :except => [:listgames]
def getgamestats(username, gamename)
# get the stats for a specific game
stats = []
statdetails = Footballgames.find_by_sql(
["select statfor, as stattype, statvalue, statlogged from gametimestats
where gamename = ?",gamename])
statdetails.each do |rec|
stats << Footballstats.new(:statfor => rec.statfor, :stattype => rec.stattype,
:statvalue => rec.statvalue, :statlogged => rec.statlogged)
end
return stats
end
def listgames(year)
# get a list of football games stats are available for
Footballgames.find_by_sql(["select distinct gamename from gametimestats
where gametimestats_year = ?", year]).map {|rec| rec.gamename}
end
protected
def checkusername(name, args)
if (args[0] != “kevin”)
raise “Access denied!”
end
end
end
The model implements the web service’s business logic. It’s where you’ll spend most of your development effort, so let’s take a little extra time to explain everything that’s going on here.
The first line, web_service_api StatsApi, starts by associating the model with the StatsApi. Remember that only one API can be associated with a class, so every method in the model that we plan to expose through the web service must have a matching api_method definition in the StatsApi class.
Next, apply some access control by calling before_invocation:checkusername, :except => [:listgames. This method arranges for the checkusername method to be called before any other method in this class is called (except for the listgames method). If the call to checkusername returns anything other than the value "true" (which is the default return value for any Ruby method that has no return value), the web service is not invoked, and an error message is returned to the client. Thus, we're using before_invocation to restrict access to all our methods except listgames.
Like the Rails ActionPack, AWS supports both a before_invocation and after_invocation method. Each of these methods can call a symbol referring to a method in the controller (before_invocation :checkusername), a block of code (before_invocation {|obj, meth, args| false}), or an object referring to a model (before_invocation Checkdata). If you pass an object as the parameter, the object is expected to have an intercept() method that is automatically called upon instantiation. before_invocation provides you with the name of the method that was called, as well as the parameters that were passed. Instance methods called via before_invocation should expect two parameters: the name of the method that's being called and an array consisting of the method call's parameters. Blocks and objects are passed three parameters: the object containing the web service method, the method name, and an array of parameters.
Similarly, after_invocation provides you with the method that was called, the parameters sent in, and the results from the web service call. Instance methods being called via the after_invocation method should expect three parameters: the method name, an array of the parameters sent with the method request, and the method return value. Blocks and objects are passed four parameters: the object containing the web service method, the method name, an array of parameters sent with the method request, and the method's return value.
Our model uses before_invocation to insert a call to checkusername before method invocations. Our implementation of checkusername is extremely simple: it tests against a hardcoded value. If your name is "kevin", it lets you in. In a real implementation, you would check against a database and possibly use some encryption procedures. Remember that before_invocation calls are passed the method name and an array consisting of the parameters that were sent with the method call. Since we know the first parameter of getgamestats method is the username, we check against the value of args[0]. There’s a consequence to this design. In the future, we may add methods to the web service, and those methods may require validation by checkusername. We’ll therefore have to make sure that any future method that needs validation has a username as its first parameter.
Finally, we’re ready to discuss the actual web service methods. Our API specified two methods, getgamestates and listgames. getgamestats is supposed to return an array of ActionWebService::Struct types, with data from an actual game. So it performs a simple database query and uses the results to build an array of Footballstats, which we will define as an ActionWebService::Struct subclass. (We’ll see that definition shortly.) Our second method, listgames, searches for a list of game names that were played in a given year. We use the find_by_sql method of ActiveRecord to query the database and then collect the results of that query into an array of strings, which is what our API expects listgames to return.
Note: In a real web service, it’s more likely that you would implement getgamestats using the ActiveRecord model with the gamename parameter to get the statistics from a given game. To show ActionWebService::Struct in action, I chose to find_by_sql to query the database, and then store the results into an array of Struct values.
We’re almost done building our second web service. But before we’re done, we have to define a couple of data models. The Footballgames class is an ActiveRecord model we’ll use to query the database for the games for which we have statistics available. In our examples, we used the find_by_sql method to query the database, so we are really just treating Footballgames as a generic ActiveRecord class to establish a connection to our database. Footballstats.rb is an ActionWebService::Struct model that we’ll use to hold the actual statistics for a game. Remember that ActionWebService::Struct is really just another way of defining a Hash that we intend to use a lot; since we could potentially have hundreds or even thousands of stats per game, using a Struct saves us a lot of typing and hopefully makes our code easier to follow. We save both of these files in the models directory:
class Footballgames < ActiveRecord::Base end class Footballstats < ActionWebService::Struct # our football stats struct model member :statfor, :string member :stattype, :string member :statvalue, :string member :statlogged, :datetime member :statnote, :string end
Finally, our web service is complete and ready for clients. SOAP clients can use the automatically generated WSDL at http://localhost:3000/stats/wsdl, or the URI of http://localhost:3000/stats/footballstats and the namespace of urn:sportsxml. XML-RPC clients can use the URI http://localhost:3000/stats/footballstats.
As with any code you write, it’s important to test before you release. If you’ve used the Rails generator scripts to create the files above, you already have the outline of some functional tests available. If not, you can manually create the file using our previous functional test example as an outline. Keep in mind that you use a different invoke command based on your dispatching mode: invoke for :direct, invoke_delegated for :delegated, and invoke_layered for :layered. Additionally, the following code snippets can be used to test your service as real clients. Simply save each as a local Ruby program and run them at the command line:
# XML-RPC client
require 'xmlrpc/client'
server = XMLRPC::Client.new2("http://localhost:3000/stats/footballstats")
result = server.call("Listgames", 2004)
puts result
# SOAP client using WSDL
require 'soap/wsdlDriver'
driver =
SOAP::WSDLDriverFactory.new("http://localhost:3000/stats/wsdl").create_rpc_driver
results = driver.getgamestats("kevin","GB@DET")
results.each do |rec|
puts rec["statfor"]
puts rec["stattype"]
puts rec["statvalue"]
puts rec["statlogged"]
puts rec["statnote"]
end
This is a good point to look at the difference between :delegated and :layered dispatching. As I said earlier, there’s no difference in the code, aside from the symbol passed to the web_service_dispatching_mode method (and the invoke command you’ll use in your functional tests). Had we chosen :layered dispatching, XML-RPC clients would use the URI http://localhost:3000/stats/api, and would reference our methods as footballstats.Listyears and footballstats.Getgamestats.
SOAP clients not wishing to use WSDL would also use http://localhost:3000/stats/api as the endpoint URI.
Comments
Leave a Reply
You must be logged in to post a comment.
