Typed Data and Schema-Aware Queries
Validation does more than say yes or no. A validated document carries the types of its schema: each element and attribute is annotated with its type, and its typed value is a number, a name or a list, whatever the type says, instead of untyped text. XPath, XQuery and XSLT can use those types. Processors that can do this are called schema-aware, and sxml is one.
Typed values in XQuery
import schema brings a schema's declarations into a query, and
validate validates a document (or element) and returns a typed copy:
typed.xq:
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
declare namespace vane = "urn:example:vane";
let $machine := validate { / }
for $data in $machine//sc:data
return `{$data/@id}: {data($data/@vane:type)}`
$ sxml xquery --method=adaptive --context-item=flag.scxml typed.xq
"failures: vane:failure-count"
"maxFailures: vane:failure-limit"
"stepSeconds: vane:seconds"
"pauseSeconds: vane:seconds"
The schema imports vane-data.xsd, so its declarations come along;
declare namespace only binds the prefix. vane:type is declared as
an xs:QName, so its typed value is a name with a namespace, not a
string with a colon in it:
$ sxml xquery --method=adaptive --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
declare namespace vane = "urn:example:vane";
let $type := data(((validate { / })//sc:data/@vane:type)[3])
return ($type instance of xs:QName, namespace-uri-from-QName($type), local-name-from-QName($type))'
true()
"urn:example:vane"
"seconds"
The prefix vane was resolved when the document was validated, with
the namespace declarations of the machine; a query that used another
prefix for the same namespace would get the same name.
Lists are sequences. A transition's event is a list of event
descriptors, so @event = "step.done" is true for the transition
that waits for kill.request step.done, as general comparisons
compare each item; the nine transitions handle ten events in all. And
the default value of type, which no transition writes, is there:
$ sxml xquery --method=adaptive --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
let $machine := validate { / }
return (
$machine//sc:transition[@event = "step.done"]/@target/string(),
count($machine//sc:transition/data(@event)),
distinct-values($machine//sc:transition/@type)
)'
"stable"
10
"external"
Types can also be stricter than untyped data. On the document as it is
parsed, the expr of the data items are untyped, and sum takes them
as numbers. After validation, they are what the schema says, strings,
because an expr is an ECMAScript expression, not a number; sum of
strings is an error:
$ sxml xquery --context-item=flag.scxml --expression='
declare namespace sc = "http://www.w3.org/2005/07/scxml";
sum(//sc:data/@expr)'
903
$ sxml xquery --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
sum((validate { / })//sc:data/@expr)'
expression:3:5: err:FORG0006: The values are not all numbers, or not all durations of one kind, or not comparable (F&O 4.0, fn:sum, fn:avg, fn:max, fn:min)
$ sxml xquery --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
sum((validate { / })//sc:data/@expr ! xs:integer(.))'
903
The error is useful: an expr such as failures + 1 is no number, and
a typed query says so instead of returning NaN. Saying what you mean,
xs:integer(.), makes the conversion explicit.
When the document is not valid, validate raises err:XQDY0027 with
the message sxml validate gives, which a query can catch, for example
to check several files and go on:
$ sxml xquery --method=adaptive --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
for $file in ("flag.scxml", "bad-target.scxml", "duplicate-event.scxml")
return try {
(validate { doc($file) }) ! ($file || ": valid")
} catch err:XQDY0027 {
$file || ": " || substring-before($err:description, ":")
}'
"flag.scxml: valid"
"bad-target.scxml: cvc-identity-constraint.4.2.2"
"duplicate-event.scxml: cvc-identity-constraint.4.1"
The description is the message of the first error that validation found.
Validating in a query
validate has three modes:
validate { E }orvalidate strict { E }: the element must have a global declaration, and must be valid by it;validate lax { E }: elements without a declaration are let through, and the ones with a declaration are validated wherever they are;validate type T { E }: the element is validated against the typeT, whatever its name.
A flag's log holds the status payloads the service sent. vane-data.xsd
declares vane:status but no log, and lax validation still finds the
status inside and types it, so its retry-after is a number:
$ sxml xquery --method=adaptive --expression='
import schema namespace vane = "urn:example:vane" at "vane-data.xsd";
let $log := validate lax {
<log><vane:status><vane:flag>new-checkout</vane:flag><vane:state>paused</vane:state>
<vane:failed-checks>3</vane:failed-checks><vane:retry-after>300</vane:retry-after></vane:status></log>
}
return $log/vane:status/vane:retry-after + 1'
301
$ sxml xquery --method=adaptive --expression='
import schema namespace vane = "urn:example:vane" at "vane-data.xsd";
validate { <log/> }'
expression:3:5: err:XQDY0084: Strict validation of an element whose name has no global element declaration among the in-scope schema definitions (XQuery 4.0, 4.25)
A validate expression can't be followed directly by a path or !;
put it in parentheses, (validate { / })//sc:data, as the examples
here do.
validate type works for an element that has no global declaration of
its own. A transition is declared only inside states, but its type is
named:
$ sxml xquery --method=adaptive --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
(validate type sc:transition { <sc:transition event="kill.request" target="stable"/> })/@type/string()'
"external"
Values of your own types
Every named simple type of an imported schema has a constructor
function, as the built-in types do, and cast as and castable as
work with it. The facets are checked:
$ sxml xquery --method=adaptive --expression='
import schema namespace vane = "urn:example:vane" at "vane-data.xsd";
vane:seconds("300") + 10, "3601" castable as vane:seconds, vane:flag-key("new-checkout")'
310
false()
"new-checkout"
$ sxml xquery --method=adaptive --expression='
import schema namespace vane = "urn:example:vane" at "vane-data.xsd";
vane:state("archived")'
expression:3:5: err:FORG0001: cvc-enumeration-valid: the value is not one of the enumeration facets (XSD 1.1 Part 2, 4.3.5.4)
A vane:seconds is a positive integer, so adding to it is integer
arithmetic. archived is a state of the machine, but not one of the
states the protocol lets a service report: the draft knows only
stable, rolling-out and paused.
Checking each value against its type
The machine says the type of each data item with vane:type, but no
schema checks that the item's expr is a value of that type: the
expr is ECMAScript, and the schema can't know that 3 means the
number three. A query can check it. vane:value is declared with the
type xs:anySimpleType, so xsi:type can give it any type, and
validating one with the item's text checks the value. Here is a copy of
the machine that allows 30 failed checks, three times the limit of
vane:failure-limit:
$ sed 's/id="maxFailures" expr="3"/id="maxFailures" expr="30"/' flag.scxml > generous.scxml
$ sxml xquery --method=adaptive --context-item=generous.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
declare namespace vane = "urn:example:vane";
declare namespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
for $data in (validate { / })//sc:data
let $type := data($data/@vane:type)
return try {
let $value := validate {
<vane:value xsi:type="vane:{local-name-from-QName($type)}">{string($data/@expr)}</vane:value>
}
return `{$data/@id}: {data($value)}`
} catch err:XQDY0027 {
`{$data/@id}: {substring-before($err:description, ":")}`
}'
"failures: 0"
"maxFailures: cvc-maxInclusive-valid"
"stepSeconds: 600"
"pauseSeconds: 300"
The constructor functions give a shorter way: function-lookup finds
the constructor whose name is the type's name, and calling it checks
the facets:
$ sxml xquery --method=adaptive --context-item=generous.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
declare namespace vane = "urn:example:vane";
for $data in (validate { / })//sc:data
let $constructor := function-lookup(data($data/@vane:type), 1)
return try { `{$data/@id}: {$constructor($data/@expr)}` }
catch err:FORG0001 { `{$data/@id}: not a {data($data/@vane:type)}` }'
"failures: 0"
"maxFailures: not a vane:failure-limit"
"stepSeconds: 600"
"pauseSeconds: 300"
The xsi:type way also works for complex types and gives the full
message; the constructor works only for simple types. Checking What
the Schema Cannot makes this one check of several.
Elements without a typed value
The typed value of an element with simple content, such as
vane:retry-after, is its value. An element whose type has
element-only content, such as sc:state, has no typed value at all,
and asking for it is an error:
$ sxml xquery --method=adaptive --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
data(((validate { / })//sc:state)[1])'
expression:3:5: err:FOTY0012: An element whose type has element-only content has no typed value (XDM 4.0, section 7.5.2.4)
That also catches a common mistake: $state = "stable" compares the
typed value of $state, so it raises this error for a validated state.
On an untyped document it compares all the text of the state, its
XHTML included, which is rarely what you meant either. Compare
$state/@id instead. Elements of mixed content, such as sc:data,
have their text as an untyped value.
Types in signatures
With a schema imported, sequence types can name its declarations and
types. schema-element(vane:status) is a vane:status element that
was validated by the global declaration. A function with that parameter
type accepts only validated payloads, which catches a whole class of
mistakes:
$ sxml xquery --method=adaptive --expression='
import schema namespace vane = "urn:example:vane" at "vane-data.xsd";
declare function local:summary($status as schema-element(vane:status)) as xs:string {
`{$status/vane:flag}: {$status/vane:state}, {$status/vane:failed-checks} failed`
};
local:summary(validate {
<vane:status><vane:flag>new-checkout</vane:flag><vane:state>stable</vane:state>
<vane:failed-checks>2</vane:failed-checks></vane:status>
})'
"new-checkout: stable, 2 failed"
Pass it the same payload without validate, and the call fails:
$ sxml xquery --method=adaptive --expression='
import schema namespace vane = "urn:example:vane" at "vane-data.xsd";
declare function local:summary($status as schema-element(vane:status)) as xs:string {
string($status/vane:flag)
};
local:summary(<vane:status><vane:flag>new-checkout</vane:flag><vane:state>stable</vane:state>
<vane:failed-checks>2</vane:failed-checks></vane:status>)'
expression:6:5: err:XPTY0004: The value does not match the required type (XPath 4.0, section 3.4, coercion rules)
The sequence types that use a schema are:
element(N, T): an element namedNwhose type isTor derived from it;element(*, T)for any name;attribute(N, T)andattribute(*, T), the same for attributes;schema-element(N): an element that the global declarationNgoverns, or a member of its substitution group;schema-attribute(N): the same for a global attribute declaration, such asschema-attribute(vane:implements).
$ sxml xquery --method=adaptive --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
declare namespace vane = "urn:example:vane";
let $machine := validate { / }
return (
($machine//sc:state)[1] instance of element(*, sc:state),
($machine//@vane:type)[1] instance of attribute(*, xs:QName),
$machine/sc:scxml instance of schema-element(sc:scxml),
/sc:scxml instance of schema-element(sc:scxml)
)'
true()
true()
true()
false()
The last one is false because the context item, the document as it was parsed, was never validated: its elements are untyped.
Typed copies
When a constructor copies validated nodes into a new element, the
copies keep their types only with declare construction preserve.
With strip, they become untyped, as if they came from a document
without a schema:
$ sxml xquery --method=adaptive --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
declare namespace vane = "urn:example:vane";
declare construction preserve;
let $copy := <types>{ (validate { / })//sc:data }</types>
return type-of(data(($copy//@vane:type)[1]))'
"xs:QName"
$ sxml xquery --method=adaptive --context-item=flag.scxml --expression='
import schema namespace sc = "http://www.w3.org/2005/07/scxml" at "scxml.xsd";
declare namespace vane = "urn:example:vane";
declare construction strip;
let $copy := <types>{ (validate { / })//sc:data }</types>
return type-of(data(($copy//@vane:type)[1]))'
"xs:untypedAtomic"
Schema-aware XSLT
In XSLT, xsl:import-schema imports a schema, and the validation
attribute of xsl:copy-of, xsl:document, xsl:result-document and
other instructions (xsl:validation on a literal result element)
validates what they make. Patterns and sequence types can test types.
This stylesheet makes the status the service answers with for a flag,
from the machine's data and parameters for the flag key, the state and
the count of failed checks:
status.xsl:
<!-- The status a flag in a given state answers with, made from the
machine's data and validated as it is made. -->
<xsl:stylesheet version="4.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:sc="http://www.w3.org/2005/07/scxml"
xmlns:vane="urn:example:vane"
exclude-result-prefixes="sc">
<xsl:import-schema namespace="http://www.w3.org/2005/07/scxml"
schema-location="scxml.xsd"/>
<xsl:output method="xml" indent="yes"/>
<!-- Typed parameters: a value given with param=value is converted to
the type, and one that is not of it stops the transformation. -->
<xsl:param name="flag" as="vane:flag-key" select="'new-checkout'"/>
<xsl:param name="state" as="vane:state" select="'stable'"/>
<xsl:param name="failures" as="vane:failure-count" select="0"/>
<!-- Validate the input, so that its values have the schema's types. -->
<xsl:template match="/">
<xsl:variable name="machine" as="document-node(schema-element(sc:scxml))">
<xsl:copy-of select="." validation="strict"/>
</xsl:variable>
<xsl:apply-templates select="$machine/sc:scxml"/>
</xsl:template>
<!-- The new status is validated against its declaration as it is made. -->
<xsl:template match="schema-element(sc:scxml)">
<vane:status xsl:validation="strict">
<vane:flag>{$flag}</vane:flag>
<vane:state>{$state}</vane:state>
<vane:failed-checks>{$failures}</vane:failed-checks>
<xsl:if test="$state = 'paused'">
<vane:retry-after>{sc:datamodel/sc:data[@id = 'pauseSeconds']/@expr}</vane:retry-after>
</xsl:if>
</vane:status>
</xsl:template>
</xsl:stylesheet>
- The parameters have types of the schema, in
as:vane:flag-key,vane:stateandvane:failure-count. A value given on the command line with--paramis text, and it is converted to the parameter's type when the transformation starts; a default inselectis converted the same way. xsl:copy-ofwithvalidation="strict"validates the input, and the variable's type,document-node(schema-element(sc:scxml)), says that it must be a validated machine.match="schema-element(sc:scxml)"matches by declaration, not only by name;match="element(*, sc:state)"would match every element whose type issc:stateor derived from it, whatever its name.xsl:validation="strict"onvane:statusvalidates the new element against its declaration as it is made: its facets, and its assertion.
$ sxml transform --param=state=paused --param=failures=3 status.xsl flag.scxml
<?xml version="1.0" encoding="UTF-8"?>
<vane:status xmlns:vane="urn:example:vane">
<vane:flag>new-checkout</vane:flag>
<vane:state>paused</vane:state>
<vane:failed-checks>3</vane:failed-checks>
<vane:retry-after>300</vane:retry-after>
</vane:status>
A state the protocol doesn't have, or a negative count, is not a value
of the parameter's type, and the transformation stops before it begins,
with the type error for a parameter, XTTE0590:
$ sxml transform --param=state=archived status.xsl flag.scxml
status.xsl: err:XTTE0590: The value does not match the required type of the parameter (XSLT 4.0, section 9.2.1)
$ sxml transform --param=failures=-1 status.xsl flag.scxml
status.xsl: err:XTTE0590: The value does not match the required type of the parameter (XSLT 4.0, section 9.2.1)
The parameters' types check each value on its own. What they make
together is checked when the status is built: a stylesheet with a
mistake, here one that always writes a retry-after, breaks the
status's assertion, and stops with a type error instead of writing an
invalid status:
$ sed "s/test=\"\$state = 'paused'\"/test=\"true()\"/" status.xsl > always.xsl
$ sxml transform always.xsl flag.scxml
always.xsl:28:5: err:XTTE1510: cvc-assertion: element 'vane:status' breaks the assertion 'exists(vane:retry-after) = (vane:state = 'paused')' of an anonymous type derived from 'xs:anyType'
And an input that isn't valid is stopped where it comes in, by the
xsl:copy-of:
$ sxml transform status.xsl bad-target.scxml
status.xsl:21:7: err:XTTE1510: cvc-identity-constraint.4.2.2: element 'Q{http://www.w3.org/2005/07/scxml}state': its key sequence is that of an earlier member (key 'Q{http://www.w3.org/2005/07/scxml}state-id')
A schema-aware stylesheet can also validate a whole output document,
with validation="strict" on xsl:result-document: a transformation
that would write an invalid document fails instead.
From C and JavaScript
Programs validate through the library's schema interface: compile a
schema once, then validate many documents with it, and receive each
error as a value with its constraint and location. Embedding sxml in
C shows the pattern in C (sxml/schema.h), and sxml in
JavaScript in JavaScript, where compileSchema makes a
schema whose validate returns a report of the validity and the
errors.