A plea for XML-RPC Introspection

The pursuit of a self documenting API....

For those with ADD: Python script here.  Usage inside.

Yesterday, I wrote a bit of a rant venting some of my frustration in working with XML-RPC.  I have worked around or through most of the protocol level frustration over the last year-- not trying to communicate with Radio UserLandvia the Blogger API using AppleScript Studio due to character escaping issues, for example.

Recently, CodeFab has been adding full XML-RPC support to our product Intent.   The API works very well and XML-RPC makes it simple to build clients.

However, we really needed to document the API before the pending release.   Given that the XML-RPC community appears to have thought this problem through and came up with the methods listMethods, methodHelp and methodSignature.   These methods weredesigned to be interoperable.  That is, an element from the list of methods returned by listMethods should be able to be passed to either of the other two methods to return the specifics for that method.

Unfortunately, as noted in my rantand by others, the community hasn't really seemed to agree upon exactly how these methods are too be used.

I feel strongly that any rant than can be backed by code should be backed by code.   Since we needed to be able to test the implementation of the introspection/reflection methods on our own code, I implemented a simple python script that will query an XML-RPC server's implement of listMethods, methodSignature, and methodHelp.   It is designed to be fault tolerant -- if it doesn't find one of the methods, it will print an appropriate warning message while also continuing to display as much information as possible.

Documentation of one particular implementation of the three methods can be foudn within the standard Python distribution.  Reading this documentation indicates that my original rant -- and the meerkat article some of it was based on -- was incorrect in the description of methodSignature'sreturn values.

You can download the python script via friday.com. It requires Python 2.2 and should run on any platform (tested on Mac OS X and Windows something-or-other).

I encourage everyone to download and try this script against your server!   If it does not spew forth a description of the API of your server in some easily human parseable fashion and you expected it to, please let me know!  For those who feel their server should not spew forth useful information in response to these three methods, please let me know why not!

I can't think of a single situation where it does not benefit the quality of the API to implement these methods.  Even if the implement returns next to nothing for one or all three of the methods for security reasons, an implementation provides remote clients with a clue that you have a clue and have a reason for designing your API as you have.   For any other RPC, implementing these methods affords you the unique opportunity of shipping a self documenting API.

Very cool!   You ship an app with an XML-RPC API that implements these methods with the knowledge and confidence that the very same version of the API the developer is using is the version of the API contained within the documentation the developer [might have] has in hand.

Using the Script

The implementation of the script is painfully simple.   It takes one required argument and one optional argument.  The required argument is the URL to the RPC server you wish to query.

The option argument is a namespace prefix that will be tacked onto the front of the three methods.  For example, the standard PHP XML-RPC implementation sticks the three RPC API query methods into their own namespace;  the system namespace.  That is, to query for all methods that a PHP XML-RPC server implements, you invoke thesystem.listMethods method.   The optional argument does not have to have a '.' postfix -- it will be added as needed.

So, to query the Radio UserLand desktop (this link only valid for Radio UserLand users who have not changed the port of their desktop web server and the app is running) web server for a list of methods it implements, you could try:

bbum% ./dump-methods.py http://127.0.0.1:5335/RPC2 
Checking server capabilities via method 'listMethods' on server http://127.0.0.1:5335/RPC2....
... ugly error message ...
bbum% ./dump-methods.py http://127.0.0.1:5335/RPC2 system
Checking server capabilities via method 'system.listMethods' on server http://127.0.0.1:5335/RPC2....
... ugly error message ...

Radio UserLand's desktop webserver does not implement the listMethods method.  As well, it returns something in its XML-RPC fault response that causes the Python xml parser to barf -- hence the "ugly error message".   Not sure whose bug that is, but the dump-methods.py script generally deals with XML-RPC faults much more gracefully.

In any case, this demonstrates the Radio UserLand does not implement the methods necessary to introspect the XML-RPC api.

An example against a server that implements part of the API (which, in this case, is quite useful).  Note that this server implements the listMethods within the server. "namespace" (prefix).
bbum% ./dump-methods.py http://diveintomark.org/cgi-bin/webservices.cgi system
Checking server capabilities via method 'system.listMethods' on server http://diveintomark.org/cgi-bin/webservices.cgi....
WARNING: It appears that the server does not implement methodSignature().
It is also possible that listMethods() did not return an array of just the method names as it should have.

WARNING: It appears that the server does not implement methodHelp().
It is also possible that listMethods() did not return an array of just the method names as it should have.

It appears that the server does not implement either methodHelp() or methodSignature().
It is also possible that listMethods() did not return an array of just the method names as it should have.
In any case, the script is punting on trying to retrieve method descriptions in the recommended fashion and
will simply dump whatever was returned by listMethods() in its raw form.

Dumping raw for 12 methods.
{'examples.getStateName(stateIndex)': 'None',
 'siteStatistics.getAllReferers()': 'get list of (count, domain, url) for each referer',
 'siteStatistics.getAllSearches()': 'get (count, url, searchstring) for all Google searches',
 'siteStatistics.getAllVisitorCountByBrowser()': 'get dictionary of {browser : visitor count}',
 'siteStatistics.getAllVisitorCountByOS()': 'get dictionary of {operating system : visitor count}',
'siteStatistics.getHitCount()': 'get total number of page hits',
 'siteStatistics.getRefererCount()': 'get total number of unique referers (based on domain name)',
 'siteStatistics.getReferersByDomain(domainname)': 'get (count, url) for a single referer',
 'siteStatistics.getVisitorCount()': 'get total number of unique visitors (based on IP address)',
 'siteStatistics.getVisitorCountByBrowser(browsername)': 'get number of visitors using a specific browser',
 'siteStatistics.getVisitorCountByOS(osname)': 'get number of visitors using a specific operating system',
 'system.listMethods()': 'None'}
And, finally, an example from a server that implements all three methods "correctly" (actually, this appears to be the default behavior of PHP's support for XML-RPC).

bbum% ./dump-methods.py http://www.oreillynet.com:80/meerkat/xml-rpc/server.php system
Checking server capabilities via method 'system.listMethods' on server http://www.oreillynet.com:80/meerkat/xml-rpc/server.php....
Dumping information for 9 methods.
---------------------
Method: meerkat.getChannels
Signature:
        array meerkat.getChannels()

Help:

Returns an array of structs of available RSS channels each with its associated channel Id.
---------------------
Method: meerkat.getCategories
Signature:
        array meerkat.getCategories()
Help:
Returns an array of structs of available Meerkat categories each with its associated category Id.
---------------------
Method: meerkat.getCategoriesBySubstring
Signature:
        array meerkat.getCategoriesBySubstring(string)

Help:
Returns an array of structs of available Meerkat categories each with its associated category Id given a substring to match (case-insensitively).
---------------------
Method: meerkat.getChannelsByCategory
Signature:
        array meerkat.getChannelsByCategory(int)

Help:

Returns an array of structs of RSS channels in a particular category (specified by integer category id) each with its associated channel Id.
---------------------
Method: meerkat.getChannelsBySubstring
Signature:
        array meerkat.getChannelsBySubstring(string)

Help:

Returns an array of structs of RSS channels in a particular category each with its associated channel Id givena substring to match (case-insensitively).
If any of the methods on the server side had multiple signatures, it would be listed.

Conclusion

If everyone implements these three methods in their XML-RPC servers, it makes implementation of the client side both easier and much more interesting.  Client side implementation is easier in that the implementor can always go to the server to retrieve the [hopefully and more likely than an external document] latest documentation regarding the server.

Given this style of implementation, it makes building developer tools and learning clients much easier.  This, in turns, makes using XML-RPC significantly more introducing.   For example, given the standard form of data returned by the three methods, it will be trivial to build a Cocoa based GUI app that dynamically reconfigures itself based on the information returned by the API description methods.

Furthermore, python, Microsoft's .NET, and PHP all have out-of-the-box support for XML-RPC and some form of introspection along the lines described above.  Java, Perl, and Ruby all have implementations available via third parties (and likely built in by now, in some cases).

In other words, no matter what language your server is implemented in, there is very likely already built in support for these methods or third party implementations that are freely available.  There is no excuse for not providing an implementation of these methods and your developer community will greatly appreciate it!