XQuery/REST interface definition
REST interfaces, in particular the URL language used to invoke services, has no equivalent to SOAP's WSDL. This example looks a creating a simple XML schema for defining such an interface, and using an XQuery script to create a generic interface to the site based on the interface definition.
Example REST definition
editHere is a somewhat partial definition of the del.icio.us interface using a home-made schema. Parameters are defined using unique local names for each parameter, and then the services supported by the interface are defined using templates with curly braces delimiting the names of parameters, to be replaced by their actual values.
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<name>del.icio.us</name>
<description><p>An almost complete description of the REST interface of the del.icio.us social
bookmark site, excluding services requiring login</p>
<p> Chris Wallace May 2009</p></description>
<endpoint>http://del.icio.us/</endpoint>
<parameters>
<parameter>
<name>user-id</name>
<purpose>User Identifier</purpose>
<default>morelysq</default>
<tag>model</tag>
</parameter>
<parameter>
<name>tag</name>
<purpose>a group of bookmarks</purpose>
<default>xml</default>
<tag>model</tag>
</parameter>
<parameter>
<name>url</name>
<purpose>bookmark</purpose>
<default>http://xml.com/</default>
<tag>model</tag>
</parameter>
<parameter>
<name>tagview</name>
<purpose>the tag list appearance</purpose>
<options>
<option>list</option>
<option>cloud</option>
</options>
<default>list</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>tagsort</name>
<purpose>the order of tags in the tag list</purpose>
<options>
<option>alpha</option>
<option>freq</option>
</options>
<default>list</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>minfreq</name>
<purpose>the minimum frequency of a tag to appear in the tag list</purpose>
<options>
<option>1</option>
<option>2</option>
<option>5</option>
</options>
<default>1</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>bundleview</name>
<purpose>whether bundles are shown</purpose>
<options>
<option>show</option>
<option>hide</option>
</options>
<default>show</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>pageno</name>
<purpose>the page of the bookmark list</purpose>
<format>[0-9]+</format>
<default>1</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>count</name>
<purpose>the number of bookmarks to shown per page</purpose>
<options>
<option>10</option>
<option>25</option>
<option>50</option>
<option>100</option>
</options>
<default>10</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>search</name>
<purpose>search string</purpose>
<tag>ui</tag>
</parameter>
<parameter>
<name>scope</name>
<purpose>search scope</purpose>
<options>
<option>user</option>
<option>all</option>
<option>web</option>
</options>
<default>all</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>helptopic</name>
<purpose>a page of the help manual</purpose>
<default>urlhistory</default>
<tag>help</tag>
</parameter>
</parameters>
<services>
<service>
<template/>
<purpose>Home Page</purpose>
<tag>home</tag>
</service>
<service>
<template>{user-id}</template>
<purpose>View a user's public bookmarks</purpose>
<tag>user</tag>
</service>
<service>
<template>{user-id}?settagview={tagview}&settagsort={tagsort}&setminfreq={minfreq}&setbundleview={bundleview}&page={pageno}&setcount={count}</template>
<purpose>View a user's public bookmarks, controlling its appearance</purpose>
<tag>user</tag>
</service>
<service>
<template>rss/{user-id}</template>
<purpose>Get an RSS feed of a user's bookmarks - limited to the latest 20 items</purpose>
<tag>user</tag>
<tag>RSS</tag>
</service>
<service>
<template>{user-id}/{tag}</template>
<purpose>View a user's bookmarks by tag</purpose>
<tag>user</tag>
<tag>tag</tag>
</service>
<service>
<template>tag/{tag}</template>
<purpose>View tagged bookmarks</purpose>
<tag>tag</tag>
</service>
<service>
<template>network/{user-id}</template>
<purpose>View a user's network and their tags</purpose>
<tag>user</tag>
<tag>network</tag>
</service>
<service>
<template>subscriptions/{user-id}</template>
<purpose>View a user's subscriptions - i.e.watched bookmarks</purpose>
<tag>user</tag>
<tag>subscriptions</tag>
</service>
<service>
<template>for/{user-id}</template>
<purpose>View links suggested to a user</purpose>
<tag>user</tag>
<tag>links</tag>
</service>
<service>
<template>rss/tag/{tag}</template>
<purpose>Get an RSS feed of tagged bookmarks</purpose>
<tag>tag</tag>
<tag>RSS</tag>
</service>
<service>
<template>popular/{tag}</template>
<purpose>View popular tagged bookmarks</purpose>
<tag>tag</tag>
</service>
<service>
<template>popular/</template>
<purpose>View today's popular bookmarks</purpose>
<tag>current</tag>
</service>
<service>
<template>popular/?new</template>
<purpose>View today's new popular bookmarks</purpose>
<tag>current</tag>
</service>
<service>
<template>url?url={url}</template>
<purpose>View Bookmarks for a URL</purpose>
<tag>url</tag>
</service>
<service>
<template>help/</template>
<purpose>Help index</purpose>
<tag>help</tag>
</service>
<service>
<template>help/{helptopic}</template>
<purpose>View a page of the help manual</purpose>
<tag>help</tag>
</service>
<service>
<template>search/?fr=del_icio_us&p={search}&searchtype={scope}</template>
<purpose>Search for string in different scopes</purpose>
<tag>search</tag>
</service>
<service>
<template>rss/</template>
<purpose>RSS hotlist feed</purpose>
<tag>current</tag>
<tag>RSS</tag>
</service>
<service>
<template>rss/tag/{tag}</template>
<purpose>RSS feed for a tag</purpose>
<tag>tag</tag>
<tag>RSS</tag>
</service>
<service>
<template>html/{user-id}/</template>
<purpose>Get a contolled HTML extract of a user's tag</purpose>
<tag>user</tag>
<tag>html</tag>
</service>
</services>
</interface>
Generate the interface
editAn XQuery script takes one parameter called uri, the uri of the XML interface description. The script creates a generic interface based on this definition, regenerating the service urls when values are changed in the form and the form refreshed.
The Script
editdeclare namespace rest = "http://ww.cems.uwe.ac.uk/xmlwiki/rest";
(: declare global variables :)
declare variable $uri := request:get-parameter("_uri",());
declare variable $index := request:get-parameter("_index","tag");
declare variable $interface := doc(concat($uri,"?r=",math:random()))/interface;
declare function rest:template-parameters($template as xs:string) as xs:string* {
(: parse the template to get the parameters :)
distinct-values(
for $p in subsequence(tokenize($template,"\{"),2)
return substring-before($p,"}")
)
};
declare function rest:parameter-value($name as xs:string) as xs:string? {
let $parameter := $interface/parameters/parameter[name=$name]
return (request:get-parameter($name,$parameter/default),"")[1]
};
declare function rest:replace-template-parameters($template as xs:string, $names as xs:string* ) as xs:string {
(: recursively replace the tempate paramters by their current values :)
if (empty($names))
then $template
else
let $name := $names[1]
let $value := rest:parameter-value($name)
let $templatex :=
if (exists($value))
then replace($template, concat("\{",$name,"\}"),$value)
else $template
return rest:replace-template-parameters( $templatex,subsequence($names,2))
};
(: interface generation :)
declare function rest:parameter-input-field( $parameter as element(parameter) ) as element(span)? {
(: create a parameter field in the parameter input form :)
let $name := $parameter/name
let $value := rest:parameter-value($name)
return
<span class="input">
<label for="{$name}">
{if ($index = "parameter") (: if it the index is by parameter, generate a link to that part of the index :)
then <a href="#{$name}"> {string($name)}</a>
else $name
}
</label>
{if ($parameter/options)
then
<select name="{$name}" title="{$parameter/purpose}" >
{for $option in $parameter/options/option
return
<option value="{$option}" title="{$option/@label}">
{ if ($option= $value)
then attribute selected {"true"}
else ()
}
{string($option)}
</option>
}
</select>
else
<input type="text" name="{$name}" title="{$parameter/purpose}" value="{$value}" size="{string-length($value)+1}"/>
}
</span>
};
declare function rest:parameter-form() {
<form method="post" action="interface.xq">
<div class="subhead"> interface
<div class="group">
<label for="_uri" > uri </label>
<input type="text" name="_uri" value="{$uri}" size="80"/>
</div>
</div>
{for $tag in distinct-values($interface/parameters/parameter/tag)
return
<div>
<div class="subhead">{$tag} </div>
<div class="group">
{ for $parameter in $interface/parameters/parameter[tag=$tag]
return
rest:parameter-input-field($parameter)
}
</div>
</div>
}
<hr/>
Index services by <select name="_index">
{for $index in ("parameter","tag")
return
if ($index = request:get-parameter("index","tag"))
then <option value="{$index}" selected="true"> {$index} </option>
else <option value="{$index}" > {$index} </option>
}
</select>
<hr/><input type="submit" value="refresh"/>
</form>
};
declare function rest:service-link ($service as element(service) )as element(tr) {
<div>
<div class="label">{string($service/purpose)}</div>
{
let $names := rest:template-parameters($service/template)
let $filledTemplate := rest:replace-template-parameters($service/template,$names)
let $uri :=
if (starts-with($service/template,"http://"))
then $filledTemplate
else concat($interface/endpoint,$filledTemplate)
return
<div class="link"><a href="{$uri}">../{$filledTemplate}</a> </div>
}
</div>
};
declare function rest:parameter-index() {
<div id="index">
<h2>Parameter index </h2>
{for $parameter in $interface/parameters/parameter
let $name := $parameter/name
let $match := concat("{",$name,"}")
order by lower-case($name)
return
<div>
<div class="subhead"><a name="{$name}">{string($name)} </a> </div>
<div class="group">
{for $service in $interface//service[contains(template,$match)]
return rest:service-link($service)
}
</div>
</div>
}
</div>
};
declare function rest:tag-index() {
<div id="index">
<h2>Tag index </h2>
{for $tag in distinct-values($interface//service/tag)
order by lower-case($tag)
return
<div>
<div class="subhead" >{$tag}</div>
<div class="group">
{for $service in $interface//service[tag=$tag]
return rest:service-link($service)
}
</div>
</div>
}
</div>
};
declare option exist:serialize "method=xhtml media-type=text/html omit-xml-declaration=no indent=yes
doctype-public=-//W3C//DTD XHTML 1.0 Transitional//EN
doctype-system=http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";
<html>
<head>
<title>Infterface for {string($interface/name)}</title>
<link rel="stylesheet" type="text/css" href="screen.css" />
</head>
<body>
<h1>{string($interface/name)} interface</h1>
{$interface/description/(text(),*)}
<div id="parameters">
{rest:parameter-form()}
</div>
{if (exists($interface))
then
<div id="services">
<h2> Interface properties </h2>
<div class="group">
<div class="label">Interface definition
<div class="link"><a href="{$uri}">{$uri} </a> </div>
</div>
<div class="label">Service endpoint
<div class="link"><a href="{$interface/endpoint}"> {string($interface/endpoint)} </a> </div>
</div>
</div>
{if ($index = "parameter")
then rest:parameter-index()
else if ($index= "tag")
then rest:tag-index()
else ()
}
</div>
else ()
}
</body>
</html>
Discussion
editArchitecture
editThe script uses a common layered architecture in which low level functions operate on the base data model, and these functions are in turn used by functions which generate the user interface. Finally class and id hooks in the generated XHTML link with CSS to style the page. Determining how many layers to use and how the layers should interface is a central design decision in XQuery application, as it is in other technologies. Several alternatives are worth considering: the script generates an intermediate XML structure which is transformed server- or client-side with XSLT; the script generates an XForm in place of the HTML form; the whole task is handled client-side with JavaScript; client-side AJAX interfaces with a base XQuery script. Handling this design space is one of the challenges of web development.
Cache busting
editFor scripts running inside a proxy server,as these scripts are on the UWE server, repeated access to the same url in the doc() function will return the cached file. To break the cache, a random number is added to the URL.
Global Variables
editThe script uses variable declarations to define some global variables used in the script functions. Global variables feel like a reversion to Fortran COMMON and similar horrors, except that these are all constant once defined. Nonetheless, the dependence on these variables is not explicit. An alternative would be to explicitly pass this data down through the functions. An alternative script using this style, passing a single node which composed the data into a single 'object', executes several times slower, is more verbose and arguably no more understandable.
Recursion
editReplacement of the multiple parameters in a template is a recursive function, successively replacing each parameter throughout the template in turn.