xml-rpc

  1. xml-rpc
    1. Description
    2. Author
    3. Requirements
    4. Documentation
      1. Client
      2. Server
      3. Low-level
        1. Scheme to XML-RPC
        2. XML-RPC to Scheme
    5. Examples
    6. Changelog
    7. License

Description

A library for XML-RPC client/servers.

Author

Peter Bex (inspired by and using code from an earlier egg by Felix Winkelmann)

Requirements

Documentation

This implementation of XML-RPC is extended to allow returning multiple values. Errors during the execution of a server-method are propagated to the client as "fault" responses.

Client

Usage:

 (use xml-rpc-client)
[procedure] (xml-rpc-server uri)

Returns a procedure that, when called with the name of a remote XML-RPC method, will return a procedure that passes its arguments to the XML-RPC server which is given in uri (which can be an URI object or a string representing an URI).

To determine how XML-RPC types are mapped to Scheme types and vice-versa, see below. It's important that you read this, because there is some ambiguity in how lists are mapped (either to arrays or to structs).

Here is an example that fetches the current time of day from xml-rpc.org:

(require-extension xml-rpc-client)

(define time-server
  (xml-rpc-server "http://xml-rpc.org/RPC2") )
  
(define get-current-time
  (time-server "currentTime.getCurrentTime") )
  
(print (time->string (get-current-time)))

For lower-level access to the client (implementing custom handlers, for example), you can use the following procedures:

[procedure] (xml-rpc-methodcall method-name args)

Constructs an SXML representation of a method call to the procedure method-name with an arguments list of args.

(use xml-rpc-client)

(xml-rpc-methodcall 'scheme.makeList '(1 2 3 "testing"))
 =>
(methodCall
  (methodName "scheme.makeList")
  (params
    (param (value (i4 "1")))
    (param (value (i4 "2")))
    (param (value (i4 "3")))
    (param (value (string "testing")))))
[procedure] (xml-rpc-response->values response-sxml)

This procedure accepts as response-sxml the SXML representation of a server's response, and either returns the values returned by the procedure call encoded in the response, or throws an exception of type exn xml-rpc in case the response contains invalid data.

Server

Usage:

 (require-extension xml-rpc-server)
[procedure] (make-xml-rpc-request-handler procedures)

This creates a procedure which accepts two arguments; an intarweb request object and a response object. It will read an XML-RPC request from the request-port and respond to the request with the response object, writing to its port.

The procedure that is requested to be called is looked up in the procedures argument, which is an alist of procedure name (symbols) to procedure (lambda) mappings. The procedures are called with exactly the arguments that are sent by the client, encoded in the call (call-sxml). They will be converted to regular Scheme values before the procedure is invoked.

To determine how XML-RPC types are mapped to Scheme types and vice-versa, see below. It's important that you read this, because there is some ambiguity in how lists are mapped (either to arrays or to structs).

[procedure] (start-simple-xml-rpc-server procedures [port])

Create a standalone XML-RPC server on port (defaults to 8080), which accepts an XML-RPC request on any URL.

You can also use slightly more low-level procedures to implement your own server to be exactly like you want it to be:

[procedure] (xml-rpc-call->xml-rpc-response call-sxml procedures)

This procedure converts an XML-RPC procedure call described by call-sxml into an SXML representation of the result. The procedure is looked up in procedures, invoked, and its return values are converted into the appropriate SXML structure describing a methodResponse. If an error occurs inside the procedure, the procedure does not exist, or the XML is invalid, a methodResponse encoding the fault is constructed instead.

(use xml-rpc-server)

(xml-rpc-call->xml-rpc-response
  `(*TOP*
     (*PI* xml "version=\"1.0\"")
     (methodCall  ; (xml-rpc-methodcall 'scheme.makeList '(1 2 3))
       (methodName "scheme.makeList")
         (params
           (param (value (int "1")))
           (param (value (int "2")))
           (param (value (int "3"))))))
  `((scheme.makeList . ,list)))
 =>
(methodResponse
  (params
    (param (value
             (array
	       (data
	         (value (i4 "1"))
                 (value (i4 "2"))
                 (value (i4 "3"))))))))
[procedure] (call-xml-rpc-proc call-sxml procedures)

This procedure accepts as call-sxml the SXML representation of a procedure call from a client and calls it, returning its values.

This is exactly like xml-rpc-call->xml-rpc-response, except it does not construct an SXML result tree. Instead, the return values are those returned by the procedure being called. In case the procedure could not be found or if the call contains an invalid XML structure, an exception of type (exn xml-rpc) is thrown. The xml-rpc part of the condition contains a code property which contains the fault code. This is 1 in case the procedure could not be found and 2 in case the XML is bad.

(use xml-rpc-server)

(call-xml-rpc-proc
  `(*TOP*
     (*PI* xml "version=\"1.0\"")
     (methodCall
      (methodName "Math.add")
      (params
       (param (value (int "1")))
       (param (value (int "2")))
       (param (value (int "3"))))))
   `((Math.add . ,+)))
 =>
6

Low-level

Sometimes you want complete control over how Scheme values are mapped to XML-RPC values and vice versa. For that, use this module.

Usage:

 (use xml-rpc-lolevel)
Scheme to XML-RPC
[parameter] (xml-rpc-unparsers [alist])

This parameter controls how Scheme values are encoded into XML-RPC values. The keys of this alist are predicate procedures, the values are conversion procedures. If the predicate procedure returns true for its argument, it's a datatype that will be converted to SXML by the matching conversion procedure.

Defaults to:

`((,vector? . ,vector->xml-rpc-array)
  (,(conjoin number? exact?) . ,number->xml-rpc-int)
  (,number? . ,number->xml-rpc-double)
  (,boolean? . ,boolean->xml-rpc-boolean)
  (,string? . ,->xml-rpc-string)
  (,symbol? . ,->xml-rpc-string)
  (,u8vector? . ,u8vector->xml-rpc-base64)
  (,blob? . ,blob->xml-rpc-base64)
  (,hash-table? . ,hash-table->xml-rpc-struct)
  ;; see below for an explantation of this predicate
  (,nonempty-symbol-keyed-alist? . ,alist->xml-rpc-struct)
  (,list? . ,list->xml-rpc-array))

Order matters in this alist; the converter corresponding to the first predicate returning a true value is used.

The SXML returned by these conversion procedures is the element inside the value element.

(use xml-rpc-lolevel)

(number->xml-rpc-int 1)
 =>
(i4 "1")
[procedure] (value->xml-rpc-fragment value)

This procedure converts any Scheme value to SXML for its XML-RPC representation. It looks up the conversion procedure in the xml-rpc-unparsers parameter.

[procedure] (nonempty-symbol-keyed-alist? obj)

Returns #t when obj is a nonempty list of pairs, each of which has a symbol as car.

The idea behind this predicates is that it helps to do "The Right Thing" when you call an XML-RPC procedure. You can pass in regular lists or alists, and it will try to make the right decision whether to convert your lists to structs or arrays.

The predicate returns true for nonempty lists only because it's much more likely that you will have empty regular lists than empty alists. However, it's important to be aware of this because you might end up with an empty alist. For absolute safety, remove this predicate from the parameter and use only hash-tables.

[procedure] (list->xml-rpc-array list)
[procedure] (vector->xml-rpc-array vector)
[procedure] (number->xml-rpc-int number)
[procedure] (number->xml-rpc-double number)
[procedure] (boolean->xml-rpc-boolean boolean)
[procedure] (u8vector->xml-rpc-base64 u8vector)
[procedure] (blob->xml-rpc-base64 blob)
[procedure] (alist->xml-rpc-struct alist)
[procedure] (hash-table->xml-rpc-struct hash-table)

These procedures pretty much do the obvious thing: they encode a Scheme object of the given type to an SXML representation for use in the XML-RPC request. Again, the return values look like (i4 "1") and (string "foo"), not like (value (string "foo")). Inside arrays and structs, the value is automatically wrapped around the right values.

[procedure] (->xml-rpc-string obj)

This procedure converts the obj to string with ->string and then encodes it in SXML as an XML-RPC string value. This is useful for passing symbols, regular strings or numbers to procedures expecting string representation.

[procedure] (vector->xml-rpc-iso8601 time-vector)

This procedure encodes a "time vector" (10-element vector, as returned by eg seconds->local-time) to an iso8601 string representing the same date. Currently this procedure is not in the parameter list by default, because it's impossible to differentiate between a regular vector that just happens to be 10 elements long and a "time-vector". The same problem exists for integers and the "seconds since the epoch" representation of time.

Using srfi-19 is a solution to this problem, as it provides a distinct datatype for date/time objects. But you would have to make your own conversion routines in this case.

XML-RPC to Scheme
[parameter] (xml-rpc-parsers [alist])

This parameter controls how XML-RPC values are decoded back into Scheme values. The keys of this alist are symbols, the values are conversion procedures. If the name of a predicate procedure returns true for its argument, it's a datatype that will be converted to SXML by the matching conversion procedure.

Defaults to:

`((i4 . ,xml-rpc-int->number)
  (int . ,xml-rpc-int->number)
  (double . ,xml-rpc-double->number)
  (boolean . ,xml-rpc-boolean->number)
  (string . ,xml-rpc-string->string)
  (base64 . ,xml-rpc-base64->u8vector)
  (dateTime.iso8601 . ,xml-rpc-datetime->vector)
  (array . ,xml-rpc-array->vector)
  (struct . ,xml-rpc-struct->hash-table))

