Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Finding Things with XPath

XPath is the language at the heart of everything else in this book. XQuery is XPath with more around it, and XSLT uses XPath to select what its templates process. Time spent learning XPath pays off three times.

This chapter and the next two work on one document: the specification of the Vane protocol. The Vane service holds the feature flags of applications, and an SDK in each application keeps a cache of them. The protocol is how the service tells the SDKs, on an update stream, that a flag rolled out or was killed, and how an SDK asks for the status of a flag. The draft is written the way the IETF writes its specifications: as an Internet-Draft in XML, in the vocabulary of the xml2rfc tool (RFC 7991), which turns such a file into the HTML and the plain text you know from RFCs. Keeping a specification in XML means a program can answer questions about it: which requirements there are, which terms are used and where, what changed between two versions. By the end of the chapters on XPath, XQuery and XSLT, we'll have the tools to do all of that.

The examples run in docs/book/examples/spec, the directory of the specification project, and give the draft as the context item. Here is the file, vane.xml:

vane.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!-- An Internet-Draft in the xml2rfc version 3 vocabulary (RFC 7991).
     A requirement is a paragraph with an anchor. Anchors are names given
     once, in the order the requirements were written, and never changed,
     so that other documents (flag.scxml) can refer to them; the renderer
     numbers sections and requirements in document order. -->
<rfc version="3" category="std" ipr="trust200902" submissionType="IETF"
     docName="draft-example-vane-protocol-01">
  <front>
    <title abbrev="Vane">The Vane Protocol for Feature Flag Rollouts</title>
    <seriesInfo name="Internet-Draft" value="draft-example-vane-protocol-01"/>
    <author fullname="Ada Byron" initials="A." surname="Byron">
      <organization>Example Software</organization>
      <address><email>ada@example.com</email></address>
    </author>
    <author fullname="Alan Mathison" initials="A." surname="Mathison">
      <organization>Example Software</organization>
      <address><email>alan@example.com</email></address>
    </author>
    <date year="2026" month="October" day="21"/>
    <area>Applications</area>
    <keyword>feature flag</keyword>
    <keyword>progressive delivery</keyword>
    <abstract>
      <t>This document specifies Vane, the protocol between the SDKs that
      evaluate feature flags in applications and the Vane service that
      holds the flags. It defines the states of a flag, the messages of the
      update stream that keeps the cache of each SDK current, and the pause
      that protects users from a rollout that fails its checks.</t>
    </abstract>
  </front>
  <middle>
    <section anchor="intro">
      <name>Introduction</name>
      <t>An application has many features, and each feature has a flag. An
      <xref target="term-sdk">SDK</xref> in the application keeps the flags
      in a cache and evaluates them for each user. It holds an update stream
      open to the Vane service, which sends a message on it whenever a flag
      changes, and it asks the service for the status of a flag.</t>
      <section anchor="terminology">
        <name>Terminology</name>
        <t>The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
        NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED",
        "MAY", and "OPTIONAL" in this document are to be interpreted as
        described in BCP 14 <xref target="RFC2119"/> <xref target="RFC8174"/>
        when, and only when, they appear in all capitals, as shown here.</t>
        <dl>
          <dt anchor="term-flag">Flag</dt>
          <dd>A named switch in an application that turns a feature on for a
          share of its users.</dd>
          <dt anchor="term-sdk">SDK</dt>
          <dd>The library in an application that evaluates flags from its
          cache and keeps the cache current with Vane messages.</dd>
          <dt anchor="term-percentage">Percentage</dt>
          <dd>A whole number from 0 to 100 that an owner gives to roll a
          flag out to that share of users. An SDK puts a user in the share by
          a hash of the flag and the user, so the user stays in it while the
          percentage grows.</dd>
          <dt anchor="term-pause">Pause</dt>
          <dd>A state in which a <xref target="term-flag">flag</xref>
          refuses every rollout for a time.</dd>
        </dl>
      </section>
    </section>
    <section anchor="states">
      <name>Flag States</name>
      <t anchor="req-1">A <xref target="term-flag">flag</xref>
      <bcp14>MUST</bcp14> be in exactly one of the states "stable",
      "rolling-out" and "paused".</t>
      <t anchor="req-2">A flag <bcp14>MUST</bcp14> start in the state
      "stable" when it is created.</t>
      <t anchor="req-3">A rolling-out flag <bcp14>SHOULD</bcp14> return to the
      state "stable" ten minutes after its rollout step began.</t>
    </section>
    <section anchor="messages">
      <name>Messages</name>
      <t>Each message is one line of ASCII text on the update stream: the
      service sends ROLLOUT and KILL, an SDK sends STATUS. The syntax uses
      ABNF <xref target="RFC5234"/>.</t>
      <sourcecode type="abnf" name="vane.abnf"><![CDATA[
message        = rollout-update / kill-update / status-request
rollout-update = "ROLLOUT" SP flag-key SP percentage CRLF
kill-update    = "KILL" SP flag-key CRLF
status-request = "STATUS" SP flag-key CRLF
flag-key       = 1*32(ALPHA / DIGIT / "-")
percentage     = 1*3DIGIT
]]></sourcecode>
      <section anchor="rollout">
        <name>Rollout</name>
        <t anchor="req-4">A service <bcp14>MUST</bcp14> check the guardrail
        metrics of a flag before it applies the
        <xref target="term-percentage">percentage</xref> of a rollout
        request.</t>
        <t anchor="req-5">A service <bcp14>MUST NOT</bcp14> roll out a flag
        while it is in a <xref target="term-pause">pause</xref>
        (<xref target="pause"/>).</t>
        <t anchor="req-9">A service <bcp14>MAY</bcp14> ask an owner for a
        time-based one-time password <xref target="RFC6238"/> before a
        rollout.</t>
      </section>
      <section anchor="status">
        <name>Status</name>
        <t anchor="req-6">A service <bcp14>MUST</bcp14> answer a status request
        with the state of the flag and the number of failed checks since its
        last rollout step.</t>
        <t anchor="req-10">An SDK <bcp14>SHOULD NOT</bcp14> send more
        than one status request per second for a flag; it uses its cached
        answer in between.</t>
      </section>
    </section>
    <section anchor="pause">
      <name>Pause</name>
      <t anchor="req-7">After three failed checks in a row, a service
      <bcp14>MUST</bcp14> put the flag in a <xref target="term-pause">pause</xref>.</t>
      <t anchor="req-8">A pause <bcp14>SHOULD</bcp14> last five minutes.
      <cref anchor="issue-1" source="AM">Should an owner be able to
      change the duration?</cref></t>
      <t anchor="req-11">A service <bcp14>MAY</bcp14> report a pause to the
      owners of the flag as soon as it begins.</t>
    </section>
    <section anchor="security">
      <name>Security Considerations</name>
      <t>Vane messages travel in clear text on the network. Deployments
      <bcp14>SHOULD</bcp14> carry Vane over an encrypted link.
      <cref anchor="issue-2" source="AB">This needs a requirement anchor of
      its own.</cref></t>
    </section>
    <section anchor="iana">
      <name>IANA Considerations</name>
      <t>This document has no IANA actions.</t>
    </section>
  </middle>
  <back>
    <references anchor="references">
      <name>References</name>
      <references anchor="normative">
        <name>Normative References</name>
        <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
          <front>
            <title>Key words for use in RFCs to Indicate Requirement Levels</title>
            <author fullname="S. Bradner" initials="S." surname="Bradner"/>
            <date year="1997" month="March"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="2119"/>
        </reference>
        <reference anchor="RFC5234" target="https://www.rfc-editor.org/info/rfc5234">
          <front>
            <title>Augmented BNF for Syntax Specifications: ABNF</title>
            <author fullname="D. Crocker" initials="D." surname="Crocker" role="editor"/>
            <author fullname="P. Overell" initials="P." surname="Overell"/>
            <date year="2008" month="January"/>
          </front>
          <seriesInfo name="STD" value="68"/>
          <seriesInfo name="RFC" value="5234"/>
        </reference>
        <reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
          <front>
            <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
            <author fullname="B. Leiba" initials="B." surname="Leiba"/>
            <date year="2017" month="May"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="8174"/>
        </reference>
      </references>
      <references anchor="informative">
        <name>Informative References</name>
        <reference anchor="RFC6238" target="https://www.rfc-editor.org/info/rfc6238">
          <front>
            <title>TOTP: Time-Based One-Time Password Algorithm</title>
            <author fullname="D. M'Raihi" initials="D." surname="M'Raihi"/>
            <author fullname="S. Machani" initials="S." surname="Machani"/>
            <author fullname="M. Pei" initials="M." surname="Pei"/>
            <author fullname="J. Rydell" initials="J." surname="Rydell"/>
            <date year="2011" month="May"/>
          </front>
          <seriesInfo name="RFC" value="6238"/>
        </reference>
      </references>
    </references>
  </back>
