3.3. 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.
|