The SXML arguments to these conversion procedures is the element inside the value element. In other words, the element name (or car)of its SXML argument is equal to its key in this alist.

Important note: Due to unspecified details of POSIX strptime, you should never rely on proper values in the day-of-year or day-of-week slots for datetime values returned by xml-rpc. This is because XML-RPC internally uses regular ISO8601 format where these fields are not present. Some C libraries derive this information from the other fields (which is possible and can be done correctly), but others do not.

[procedure] (xml-rpc-fragment->value sxml-fragment)

This procedure converts an SXML representation of an XML-RPC value to its Scheme representation. It looks up the conversion procedure in the xml-rpc-parsers parameter.

(use xml-rpc-lolevel)

(xml-rpc-fragment->value '(i4 "1"))
 =>
1
[procedure] (xml-rpc-int->number sxml-fragment)
[procedure] (xml-rpc-double->number sxml-fragment)
[procedure] (xml-rpc-boolean->number sxml-fragment)
[procedure] (xml-rpc-string->string sxml-fragment)
[procedure] (xml-rpc-array->vector sxml-fragment)
[procedure] (xml-rpc-array->list sxml-fragment)
[procedure] (xml-rpc-struct->alist sxml-fragment)
[procedure] (xml-rpc-struct->hash-table sxml-fragment)
[procedure] (xml-rpc-base64->string sxml-fragment)
[procedure] (xml-rpc-base64->u8vector sxml-fragment)
[procedure] (xml-rpc-base64->blob sxml-fragment)
[procedure] (xml-rpc-datetime->vector sxml-fragment)

Convert the given sxml-fragment to the corresponding Scheme value.

Examples

A simple "hello" server:

(require-extension xml-rpc-server)

(define (say-hello var)
  (sprintf "Hello, ~A!" var) )

((start-simple-xml-rpc-server `((hello . ,say-hello)) 4242))

You can access it using this client:

(require-extension xml-rpc-client)

(define srv (xml-rpc-server "http://localhost:4242/RPC2"))
(define hello (srv "hello"))

(print "-> " (hello "you"))

Then run it as follows:

 % csi -script hello.scm &
 % csi -script client.scm
 
 -> Hello, you!

The same server, running as a Spiffy vhost request handler:

(use spiffy xml-rpc)

(define (say-hello var)
  (sprintf "Hello, ~A!" var) )

(vhost-map
  `(("test1" . ,(let ((handler (make-xml-rpc-request-handler
                                 `((hello . ,say-hello)))))
                  (lambda _
                     (handler (current-request)
                              (current-response)))))))

(start-server)

When requesting any resource under the host test1, this will trigger the XML-RPC handler.

(require-extension xml-rpc-client)

(define srv1 (xml-rpc-server "http://test1:8080/RPC2"))
(define hello1 (srv1 "hello"))

(define srv2 (xml-rpc-server "http://localhost:8080/RPC2"))
(define hello2 (srv2 "hello"))


;; Prints:
;; -> Hello, you!
(print "-> " (hello1 "you"))

;; Throws an (exn http client-error), with 404 not found:
(print "-> " (hello2 "you"))

If you wish more control over exactly under which URIs the XML resource is available, you could have a look at uri-dispatch, or roll your own URI path checker.

Changelog

License

 Copyright (c) 2009-2012, Peter Bex
 Parts Copyright (c) 2003-2006, Felix Winkelmann
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:
 
   Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
 
   Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
 
   Neither the name of the author nor the names of its contributors may
   be used to endorse or promote products derived from this software
   without specific prior written permission.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 DAMAGE.