</rfc>

It is long, but its shape is simple. The front has the title, the authors and the date; the middle has the sections, each with a name and paragraphs t; the back has the references. A requirement is a paragraph with an anchor, and it says how strong it is with one of the key words of BCP 14 (MUST, SHOULD, MAY and their negations) in a bcp14 element. xref elements refer to other parts of the draft by their anchors: to the terms of section 1.1, to sections, and to references. The same directory holds vane-00.xml, the draft's previous version.

What an XPath expression is

An XPath expression is a formula. It never changes anything: it looks at its input and computes a value. Every value is a sequence: zero, one or more items in order. An item is one of three things:

  • a node: a part of a document, such as an element, an attribute or a piece of text;
  • an atomic value: a string, a number, a date, a boolean and so on, each with a type such as xs:string or xs:integer;
  • a function, which includes maps and arrays, as we'll see later in this chapter.

A single item and a sequence of one item are the same thing, and sequences never nest: (1, (2, 3)) is the sequence 1, 2, 3.

$ sxml xpath '(1, (2, 3), ())'
1
2
3

() is the empty sequence. It is how XPath says "nothing", and you'll meet it often: a path that finds nothing gives (), not an error.

The document as a tree

XPath sees a document as a tree of nodes. At the top is the document node, which stands for the whole file. Its children are the comment at the start of the file and the outermost element, rfc:

$ sxml xpath --context-item=vane.xml '/node() ! (if (self::comment()) then "comment" else name())'
"comment"
"rfc"

Each element has its attributes, and its children: more elements, and text nodes for the text between the tags, including the line breaks and spaces that indent the file. Count them:

$ sxml xpath --context-item=vane.xml 'count(//*), count(//@*), count(//text()), count(//comment())'
126
116
219
1

A hundred and twenty-six elements, a hundred and sixteen attributes, two hundred and nineteen text nodes (most of them the whitespace between the tags) and one comment.

Sections nest: a section can contain sections. The draft has six sections at the top of middle and nine in all:

$ sxml xpath --context-item=vane.xml 'count(/rfc/middle/section), count(//section)'
6
9

A paragraph has mixed content: text with elements in it. Requirement req-7 is a text node, the bcp14 element, another text node, an xref and a last text node with the full stop:

$ sxml xpath --context-item=vane.xml '//t[@anchor = "req-7"]/node() ! (if (self::text()) then "text" else "element " || name())'
"text"
"element bcp14"
"text"
"element xref"
"text"

Keep this picture in mind: most of the surprises in XPath come from forgetting that text is made of nodes too.

The rest of this chapter has five parts: