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

edit

Here 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}&amp;settagsort={tagsort}&amp;setminfreq={minfreq}&amp;setbundleview={bundleview}&amp;page={pageno}&amp;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&amp;p={search}&amp;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

edit

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

del.icio.us interface

The Script

edit
declare 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&#160;XHTML&#160;1.0&#160;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

edit

Architecture

edit

The 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

edit

For 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

edit

The 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

edit

Replacement of the multiple parameters in a template is a recursive function, successively replacing each parameter throughout the template in turn.

Other interface

edit