Chapter 11Calling steps as functions

In XML Calabash, any pipeline that you declare with an explicit type also creates a function with that name. The function is available to XPath expressions with the scope of that step type. (For a little background about this feature, see the weblog post Pipelineception on https://so.nwalsh.com/.)

An example

To see how this works, let’s consider a plausibly useful step, ex:hash:

 1 |<p:declare-step type="ex:hash">
   |  <p:output port="result" content-types="text"/>
   |  <p:option name="text" as="xs:string" required="true"/>
   |  <p:option name="algorithm" as="xs:string" select="'sha'"/>
 5 |  <p:option name="version" as="xs:string?"
   |            select="if ($algorithm = 'sha') then '256' else ()"/>
   | 
   |  <p:hash algorithm="{$algorithm}" value="{$text}" match="/doc">
   |    <p:with-option name="version" select="$version"/>
10 |    <p:with-input>
   |      <doc/>
   |    </p:with-input>
   |  </p:hash>

A typical use for this step in a pipeline might look something like this:

  |<ex:hash text="Hello, world."/>
  |<p:wrap-sequence wrapper="hash"/>

That pipeline fragment takes the SHA256 hash of “Hello, world.” and wraps an element named hash around it, producing:

  |<hash>f8c3bf62a9aa3e6fc1619c250e48abe7519373d3edf41be62eb5dc45199af2ef</hash>

Alternatively, we can take advantage of the fact that a function has been created and call that instead (we’ll come back to the function arguments in a bit):

1 |<p:identity>
  |  <p:with-input>
  |    <hash>{ex:hash(map{'text': 'Hello, world.'})?result}</hash>
  |  </p:with-input>
5 |</p:identity>

On the surface, that’s not a huge improvement. The real magic is that the function is declared in the XPath context of the step, and by extension, in the XPath context of all the steps that occur within it.

Consider this pipeline:

 1 |<p:declare-step xmlns:p="http://www.w3.org/ns/xproc"
   |                xmlns:xs="http://www.w3.org/2001/XMLSchema" 
   |                xmlns:ex="http://example.com/ns"
   |                exclude-inline-prefixes="#all"
 5 |                name="main" version="3.0">
   |  <p:output port="result" sequence="true"/>
   | 
   |  <p:declare-step type="ex:hash">…same as above…</p:declare-step>
   | 
10 |  <p:xslt>
   |    <p:with-input port="source">
   |      <doc>
   |        <hash>Hello, world.</hash>
   |      </doc>
15 |    </p:with-input>
   |    <p:with-input port="stylesheet" href="pception.xsl"/>
   |  </p:xslt>
   | 
   |</p:declare-step>

Where the stylesheet, pception.xsl, is:

 1 |<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
   |                xmlns:ex="http://example.com/ns"
   |                xmlns:xs="http://www.w3.org/2001/XMLSchema"
   |                exclude-result-prefixes="ex xs"
 5 |                expand-text="yes"
   |                version="3.0">
   | 
   |<xsl:output method="xml" encoding="utf-8" indent="no"/>
   | 
10 |<xsl:mode on-no-match="shallow-copy"/>
   | 
   |<xsl:template match="hash">
   |  <p>The hash of “{string(.)}” is:</p>
   |  <xsl:copy>
15 |    <xsl:apply-templates select="@*"/>
   |    <xsl:sequence select="ex:hash(map{'text': string(.)})?result"/>
   |  </xsl:copy>
   |</xsl:template>
   | 
20 |</xsl:stylesheet>

That pipeline will run in XML Calabash, producing the output:

  |<doc>
  |   <p>The hash of “Hello, world.” is:</p>
  |   <hash>f8c3bf62a9aa3e6fc1619c250e48abe7519373d3edf41be62eb5dc45199af2ef</hash>
  |</doc>

That’s because the p:xslt step inherits the XPath context that includes the function definition for ex:hash() and consequently so does the stylesheet.

Calling a step function

The function signature for a “step function” is one argument for each of the step inputs, in declaration order. If the step has options, a final map(xs:QName,item()*) argument is allowed. The key names must be the names of options.

The function always returns a map(xs:NCName, item()*) map where the key names are the names of output ports from the step.

The function signature for our ex:hash function from the example above is:

function ex:hash($options as map(xs:QName, item()*)?) as map(xs:NCName, item()*)

More generally for a step declared thus:

1 |<p:declare-step type="ex:name">
  |  <p:input name="input1"/>
  |  <p:input name="input2"/>
  |  
5 |  <p:input name="inputN"/>
  |  <p:option name="option1"/>
  |  
  |</p:declare-step>

The function signature would be:

 
function ex:name($input1 as item()*,
                 $input2 as item()*,
                 …,
                 $inputN as item()*,
                 options as map(xs:QName, item()*)?)
   as map(xs:NCName, item()*)

Using step functions outside XProc

It’s possible to take this one step further. It’s possible to make this work even if you aren’t running a pipeline. It just requires a little more work to set up.

  1. You have to hook into Saxon’s startup. The -init: option let’s you do that.

  2. You have to be able to point to a library of pipelines. We can use a Java property for that.

So, in fact, this also “just works” (assuming doc.xml contains the input and the ex:hash step is defined in library.xpl):

XLIB=/path/to/library.xpl
CP=...all the jars...
java -Dcom.xmlcalabash.pipelines="$XLIB" -cp "$CP" \
     net.sf.saxon.Transform \
     -init:com.xmlcalabash.api.RegisterSaxonFunctions \
     -s:doc.xml -xsl:pception.xsl

To be fair “…all the jars…” is doing some work in there, but XML Calabash ships with shell and Powershell scripts that point the way.