Skip to content

Instantly share code, notes, and snippets.

@lcahlander
Last active April 9, 2019 14:13
Show Gist options
  • Save lcahlander/0eecf499dfe307d7aecbb15f775bd4f2 to your computer and use it in GitHub Desktop.
Save lcahlander/0eecf499dfe307d7aecbb15f775bd4f2 to your computer and use it in GitHub Desktop.
Translate an XML Schema into equivalent JSON Schema
xquery version "3.1";
(:~
This XQuery library module transforms an XML Schema to a JSON Schema equivalent.
NOTE: This is a work in progress. I would appreciate any comments to make it more robust.
To execute the transformation, place the followin in another XQuery module.
xquery version "3.1";
import module namespace xsd2json="http://easymetahub.com/ns/xsd2json" at "xsd2json.xqy";
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";
declare namespace xs="http://www.w3.org/2001/XMLSchema";
declare option output:method "json";
declare option output:indent "yes";
xsd2json:run(.//xs:schema, map { } )
:)
module namespace xsd2json="http://easymetahub.com/ns/xsd2json";
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare namespace xs="http://www.w3.org/2001/XMLSchema";
declare variable $xsd2json:RESTRICTIVE := 'restrictive';
declare variable $xsd2json:SCHEMAID := 'schemaId';
declare variable $xsd2json:KEEP_NAMESPACES := 'keepNamespaces';
declare variable $xsd2json:DATATYPES := ( 'string', 'normalizedString', 'token', 'base64Binary', 'hexBinary', 'integer', 'positiveInteger', 'negativeInteger', 'nonNegativeInteger', 'nonPositiveInteger', 'long', 'unsignedLong', 'int', 'unsignedInt', 'short', 'unsignedShort', 'byte', 'unsignedByte', 'decimal', 'float', 'double', 'duration', 'dateTime', 'date', 'time', 'gYear', 'gYearMonth', 'gMonth', 'gMonthDay', 'gDay', 'Name', 'QName', 'NCName', 'anyURI', 'language', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS', 'anySimpleType', 'anyType', 'token', 'decimal', 'boolean' );
declare function xsd2json:prefix-type($bases as map(*), $attr) {
let $value := xs:string($attr)
let $prefixed := fn:contains($value, ':')
return
if ($prefixed)
then
if (map:get($bases, fn:substring-before($value, ':')) = "http://www.w3.org/2001/XMLSchema")
then attribute { $attr/name() } { 'xs:' || fn:substring-after($value, ':') }
else $attr
else
if (map:get($bases, '') = "http://www.w3.org/2001/XMLSchema")
then
if ($xsd2json:DATATYPES = $value)
then attribute { $attr/name() } { 'xs:' || $value }
else $attr
else $attr
};
declare function xsd2json:change-element-ns-deep
( $nodes as node()* ,
$newns as xs:string ,
$prefix as xs:string ) as node()* {
for $node in $nodes
let $bases := map:merge((
for $prefix in fn:in-scope-prefixes($node/ancestor-or-self::xs:schema)
return map:entry($prefix, xs:string(fn:namespace-uri-for-prefix($prefix, $node/ancestor-or-self::xs:schema)))
))
return if ($node instance of element())
then (element
{QName ($newns,
concat($prefix,
if ($prefix = '')
then ''
else ':',
local-name($node)))}
{
for $attr in $node/@*
return
switch ($attr/name())
case 'type' return xsd2json:prefix-type($bases, $attr)
case 'base' return xsd2json:prefix-type($bases, $attr)
default return $attr
,
xsd2json:change-element-ns-deep($node/node(),
$newns, $prefix)})
else if ($node instance of document-node())
then xsd2json:change-element-ns-deep($node/node(),
$newns, $prefix)
else $node
} ;
(:~
: This is called to transform an XML Schema (and imported + included) to a JSON Schema.
:
: The options that are supported are:
:
: 'keepNamespaces' - set to true if keeping prefices in the property names
: is required otherwise prefixes are eliminated.
: 'schemaId' - the name of the schema
:
: @author Loren Cahlander
: @version 1.0
: @param $base the base XML Schema to be transformed
: @param $options Options to control the transformation
:)
declare function xsd2json:run($base as node(), $options as map(*)) as map(*) {
let $m := map:merge((xsd2json:parse-level('target', $base, ()), $options))
let $base-w-includes := map:get($m, 'target')
let $referred-to-element-names := fn:distinct-values($base-w-includes//xs:element/@ref/string())
let $root-elements := $base-w-includes/xs:element[fn:not(./@name = $referred-to-element-names)]
return
map:merge((
map:entry('id', if (map:contains($options, $xsd2json:SCHEMAID)) then map:get($options, $xsd2json:SCHEMAID) || '#' else 'output.json#'),
map:entry('$schema', 'http://json-schema.org/draft-04/schema#'),
map:entry('version', '0.0.1'),
if (($base-w-includes/xs:complexType[@name], $base-w-includes/xs:simpleType[@name]))
then
(
map:entry('type', 'object'),
map:entry(
'definitions',
map:merge((
for $element in $base-w-includes/xs:complexType[@name]
return xsd2json:complexType($element, map:merge((map:entry('definitions', fn:true()), $m))),
for $element in $base-w-includes/xs:simpleType[@name]
return xsd2json:simpleType($element, map:merge((map:entry('definitions', fn:true()), $m)))
))
)
)
else
(),
if ((fn:count($root-elements) eq 1) and fn:not(($base-w-includes/xs:complexType[@name], $base-w-includes/xs:simpleType[@name])))
then
let $element := $root-elements
let $documentation := ($base-w-includes/xs:annotation/xs:documentation, $element/xs:annotation/xs:documentation, $element/xs:complexType/xs:annotation/xs:documentation)
return
(
xsd2json:documentation($documentation, map { }),
if ($element/@type)
then xsd2json:element-type($element, map:merge(($m, map:entry('noDoc', fn:true()))))
else (),
xsd2json:passthru($element, map:merge(($m, map:entry('noDoc', fn:true())))),
map:entry('additionalProperties', fn:ends-with($element/@type, 'anyType'))
)
else if (fn:count($root-elements) = 0)
then
map:merge((
xsd2json:documentation($base-w-includes/xs:annotation/xs:documentation, map { }),
map:entry('additionalProperties', fn:false()),
map:entry('type', 'object')
))
else if (fn:count($root-elements) gt 1)
then
(
xsd2json:documentation($base-w-includes/xs:annotation/xs:documentation, map { }),
map:entry(
'oneOf',
array {
for $element in $root-elements
return
map:merge((
map:entry('properties', xsd2json:element($element, $m)),
map:entry('additionalProperties', fn:false()),
map:entry('type', 'object'),
map:entry(
'required',
array {
$element/@name/string()
})
))
}
)
)
else
(
xsd2json:documentation($base-w-includes/xs:annotation/xs:documentation, map { }),
for $element in $root-elements
return
map:merge((
map:entry('properties', xsd2json:element($element, $m)),
map:entry('additionalProperties', fn:false()),
map:entry('type', 'object'),
map:entry(
'required',
array {
$element/@name/string()
})
))
)
))};
(:~
:
: Find a loaded schema based on the prefix of the QName.
:
: @author Loren Cahlander
: @version 1.0
: @param $qname the name of the item used to locate the schema
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:schema-from-qname($node as node(), $qname as xs:string, $model as map(*)) as node() {
let $ancestor := $node/ancestor::xs:schema
let $retval :=
if (fn:contains($qname, ':'))
then
let $prefix := fn:substring-before($qname, ':')
let $postfix := fn:substring-after($qname, ':')
let $ns := map:get($model, $prefix)
let $s := if (fn:exists($ns))
then map:get($model, $ns)
else ()
return
if (fn:exists($s))
then $s
else $ancestor
else $ancestor
return ($retval, map:get($model, 'target'))[1]
};
declare function xsd2json:is-xsd-datatype($name as xs:string) as xs:boolean {
try {
let $qname as xs:QName := xs:QName($name)
return (fn:namespace-uri-from-QName($qname) eq "http://www.w3.org/2001/XMLSchema")
} catch * {
let $prefix := fn:substring-before($name, ':')
return (('xs', 'xsd') = $prefix)
}
};
(:~
:
: Find prefix of the QName.
:
: @author Loren Cahlander
: @version 1.0
: @param $qname the name of the item used to locate the schema
:)
declare function xsd2json:prefix-from-qname($qname as xs:string) as xs:string? {
try {
fn:local-name-from-QName(xs:QName($qname))
} catch * {
fn:substring-before($qname, ':')
}
};
(:~
:
: Find postfix of the QName.
:
: @author Loren Cahlander
: @version 1.0
: @param $qname the name of the item used to locate the schema
:)
declare function xsd2json:postfix-from-qname($qname as xs:string) as xs:string? {
try {
fn:local-name-from-QName(xs:QName($qname))
} catch * {
if (fn:contains($qname, ':'))
then fn:substring-after($qname, ':')
else $qname
}
};
(:~
:
: Find a loaded schema based on the ref attribute of the node.
:
: @author Loren Cahlander
: @version 1.0
: @param $qname the name of the item used to locate the schema
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:schema-from-ref($node as node(), $model as map(*)) as node() {
xsd2json:schema-from-qname($node, $node/@ref, $model)
};
(:~
:
: Find postfix from the ref attribute of the node.
:
: @author Loren Cahlander
: @version 1.0
: @param $qname the name of the item used to locate the schema
:)
declare function xsd2json:postfix-from-ref($node as node()) as xs:string? {
xsd2json:postfix-from-qname($node/@ref)
};
(:~
:
: Copy of the FunctX method of the same name.
:
: @author Loren Cahlander
: @version 1.0
: @param $arg the string to trim
:)
declare function xsd2json:trim( $arg as xs:string? ) as xs:string {
replace(replace($arg,'\s+$',''),'^\s+','')
} ;
(:~
:
: Copy of the FunctX method of the same name.
:
: @author Loren Cahlander
: @version 1.0
: @param $arg the string to trim
: @param $regex the pattern to match
:)
declare function xsd2json:substring-before-last-match($arg as xs:string?, $regex as xs:string) as xs:string? {
replace($arg, concat('^(.*)', $regex, '.*'), '$1')
};
(:~
:
: Load an XML Schema from the listed path.
:
: @author Loren Cahlander
: @version 1.0
: @param $path the path to the XML Schema
:)
declare function xsd2json:loadSchema($path as xs:string) {
try {
xsd2json:change-element-ns-deep(fn:doc($path)/xs:schema, "http://www.w3.org/2001/XMLSchema", "xs")
} catch * {
'Falied to load ' || $path
}
};
(:~
:
: Lists all of the prefixes for the namespaces in the set of XML Schemas.
:
: @author Loren Cahlander
: @version 1.0
: @param $uri
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:prefixes-from-namespace($uri as xs:string?, $model as map(*)) as xs:string* {
map:for-each(
$model,
function($k, $v) {
try {
if (fn:starts-with($v, 'http:'))
then $k
else ()
} catch * {
()
}
}
)
};
declare function xsd2json:parse-level($ns as xs:string, $base, $namespaces as xs:string*) as map(*) {
let $baseuri := fn:base-uri($base)
let $cleansed-base := xsd2json:change-element-ns-deep($base, "http://www.w3.org/2001/XMLSchema", "xs")
let $included :=
try {
for $include in $cleansed-base/xs:include
let $loadPath := fn:concat(xsd2json:substring-before-last-match(xs:string($baseuri), '/'), '/', $include/@schemaLocation/string())
return
xsd2json:change-element-ns-deep(fn:doc($loadPath)//xs:schema/node(), "http://www.w3.org/2001/XMLSchema", "xs")
} catch * {
()
}
return
map:merge((
map:entry(xs:string($ns), element { $cleansed-base/name() } { $cleansed-base/@*, $cleansed-base/node(), $included }),
try {
for $prefix in fn:in-scope-prefixes($base)
let $trace := fn:trace($prefix, 'Prefix loaded: ')
return (
map:entry($prefix, fn:namespace-uri-for-prefix($prefix, $base))
)
} catch * {
()
},
try {
for $import in $base//xs:import
let $loadPath := fn:concat(xsd2json:substring-before-last-match(xs:string($baseuri), '/'), '/', $import/@schemaLocation/string())
return
if ($namespaces = $import/@namespace/string())
then map { }
else
xsd2json:parse-level(
$import/@namespace/string(),
xsd2json:loadSchema($loadPath), ($namespaces, $import/@namespace/string()))
} catch * {
()
}
))
};
(:~
: The most effective way to use the typeswitch expression to transform XML is to create a series of XQuery functions.
: In this way, we can cleanly separate the major actions of the transformation into modular functions. (In fact,
: the library of functions can be saved into an XQuery library module, which can then be reused by other XQueries.)
: The "magic" of this typeswitch-style transformation is that once you understand the basic pattern and structure of
: the functions, you can adapt them to your own data. You'll find that the structure is so modular and straightforward
: that it's even possible to teach others the basics of the pattern in a short period of time and empower them
: to maintain and update the transformation rules themselves.
:
: The first function in our module is where the typeswitch expression is located. This function is conventionally called
: the "dispatch" function
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:dispatch($node as node()?, $model as map(*)) as map(*) {
if ($node) then
typeswitch($node)
case element(xs:all) return xsd2json:all($node, $model)
case element(xs:annotation) return xsd2json:annotation($node, $model)
case element(xs:any) return xsd2json:any($node, $model)
case element(xs:anyAttribute) return xsd2json:anyAttribute($node, $model)
case element(xs:appinfo) return xsd2json:appinfo($node, $model)
case element(xs:attribute) return xsd2json:attribute($node, $model)
case element(xs:attributeGroup) return xsd2json:attributeGroup($node, $model)
case element(xs:choice) return xsd2json:choice($node, $model)
case element(xs:complexContent) return xsd2json:complexContent($node, $model)
case element(xs:complexType) return xsd2json:complexType($node, $model)
case element(xs:documentation) return xsd2json:documentation($node, $model)
case element(xs:element) return xsd2json:element($node, $model)
case element(xs:enumeration) return map { }
case element(xs:extension) return xsd2json:extension($node, $model)
case element(xs:field) return xsd2json:field($node, $model)
case element(xs:fractionDigits) return xsd2json:fractionDigits($node, $model)
case element(xs:group) return xsd2json:group($node, $model)
case element(xs:import) return xsd2json:import($node, $model)
case element(xs:include) return xsd2json:include($node, $model)
case element(xs:key) return xsd2json:key($node, $model)
case element(xs:keyref) return xsd2json:keyref($node, $model)
case element(xs:length) return xsd2json:length($node, $model)
case element(xs:list) return xsd2json:list($node, $model)
case element(xs:maxExclusive) return xsd2json:maxExclusive($node, $model)
case element(xs:maxInclusive) return xsd2json:maxInclusive($node, $model)
case element(xs:maxLength) return xsd2json:maxLength($node, $model)
case element(xs:minExclusive) return xsd2json:minExclusive($node, $model)
case element(xs:minInclusive) return xsd2json:minInclusive($node, $model)
case element(xs:minLength) return xsd2json:minLength($node, $model)
case element(xs:notation) return xsd2json:notation($node, $model)
case element(xs:pattern) return xsd2json:pattern($node, $model)
case element(xs:redefine) return xsd2json:redefine($node, $model)
case element(xs:restriction) return xsd2json:restriction($node, $model)
case element(xs:schema) return xsd2json:schema($node, $model)
case element(xs:selector) return xsd2json:selector($node, $model)
case element(xs:sequence) return xsd2json:sequence($node, $model)
case element(xs:simpleContent) return xsd2json:simpleContent($node, $model)
case element(xs:simpleType) return xsd2json:simpleType($node, $model)
case element(xs:totalDigits) return xsd2json:totalDigits($node, $model)
case element(xs:union) return xsd2json:union($node, $model)
case element(xs:unique) return xsd2json:unique($node, $model)
case element(xs:whiteSpace) return xsd2json:whiteSpace($node, $model)
default return xsd2json:passthru($node, $model)
else map { }
};
(:~
:
: The passthru() function recurses through a given node's children, handing each of them back to the main typeswitch operation.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:passthru($node as node()?, $model as map(*)) as map(*) {
map:merge(
if ($node)
then
if ($node/xs:annotation/xs:documentation)
then
(
xsd2json:documentation($node/xs:annotation/xs:documentation, $model),
for $cnode in $node/*
return xsd2json:dispatch($cnode, map:merge(($model, map { 'noDoc': fn:true() })))
)
else
for $cnode in $node/*
return xsd2json:dispatch($cnode, $model)
else ()
)
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:require-dispatch($node as node()?, $model as map(*)) as xs:string* {
if ($node) then
typeswitch($node)
case element(xs:attribute) return xsd2json:require-attribute($node, $model)
case element(xs:attributeGroup) return xsd2json:require-attributeGroup($node, $model)
case element(xs:element) return xsd2json:require-element($node, $model)
case element(xs:complexType) return xsd2json:require-complexType($node, $model)
default return xsd2json:require-passthru($node, $model)
else ()
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:require-passthru($node as node()?, $model as map(*)) as xs:string* {
if ($node) then for $cnode in $node/* return xsd2json:require-dispatch($cnode, $model) else ()
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:require-attribute($node as node(), $model as map(*)) as xs:string? {
(: Attributes:
: Child Elements:
:)
if ($node/@name)
then
if (($node/@minOccurs/fn:number(.) = 1) or ($node[@use = 'required']))
then '@' || $node/@name/string(.)
else ()
else ()
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:require-attributeGroup($node as node(), $model as map(*)) as xs:string* {
(: Attributes:
: Child Elements:
:)
if ($node/@name)
then
xsd2json:require-passthru($node, $model)
else
let $postfix := xsd2json:postfix-from-ref($node)
let $schema := xsd2json:schema-from-ref($node, $model)
return
if ($schema//xs:attributeGroup[@name = $postfix])
then
let $attribute := $schema//xs:attributeGroup[@name = $postfix]
return
if ($attribute)
then xsd2json:require-attributeGroup($attribute, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing attributeGroup', $postfix)
else ()
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:require-complexType($node as node(), $model as map(*)) as xs:string? {
(: Attributes:
: Child Elements:
:)
()
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:require-element($node as node(), $model as map(*)) as xs:string? {
(: Attributes:
: Child Elements:
:)
if ($node/@name)
then
if ($node/@minOccurs/fn:number(.) = 0)
then ()
else $node/@name/string(.)
else if ($node/@ref)
then
if ($node/@minOccurs/fn:number(.) = 0)
then ()
else fn:substring-after($node/@ref/string(.), ':')
else ()
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:maxOccurs-dispatch($node as node()?, $model as map(*)) as map(*) {
if ($node) then
typeswitch($node)
case element(xs:element) return xsd2json:maxOccurs-element($node, $model)
default return xsd2json:maxOccurs-passthru($node, $model)
else map { }
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:maxOccurs-passthru($node as node()?, $model as map(*)) as map(*) {
map:merge(if ($node) then for $cnode in $node/* return xsd2json:maxOccurs-dispatch($cnode, $model) else ())
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:maxOccurs-element($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
if ($node/@name)
then
let $count :=
if ($node/@maxOccurs)
then
if ($node/@maxOccurs/fn:string(.) = 'unbounded')
then 999999999999999
else xs:integer($node/@maxOccurs)
else 1
let $preceding-count :=
for $pnode in $node/following-sibling::xs:element[@name = $node/@name]
return
if ($pnode/@maxOccurs)
then
if ($pnode/@maxOccurs/fn:string(.) = 'unbounded')
then 999999999999999
else xs:integer($pnode/@maxOccurs)
else 1
let $total-count := fn:sum(($count, $preceding-count), 0)
return
map:entry($node/@name/string(.), if ($total-count ge 999999999999999) then 'unbounded' else xs:string($total-count))
else
let $count :=
if ($node/@maxOccurs)
then
if ($node/@maxOccurs/fn:string(.) = 'unbounded')
then 999999999999999
else xs:integer($node/@maxOccurs)
else 1
let $preceding-count :=
for $pnode in $node/following-sibling::xs:element[@ref = $node/@ref]
return
if ($pnode/@maxOccurs)
then
if ($pnode/@maxOccurs/fn:string(.) = 'unbounded')
then 999999999999999
else xs:integer($pnode/@maxOccurs)
else 1
let $total-count := fn:sum(($count, $preceding-count), 0)
return
map:entry($node/@ref/string(.), if ($total-count ge 999999999999999) then 'unbounded' else xs:string($total-count))
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minOccurs-dispatch($node as node()?, $model as map(*)) as map(*) {
if ($node) then
typeswitch($node)
case element(xs:element) return xsd2json:minOccurs-element($node, $model)
case element(xs:attribute) return xsd2json:minOccurs-attribute($node, $model)
case element(xs:attributeGroup) return xsd2json:minOccurs-attributeGroup($node, $model)
default return xsd2json:minOccurs-passthru($node, $model)
else map { }
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minOccurs-passthru($node as node()?, $model as map(*)) as map(*) {
map:merge(if ($node) then for $cnode in $node/* return xsd2json:minOccurs-dispatch($cnode, $model) else ())
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minOccurs-element($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:merge((
if ($node/@name)
then
let $count :=
if ($node/@minOccurs)
then xs:integer($node/@minOccurs)
else 1
let $preceding-count :=
for $pnode in $node/following-sibling::xs:element[@name = $node/@name]
return
if ($pnode/@minOccurs)
then xs:integer($pnode/@minOccurs)
else 1
let $total-count := fn:sum(($count, $preceding-count), 0)
return
map:entry($node/@name/string(.), $total-count)
else
let $count :=
if ($node/@minOccurs)
then xs:integer($node/@minOccurs)
else 1
let $preceding-count :=
for $pnode in $node/following-sibling::xs:element[@ref = $node/@ref]
return
if ($pnode/@minOccurs)
then xs:integer($pnode/@minOccurs)
else 1
let $total-count := fn:sum(($count, $preceding-count), 0)
return
map:entry($node/@ref/string(.), $total-count),
for $attributeGroup in $node/xs:attributeGroup
return xsd2json:minOccurs-attributeGroup($attributeGroup, $model),
for $attribute in $node/xs:attribute
return xsd2json:minOccurs-attribute($attribute, $model)
))
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minOccurs-attributeGroup($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:merge((
for $attribute in $node/xs:attribute
return xsd2json:minOccurs-attribute($attribute, $model)
))
};
(:~
:
: Used for determining cardinality.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minOccurs-attribute($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
let $count :=
if (($node/@minOccurs/fn:number(.) = 1) or ($node[@use = 'required']))
then 1
else 0
return
if ($node/@name)
then map:entry('@' || $node/@name/string(.), $count)
else map:entry('@' || $node/@ref/string(.), $count)
};
(:~
:
: The all element specifies that the child elements can appear in any order.
: Since, in JSON, the order does not matter, call xsd2json:sequence.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
: @see xsd2json:sequence()
:)
declare function xsd2json:all($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: minOccurs
: Child Elements:
:)
xsd2json:sequence($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:extension($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
if (xsd2json:is-xsd-datatype($node/@base))
then
if ($node/xs:attribute)
then
let $minOccurs := xsd2json:minOccurs-passthru($node, $model)
let $required := for $key in map:keys($minOccurs)
order by $key
return if (map:get($minOccurs, $key) gt 0) then $key else ()
return
map:merge((
map:entry('type', 'object'),
map:entry(
'properties',
map:merge((
map:entry(
'value',
xsd2json:dataType($node/@base, $model)
),
xsd2json:passthru($node, $model)
))
),
map:entry('additionalProperties', fn:false()),
map:entry('required', array { for $item in ('value', $required) order by $item return $item } )
))
else
map:merge((
xsd2json:dataType($node/@base, $model),
xsd2json:passthru($node, $model)
))
else
let $postfix := xsd2json:postfix-from-qname($node/@base)
let $schema := xsd2json:schema-from-qname($node, $node/@base, $model)
let $content :=
if ($schema//xs:complexType[@name = $postfix])
then
let $ct := $schema//xs:complexType[@name = $postfix]
return
if ($ct)
then map:get(xsd2json:complexType($ct, $model), $postfix)
else fn:error(xs:QName('xsd2json:err057'), 'missing complexType', $postfix)
else if ($schema//xs:simpleType[@name = $postfix])
then
let $ct := $schema//xs:simpleType[@name = $postfix]
return
if ($ct)
then xsd2json:simpleType($ct, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing simpleType', $postfix)
else if ($schema//xs:restriction[@name = $postfix])
then
let $ct := $schema//xs:restriction[@name = $postfix]
return
if ($ct)
then xsd2json:restriction($ct, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing restriction', $postfix)
else if ($schema//xs:extension[@name = $postfix])
then
let $ct := $schema//xs:extension[@name = $postfix]
return
if ($ct)
then xsd2json:extension($ct, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing extension', $postfix)
else map { }
return
map:merge((
for $key in map:keys($content)
return
map:entry(
$key,
if ($key = 'properties')
then map:merge((
map:get($content, $key),
if ($node/xs:choice)
then
xsd2json:choice($node/xs:choice, $model)
else if ($node/xs:all)
then
for $cnode in $node/xs:all/node()
return xsd2json:passthru($cnode, $model)
else
xsd2json:passthru($node, $model)
))
else map:get($content, $key)
)
))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:annotation($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:merge(xsd2json:passthru($node, $model))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:any($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:anyAttribute($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:appinfo($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: source
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:attribute($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
if ($node/@name)
then
if ($node/@type or $node/xs:simpleType/xs:restriction/@base)
then
let $type := ($node/@type/string(), $node/xs:simpleType/xs:restriction/@base/string(), 'string')[1]
return
if (xsd2json:is-xsd-datatype($type))
then
map:merge((
map:entry(
'@' || $node/@name/string(),
map:merge((
if ($node/@fixed)
then
(
map:entry(
'enum',
array {
switch (xsd2json:dataType-basic($type))
case 'integer' return xs:integer($node/@fixed)
case 'number' return xs:decimal($node/@fixed)
case 'boolean' return xs:boolean($node/@fixed)
default return xs:string($node/@fixed)
}
)
)
else (),
if ($node/@default)
then
(
map:entry(
'default',
switch (xsd2json:dataType-basic($type))
case 'integer' return xs:integer($node/@default)
case 'number' return xs:decimal($node/@default)
case 'boolean' return xs:boolean($node/@default)
default return xs:string($node/@default)
)
)
else
(),
map:entry('isAttribute', fn:true()),
xsd2json:dataType($type, $model),
xsd2json:passthru($node, $model)
))
)
))
else
let $postfix := xsd2json:postfix-from-qname($node/@type)
let $schema := xsd2json:schema-from-qname($node, $node/@type, $model)
return
if ($schema//xs:simpleType[@name = $postfix])
then
let $ct := $schema//xs:simpleType[@name = $postfix]
return
if ($ct)
then
map:entry(
'@' || $node/@name/string(),
map:merge((
if ($node/@fixed)
then
(
map:entry(
'enum',
array {
switch (xsd2json:simpleType-base($node, $model))
case 'integer' return xs:integer($node/@fixed)
case 'number' return xs:decimal($node/@fixed)
case 'boolean' return xs:boolean($node/@fixed)
default return xs:string($node/@fixed)
}
)
)
else (),
if ($node/@default)
then
(
map:entry(
'default',
switch (xsd2json:simpleType-base($node, $model))
case 'integer' return xs:integer($node/@default)
case 'number' return xs:decimal($node/@default)
case 'boolean' return xs:boolean($node/@default)
default return xs:string($node/@default)
)
)
else
(),
map:entry('isAttribute', fn:true()),
xsd2json:simpleType($ct, map:merge(($model, map:entry('noName', fn:true()))))
))
)
else fn:error(xs:QName('xsd2json:err057'), 'missing simpleType', $postfix)
else map:merge(())
else map:entry('@' || $node/@name/string(), map:merge(xsd2json:passthru($node, $model)))
else
let $postfix := xsd2json:postfix-from-ref($node)
let $schema := xsd2json:schema-from-ref($node, $model)
return
if ($schema//xs:attribute[@name = $postfix])
then
let $attribute := $schema//xs:attribute[@name = $postfix]
return
if ($attribute)
then xsd2json:attribute($attribute, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing element', $postfix)
else map:entry('@' || $node/@ref/string(), map:merge(()))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:attributeGroup($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
if ($node/@name)
then
xsd2json:passthru($node, $model)
else
let $postfix := xsd2json:postfix-from-ref($node)
let $schema := xsd2json:schema-from-ref($node, $model)
return
if ($schema//xs:attributeGroup[@name = $postfix])
then
let $attribute := $schema//xs:attributeGroup[@name = $postfix]
return
if ($attribute)
then xsd2json:attributeGroup($attribute, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing attributeGroup', $postfix)
else map { }
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:choice($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
let $enhance := map:put($model, 'noDoc', fn:false())
let $oneof :=
map:entry(
'oneOf',
array {
for $child in $node/*
return
typeswitch($child)
case element(xs:annotation) return ()
case element(xs:sequence)
return map:merge((xsd2json:sequence($child, $model)))
case element(xs:element)
return
let $required := xsd2json:require-passthru($child, $model)
let $maxOccurs := xsd2json:maxOccurs-passthru($child, $model)
let $emodel := map:merge(($model, $maxOccurs))
let $enhance := map:remove($emodel, 'noDoc')
return
map:merge((
map:entry(
'properties',
xsd2json:element($child, $emodel)
),
map:entry('additionalProperties', fn:false()),
if (fn:count($required) gt 0)
then
map:entry(
'required',
array {
for $item in fn:distinct-values($required)
order by $item
return $item
}
)
else
()
))
default
return
xsd2json:passthru($child, $model)
}
)
return
map:merge((
if ($node/../local-name() = 'sequence')
then
map { }
else
(
map:entry('properties', map:merge(())),
map:entry('additionalProperties', fn:false())
),
$oneof
))
};
(:~
:
: Overrides any setting on complexType parent.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:complexContent($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
if ($node/xs:extension)
then xsd2json:extension($node/xs:extension, $model)
else if ($node/xs:restriction)
then xsd2json:restriction($node/xs:restriction, $model)
else map { }
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:complexType($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
let $content := map:merge((
if ($node/xs:annotation) then xsd2json:annotation($node/xs:annotation, $model) else (),
if ($node/xs:complexContent)
then (map:entry('type', 'object'), map:entry('additionalProperties', fn:false()), xsd2json:complexContent($node/xs:complexContent, $model))
else if ($node/xs:simpleContent)
then (map:entry('type', 'object'), map:entry('additionalProperties', fn:false()), xsd2json:simpleContent($node/xs:simpleContent, $model))
else if ($node/xs:sequence)
then (map:entry('type', 'object'), map:entry('additionalProperties', fn:false()), xsd2json:sequence($node/xs:sequence, $model))
else if ($node/xs:choice)
then for $choice in $node/xs:choice return xsd2json:choice($choice, $model)
else if ($node/xs:all)
then (map:entry('type', 'object'), map:entry('additionalProperties', fn:false()), xsd2json:all($node/xs:all, $model))
else if ($node/xs:attribute)
then
(
map:entry('type', 'object'),
map:entry('additionalProperties', fn:false()),
if (xsd2json:require-dispatch($node, $model))
then
map:entry(
'required',
array {
for $item in
xsd2json:require-dispatch($node, $model)
order by $item
return $item
}
)
else
(),
map:entry(
'properties',
map:merge((
for $attr in $node/xs:attribute
return xsd2json:attribute($attr, $model)
))
)
)
else ()
))
return
if ($node/@name and map:contains($model, 'definitions'))
then map:entry($node/@name/string(), $content)
else $content
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $nodes the current node being processed - possibly multiple
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:documentation($nodes as node()*, $model as map(*)) as map(*) {
(: Attributes:
: source
: Child Elements:
:)
let $nl := "
"
let $params :=
<output:serialization-parameters
xmlns:output="http://www.w3.org/2010/xslt-xquery-serialization">
<output:omit-xml-declaration value="yes"/>
<output:method value="xml"/>
<output:escape-uri-attributes value="no"/>
</output:serialization-parameters>
return
if (fn:empty($nodes) or map:get($model, 'noDoc'))
then
map { }
else
map:entry(
'description',
fn:string-join(
for $node in $nodes
return
if ($node/text() and not($node/*))
then xsd2json:trim(xs:string($node))
else xsd2json:trim(fn:replace(fn:serialize(xsd2json:change-element-ns-deep($node, '', '')/node(), $params), "&amp;gt;", ">"))
,
$nl
)
)
};
(:~
:
: Return the JSON Schema equivalent for the requested XML Schema data type
:
: @author Loren Cahlander
: @version 1.0
: @param $type the name of the XML Schema data type
: @param $model - If the map contains the key-value pair 'restrictive' and false then get the non-restrictive type mapping
:)
declare function xsd2json:dataType($type as xs:string, $model as map(*)) as map(*) {
if (map:contains($model, $xsd2json:RESTRICTIVE))
then
if (map:get($model, $xsd2json:RESTRICTIVE))
then xsd2json:dataType-restrictive($type)
else xsd2json:dataType-non-restrictive($type)
else xsd2json:dataType-restrictive($type)
};
(:~
:
: Return the restrictive JSON Schema equivalent for the requested XML Schema data type
:
: @author Loren Cahlander
: @version 1.0
: @param $type the name of the XML Schema data type
:)
declare function xsd2json:dataType-restrictive($type as xs:string) as map(*) {
let $xsdType := xsd2json:postfix-from-qname($type)
return
map:merge((
switch($xsdType)
case 'string' return map {
'type': 'string'
}
case 'normalizedString' return map {
'type': 'string'
}
case 'token' return map {
'type': 'string'
}
case 'base64Binary' return map {
'type': 'string',
'pattern': '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$'
}
case 'hexBinary' return map {
'type': 'string',
'pattern': '^([0-9a-fA-F]{2})*$'
}
case 'integer' return map {
'type': 'integer'
}
case 'positiveInteger' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:true()
}
case 'negativeInteger' return map {
'type': 'integer',
'maximum': 0,
'exclusiveMaximum': fn:true()
}
case 'nonNegativeInteger' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:false()
}
case 'nonPositiveInteger' return map {
'type': 'integer',
'maximum': 0,
'exclusiveMaximum': fn:false()
}
case 'long' return map {
'type': 'integer',
'minimum': -9223372036854775808,
'maximum': 9223372036854775807,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'unsignedLong' return map {
'type': 'integer',
'minimum': 0,
'maximum': 18446744073709551615,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'int' return map {
'type': 'integer',
'minimum': -2147483648,
'maximum': 2147483647,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'unsignedInt' return map {
'type': 'integer',
'minimum': 0,
'maximum': 4294967295,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'short' return map {
'type': 'integer',
'minimum': -32768,
'maximum': 32767,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'unsignedShort' return map {
'type': 'integer',
'minimum': 0,
'maximum': 65535,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'byte' return map {
'type': 'integer',
'minimum': -128,
'maximum': 127,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'unsignedByte' return map {
'type': 'integer',
'minimum': 0,
'maximum': 255,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'decimal' return map {
'type': 'number'
}
case 'float' return map {
'type': 'number'
}
case 'double' return map {
'type': 'number'
}
case 'duration' return map {
'type': 'string',
'pattern': '^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d+[HMS])(\d+H)?(\d+M)?(\d+S)?)?$'
}
case 'dateTime' return map {
'type': 'string',
'pattern': '^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))(T((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$'
}
case 'date' return map {
'type': 'string',
'pattern': '^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$'
}
case 'time' return map {
'type': 'string',
'pattern': '^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d)(.(\d{3}))?)?$'
}
case 'gYear' return map {
'type': 'integer',
'minimum': 1,
'maximum': 9999,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'gYearMonth' return map {
'type': 'string',
'pattern': '^(19|20)\d\d-(0[1-9]|1[012])$'
}
case 'gMonth' return map {
'type': 'integer',
'minimum': 1,
'maximum': 12,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'gMonthDay' return map {
'type': 'string',
'pattern': '^(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$'
}
case 'gDay' return map {
'type': 'integer',
'minimum': 1,
'maximum': 31,
'exclusiveMinimum': fn:false(),
'exclusiveMaximum': fn:false()
}
case 'Name' return map {
'type': 'string',
'pattern': '^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9:A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'QName' return map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*: [A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'NCName' return map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'anyURI' return map {
'type': 'string',
'pattern': '^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
}
case 'language' return map {
'type': 'string',
'pattern': '^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$'
}
case 'ID' return map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'IDREF' return map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'IDREFS' return map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'ENTITY' return map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'ENTITIES' return map {
'type': 'array',
'items': map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
}
case 'NMTOKEN' return map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
case 'NMTOKENS' return map {
'type': 'array',
'items': map {
'type': 'string',
'pattern': '^[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][-.0-9A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*$'
}
}
case 'anySimpleType' return map {
'oneOf': array {
map { 'type': 'integer' },
map { 'type': 'string' },
map { 'type': 'number' },
map { 'type': 'boolean' },
map { 'type': 'null' }
}
}
case 'anyType' return map { }
case 'token' return map {
'type': 'string'
}
case 'decimal' return map {
'type': 'number'
}
case 'boolean' return map {
'type': 'boolean'
}
default return map {
'type': $type
}
))
};
(:~
:
: Return the non-restrictive JSON Schema equivalent for the requested XML Schema data type
:
: @author Loren Cahlander
: @version 1.0
: @param $type the name of the XML Schema data type
:)
declare function xsd2json:dataType-non-restrictive($type as xs:string) as map(*) {
let $xsdType := xsd2json:postfix-from-qname($type)
return
map:merge((
switch($xsdType)
case 'string' return map {
'type': 'string'
}
case 'normalizedString' return map {
'type': 'string'
}
case 'token' return map {
'type': 'string'
}
case 'base64Binary' return map {
'type': 'string'
}
case 'hexBinary' return map {
'type': 'string'
}
case 'integer' return map {
'type': 'integer'
}
case 'positiveInteger' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:true()
}
case 'negativeInteger' return map {
'type': 'integer',
'maximum': 0,
'exclusiveMaximum': fn:true()
}
case 'nonNegativeInteger' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:false()
}
case 'nonPositiveInteger' return map {
'type': 'integer',
'maximum': 0,
'exclusiveMaximum': fn:false()
}
case 'long' return map {
'type': 'integer'
}
case 'unsignedLong' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:false()
}
case 'int' return map {
'type': 'integer'
}
case 'unsignedInt' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:false()
}
case 'short' return map {
'type': 'integer'
}
case 'unsignedShort' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:false()
}
case 'byte' return map {
'type': 'integer'
}
case 'unsignedByte' return map {
'type': 'integer',
'minimum': 0,
'exclusiveMinimum': fn:false()
}
case 'decimal' return map {
'type': 'number'
}
case 'float' return map {
'type': 'number'
}
case 'double' return map {
'type': 'number'
}
case 'duration' return map {
'type': 'string',
'pattern': '^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d+[HMS])(\d+H)?(\d+M)?(\d+S)?)?$'
}
case 'dateTime' return map {
'type': 'string',
'pattern': '^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))(T((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$'
}
case 'date' return map {
'type': 'string',
'pattern': '^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$'
}
case 'time' return map {
'type': 'string',
'pattern': '^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d)(.(\d{3}))?)?$'
}
case 'gYear' return map {
'type': 'integer'
}
case 'gYearMonth' return map {
'type': 'string',
'pattern': '^(19|20)\d\d-(0[1-9]|1[012])$'
}
case 'gMonth' return map {
'type': 'integer'
}
case 'gMonthDay' return map {
'type': 'string',
'pattern': '^(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$'
}
case 'gDay' return map {
'type': 'integer'
}
case 'Name' return map {
'type': 'string'
}
case 'QName' return map {
'type': 'string'
}
case 'NCName' return map {
'type': 'string'
}
case 'anyURI' return map {
'type': 'string'
}
case 'language' return map {
'type': 'string'
}
case 'ID' return map {
'type': 'string'
}
case 'IDREF' return map {
'type': 'string'
}
case 'IDREFS' return map {
'type': 'string'
}
case 'ENTITY' return map {
'type': 'string'
}
case 'ENTITIES' return map {
'type': 'array',
'items': map {
'type': 'string'
}
}
case 'NMTOKEN' return map {
'type': 'string'
}
case 'NMTOKENS' return map {
'type': 'array',
'items': map {
'type': 'string'
}
}
case 'anySimpleType' return map {
'oneOf': array {
map { 'type': 'integer' },
map { 'type': 'string' },
map { 'type': 'number' },
map { 'type': 'boolean' },
map { 'type': 'null' }
}
}
case 'anyType' return map { }
case 'token' return map {
'type': 'string'
}
case 'decimal' return map {
'type': 'number'
}
case 'boolean' return map {
'type': 'boolean'
}
default return map {
'type': $type
}
))
};
(:~
:
: Return the non-restrictive JSON Schema equivalent for the requested XML Schema data type
:
: @author Loren Cahlander
: @version 1.0
: @param $type the name of the XML Schema data type
:)
declare function xsd2json:dataType-basic($type as xs:string) as xs:string {
let $xsdType := xsd2json:postfix-from-qname($type)
return
switch($xsdType)
case 'integer' return 'integer'
case 'gYear' return 'integer'
case 'gMonth' return 'integer'
case 'gDay' return 'integer'
case 'positiveInteger' return 'integer'
case 'negativeInteger' return 'integer'
case 'nonNegativeInteger' return 'integer'
case 'nonPositiveInteger' return 'integer'
case 'long' return 'integer'
case 'unsignedLong' return 'integer'
case 'int' return 'integer'
case 'unsignedInt' return 'integer'
case 'short' return 'integer'
case 'unsignedShort' return 'integer'
case 'byte' return 'integer'
case 'unsignedByte' return 'integer'
case 'decimal' return 'number'
case 'float' return 'number'
case 'double' return 'number'
case 'decimal' return 'number'
case 'boolean' return 'boolean'
default return 'string'
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:element-type($node as node(), $model as map(*)) as map(*) {
map:merge((
if (xsd2json:is-xsd-datatype($node/@type))
then
(
xsd2json:dataType($node/@type, $model),
if ($node/@fixed)
then
(
map:entry(
'enum',
array {
switch (xsd2json:dataType-basic($node/@type))
case 'integer' return xs:integer($node/@fixed)
case 'number' return xs:decimal($node/@fixed)
case 'boolean' return xs:boolean($node/@fixed)
default return xs:string($node/@fixed)
}
)
)
else (),
xsd2json:passthru($node, $model)
)
else
let $prefix := if (fn:contains($node/@type, ':')) then fn:substring-before($node/@type, ':') else ""
let $postfix := if (fn:contains($node/@type, ':')) then fn:substring-after($node/@type, ':') else $node/@type/string()
let $ns := if (fn:contains($node/@type, ':')) then (map:get($model, $prefix), 'target')[1] else ()
let $schema := if (fn:contains($node/@type, ':')) then map:get($model, $ns) else $node/ancestor::xs:schema
let $emodel :=
if ($node/xs:annotation/xs:documentation)
then map:merge(($model, map:entry('noDoc', fn:true())))
else $model
return
(
if ($node/xs:annotation/xs:documentation)
then xsd2json:documentation($node/xs:annotation/xs:documentation, map { })
else (),
if ($schema//xs:complexType[@name = $postfix])
then
let $ct := $schema//xs:complexType[@name = $postfix]
let $content :=
if ($ct)
then xsd2json:complexType($ct, $emodel)
else fn:error(xs:QName('xsd2json:err057'), 'missing complexType', $postfix)
return
(
$content,
if ($node/@fixed)
then
(
map:entry(
'enum',
array {
switch (map:get($content, 'type'))
case 'integer' return xs:integer($node/@fixed)
case 'number' return xs:decimal($node/@fixed)
case 'boolean' return xs:boolean($node/@fixed)
default return xs:string($node/@fixed)
}
)
)
else ()
)
else if ($schema//xs:simpleType[@name = $postfix])
then
let $st := $schema//xs:simpleType[@name = $postfix]
let $content :=
if ($st)
then xsd2json:simpleType($st, $emodel)
else fn:error(xs:QName('xsd2json:err057'), 'missing simpleType', $postfix)
return
(
$content,
if ($node/@fixed)
then
(
map:entry(
'enum',
array {
switch (map:get($content, 'type'))
case 'integer' return xs:integer($node/@fixed)
case 'number' return xs:decimal($node/@fixed)
case 'boolean' return xs:boolean($node/@fixed)
default return xs:string($node/@fixed)
}
)
)
else ()
)
else ()
)
))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:element($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
let $maximums := if (map:contains($model, 'maxOccurs')) then map:get($model, 'maxOccurs') else map { }
let $minimums := if (map:contains($model, 'minOccurs')) then map:get($model, 'minOccurs') else map { }
return
if ($node/@name)
then
let $minOccurs := if (map:contains($minimums, $node/@name)) then map:get($minimums, $node/@name) else '1'
let $maxOccurs := if (map:contains($maximums, $node/@name)) then map:get($maximums, $node/@name) else '1'
return
if ($node/@abstract/string() = 'true')
then map:entry(
$node/@name/string(),
map:merge((
map:entry('abstract', fn:true()),
for $prefix in xsd2json:prefixes-from-namespace(fn:root($node)/@targetNamespace/string(), $model)
return map:for-each($model, function($k, $v) {
try {
let $subGroup := $prefix || ':' || $node/@name/string()
let $element := $v//xs:element[@substitutionGroup = $subGroup]
return
if ($element)
then map:merge(( map:entry('substitutionGroup', $subGroup), xsd2json:element-type($element, $model)))
else map { }
} catch * { map { } }
} )
))
)
else
let $enhance := map:put($model, 'noDoc', fn:true())
let $passthrough-content := xsd2json:passthru($node, $model)
let $content := map:merge((
if ($node/xs:annotation/xs:documentation)
then
xsd2json:documentation($node/xs:annotation/xs:documentation, map { })
else
(),
if ($node/@type)
then if (($node/xs:attribute, $node/xs:attributeGroup))
then map:entry('value', xsd2json:element-type($node, $model))
else xsd2json:element-type($node, $model)
else if ($node/@fixed)
then
let $err := fn:error(xs:QName("xsd2json:aaa"), "fixed type is ", xs:string(map:get($passthrough-content, 'type')))
return
(
map:entry(
'enum',
array {
switch (xs:string((
map:get($passthrough-content, 'type'),
map:get(map:get($passthrough-content, 'items')[1], 'type')
)[1])
)
case 'integer' return xs:integer($node/@fixed)
case 'number' return xs:decimal($node/@fixed)
case 'boolean' return xs:boolean($node/@fixed)
default return xs:string($node/@fixed)
}
)
)
else (),
$passthrough-content))
return
map:entry(
$node/@name/string(),
switch ($maxOccurs)
case '1' return $content
case 'unbounded'
return
map:merge((
map:entry('type', 'array'),
map:entry('minItems', xs:integer(($node/@minOccurs/string(), '1')[1])),
map:entry('items', map:merge((map:entry('type', 'object'), $content)))
))
default
return
map:merge((
map:entry('type', 'array'),
map:entry('minItems', xs:integer($minOccurs)),
map:entry('maxItems', xs:integer($maxOccurs)),
map:entry('items', map:merge((map:entry('type', 'object'), $content)))
))
)
else let $postfix := xsd2json:postfix-from-ref($node)
let $schema := xsd2json:schema-from-ref($node, $model)
let $minOccurs := if (map:contains($minimums, $postfix)) then map:get($minimums, $postfix) else '1'
let $maxOccurs := if (map:contains($maximums, $postfix)) then map:get($maximums, $postfix) else '1'
return
if ($schema//xs:element[@name = $postfix])
then
let $element := $schema//xs:element[@name = $postfix]
return
xsd2json:element(
element { 'xs:element' } {
for $attr in $element/@*
return
typeswitch ($attr)
case attribute(name)
return
attribute name {
if (map:get($model, $xsd2json:KEEP_NAMESPACES))
then $node/@ref/string()
else $element/@name/string()
}
case attribute(maxOccurs) return ()
case attribute(minOccurs) return ()
default return $attr
,
$node/@minOccurs,
$node/@maxOccurs,
$element/*
},
$model
)
else fn:error(xs:QName('xsd2json:err057'), 'missing element', $postfix)
};
(:~
:
: A subset of XPath expressions for use in fields
:
: A utility type, not for public use
:
: The following pattern is intended to allow XPath
: expressions per the same EBNF as for selector,
: with the following change:
: Path ::= ('.//')? ( Step '/' )* ( Step | '@' NameTest )
:
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:field($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: This item is not supported in JSON Schema as JavaScript has no restrictions in the number of digits of a floating point number.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:fractionDigits($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map { }
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:group($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
if ($node/@name)
then
xsd2json:passthru($node, $model)
else
let $schema := xsd2json:schema-from-ref($node, $model)
let $postfix := xsd2json:postfix-from-ref($node)
return
if ($schema//xs:group[@name = $postfix])
then
xsd2json:group($schema//xs:group[@name = $postfix], $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing group', $postfix)
};
(:~
:
: Handled in the parse-level() while loading the schemas.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:import($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map { }
};
(:~
:
: Handled in the parse-level() while loading the schemas.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:include($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map { }
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:key($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:keyref($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:length($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:merge((
map:entry('minLength', $node/@value/number()),
map:entry('maxLength', $node/@value/number())
))
};
(:~
:
: itemType attribute and simpleType child are mutually
: exclusive, but one or other is required
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:list($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:merge((
map:entry('type', 'array'),
if ($node/xs:annotation)
then
xsd2json:annotation($node/xs:annotation, $model)
else
(),
map:entry(
'items',
array {
if ($node/@itemType)
then
xsd2json:dataType($node/@itemType, $model)
else
for $simpleType in $node/xs:simpleType
return xsd2json:simpleType($simpleType, $model)
}
)
))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:maxExclusive($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map {
'maximum': $node/@value/number(.),
'exclusiveMaximum': true()
}
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:maxInclusive($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map {
'maximum': $node/@value/number(.),
'exclusiveMaximum': false()
}
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:maxLength($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:entry('maxLength', $node/@value/number())
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minExclusive($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map {
'minimum': $node/@value/number(.),
'exclusiveMinimum': true()
}
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minInclusive($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map {
'minimum': $node/@value/number(.),
'exclusiveMinimum': false()
}
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:minLength($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:entry('minLength', $node/@value/number())
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:notation($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:pattern($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: value
: Child Elements:
: xs:annotation
:)
if ($node/preceding-sibling::xs:pattern)
then map { }
else if ($node/following-sibling::xs:pattern)
then
map:entry('pattern', fn:concat('^(', fn:string-join(($node/@value/string(), $node/following-sibling::xs:pattern/@value/string()), '|'), ')$'))
else
map:entry('pattern', fn:concat('^', $node/@value/string(), '$'))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:redefine($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: base attribute and simpleType child are mutually
: exclusive, but one or other is required
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:restriction($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:merge((
if (xsd2json:is-xsd-datatype($node/@base))
then (
xsd2json:passthru($node, $model),
xsd2json:dataType($node/@base, $model)
)
else
let $postfix := xsd2json:postfix-from-qname($node/@base)
let $schema := xsd2json:schema-from-qname($node, $node/@base, $model)
return
if ($schema//xs:complexType[@name = $postfix])
then
let $ct := $schema//xs:complexType[@name = $postfix]
return
if ($ct)
then xsd2json:complexType($ct, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing complexType', $postfix)
else if ($schema//xs:simpleType[@name = $postfix])
then
let $ct := $schema//xs:simpleType[@name = $postfix]
return
if ($ct)
then xsd2json:simpleType($ct, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing simpleType', $postfix)
else if ($schema//xs:restriction[@name = $postfix])
then
let $ct := $schema//xs:restriction[@name = $postfix]
return
if ($ct)
then xsd2json:restriction($ct, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing restriction', $postfix)
else if ($schema//xs:extension[@name = $postfix])
then
let $ct := $schema//xs:extension[@name = $postfix]
return
if ($ct)
then xsd2json:extension($ct, $model)
else fn:error(xs:QName('xsd2json:err057'), 'missing extension', $postfix)
else map:merge(()),
if ($node/xs:enumeration)
then (
map:entry(
'enum',
array {
for $enumeration in $node/xs:enumeration
return $enumeration/@value/string()
}
)
)
else (),
xsd2json:passthru($node, $model)
))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:schema($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: A subset of XPath expressions for use
: in selectors
:
: A utility type, not for public
: use
:
: The following pattern is intended to allow XPath
: expressions per the following EBNF:
: Selector ::= Path ( '|' Path )*
: Path ::= ('.//')? Step ( '/' Step )*
: Step ::= '.' | NameTest
: NameTest ::= QName | '*' | NCName ':' '*'
: child:: is also allowed
:
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:selector($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:sequence($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
let $minOccurs := map:merge((
xsd2json:minOccurs-passthru($node, $model),
for $attr in ($node/../xs:attribute, $node/../xs:attributeGroup)
return xsd2json:minOccurs-passthru($attr, $model)
))
let $maxOccurs := xsd2json:maxOccurs-passthru($node, $model)
let $required := for $key in map:keys($minOccurs)
order by $key
return if (map:get($minOccurs, $key) gt 0) then $key else ()
return
map:merge((
map:entry('type', 'object'),
map:entry(
'properties',
map:merge(
for $cnode in ($node/*, $node/../xs:attribute, $node/../xs:attributeGroup)
return
typeswitch($cnode)
case element(xs:choice) return ()
default return xsd2json:dispatch($cnode, map:merge(($model, map:entry('maxOccurs', $maxOccurs), map:entry('minOccurs', $minOccurs))))
)
),
map:entry('additionalProperties', if ($node/xs:any) then fn:true() else fn:false()),
if (fn:count($required) gt 0)
then map:entry('required', array { $required } )
else (),
(: oneOf needs to be outside of properties :)
for $cnode in $node/xs:choice
return xsd2json:choice($cnode, $model)
))
(: :)
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:simpleContent($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
if ($node/xs:extension)
then xsd2json:extension($node/xs:extension, $model)
else if ($node/xs:restriction)
then xsd2json:restriction($node/xs:restriction, $model)
else map { }
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:simpleType($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
let $content := xsd2json:passthru($node, $model)
return
if (map:contains($model, "noName"))
then $content
else
if ($node/@name and map:contains($model, 'definitions'))
then map:entry($node/@name/string(), $content)
else $content
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:simpleType-base($node as node(), $model as map(*)) as xs:string {
(: Attributes:
: Child Elements:
:)
(map:get(xsd2json:passthru($node, $model), 'type'), 'string')[1]
};
(:~
:
: This item is not supported in JSON Schema as JavaScript has no restrictions in the number of digits of a floating point number.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:totalDigits($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: value
: Child Elements:
: xs:annotation
:)
map { }
};
(:~
:
: memberTypes attribute must be non-empty or there must be
: at least one simpleType child
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:union($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
map:merge((
(: If the attribute memberTypes is populated, then tokenize the attribute and get the simpleType and process :)
for $memberType in fn:tokenize($node/@memberTypes/string(), ' ')
let $schema := xsd2json:schema-from-qname($node, $memberType, $model)
let $simpleType := $schema/xs:simpleType[@name = $memberType]
return xsd2json:dispatch($simpleType, $model),
(: If there are multiple simpleTypes within this union, then process each, ignoring the annotation :)
for $simpleType in $node/xs:SimpleType
return xsd2json:dispatch($simpleType, $model)
))
};
(:~
:
: TODO: Document this function.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:unique($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: Child Elements:
:)
xsd2json:passthru($node, $model)
};
(:~
:
: This item is not supported in JSON Schema as JavaScript has no restrictions that match.
:
: @author Loren Cahlander
: @version 1.0
: @param $node the current node being processed
: @param $model a map(*) used for passing additional information between the levels
:)
declare function xsd2json:whiteSpace($node as node(), $model as map(*)) as map(*) {
(: Attributes:
: value
: Child Elements:
: xs:annotation
:)
map { }
};
@raducoravu
Copy link

My main XQuery looks like this:

    xquery version "3.1";
    import module namespace xsd2json="http://easymetahub.com/ns/xsd2json" at "xsd2json.xqy";
    
    declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
    declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization"; 
    declare namespace xs="http://www.w3.org/2001/XMLSchema";
    
    declare option output:method "json";
    declare option output:indent "yes";
    
    xsd2json:run(.//xs:schema)

I cannot get it to work even with a very small XML Schema like:

        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
            <xs:element name="personnel">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element ref="person" minOccurs="1" maxOccurs="unbounded"/>
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
        
            <xs:element name="person"/>
        </xs:schema>

it reports:

            Description
            An empty sequence is not allowed as the result of call to xsd2json:schema-from-ref
            Severity
            Error
            System ID
            C:\Users\radu_coravu\Desktop\xsd2json.xqy
            Scenario
            xsd2json
            XQuery file
            C:\Users\radu_coravu\Desktop\main.xquery
            XML file
            C:\Users\radu_coravu\Desktop\personal.xsd
            Engine name
            Saxon-PE XQuery 9.7.0.19
            Start location
            line: 920, column: 25

@lcahlander
Copy link
Author

The XSD needs to be the context for the running. You can change the call to

xsd2json:run(fn:doc('myschema.xsd')//xs:schema)

@rwalkerands
Copy link

I'm using BaseX as XQuery engine, and this as my wrapper script as per the instructions at the beginning of the file:

#!/bin/sh

: ${BASEX:=/Users/rwalker/Software/BaseX/basex}

${BASEX} -i $1 \
 -q '
xquery version "3.1";
import module namespace xsd2json="http://easymetahub.com/ns/xsd2json" at "xsd2json.xqy";

declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";
declare namespace xs="http://www.w3.org/2001/XMLSchema";

declare option output:method "json";
declare option output:indent "yes";

xsd2json:run(.//xs:schema, map { } )
'

(I also tried using the variant xsd2json:run(fn:doc('myschema.xsd')//xs:schema) as per your comment ... it makes no difference.)

I'm having problems that seem to be related to the combination of using a simpleType that is a restriction of xs:int in an element, and referring to that element in another element. For example, if I define the simpleType, but don't use it:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
    targetNamespace="http://vocabs.ands.org.au/registry/schema/2017/01/vocabulary"
    xmlns="http://vocabs.ands.org.au/registry/schema/2017/01/vocabulary"
    xmlns:common="http://vocabs.ands.org.au/registry/schema/2017/01/common">
    <xs:simpleType name="id-type">
        <xs:restriction base="xs:int">
            <xs:minInclusive value="1"/>
        </xs:restriction>
    </xs:simpleType>
    <xs:element name="owned-vocabulary">
        <xs:complexType>
            <xs:attribute name="id" type="xs:int"/>
            <xs:attribute name="title" use="required" type="xs:token"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="owned-vocabulary-list">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded" minOccurs="0" ref="owned-vocabulary"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

the script works, and produces this output:

Prefix loaded: "common"
Prefix loaded: ""
Prefix loaded: "xs"
Prefix loaded: "xml"
{
  "definitions": {
    "id-type": {
      "maximum": 2147483647,
      "exclusiveMaximum": false,
      "xsdType": "int",
      "xsdMapping": "restrictive",
      "minimum": 1,
      "exclusiveMinimum": false,
      "type": "integer"
    }
  },
  "$schema": "http:\/\/json-schema.org\/draft-04\/schema#",
  "version": "0.0.1",
  "type": "object",
  "id": "output.json#",
  "oneOf": [
    {
      "properties": {
        "owned-vocabulary": {
          "properties": {
            "@title": {
              "xsdType": "token",
              "xsdMapping": "restrictive",
              "isAttribute": true,
              "type": "string"
            },
            "@id": {
              "maximum": 2147483647,
              "exclusiveMaximum": false,
              "xsdType": "int",
              "xsdMapping": "restrictive",
              "minimum": -2147483648,
              "exclusiveMinimum": false,
              "isAttribute": true,
              "type": "integer"
            }
          },
          "additionalProperties": false,
          "type": "object"
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "owned-vocabulary"
      ]
    },
    {
      "properties": {
        "owned-vocabulary-list": {
          "properties": {
            "owned-vocabulary": {
              "items": {
                "properties": {
                  "@title": {
                    "xsdType": "token",
                    "xsdMapping": "restrictive",
                    "isAttribute": true,
                    "type": "string"
                  },
                  "@id": {
                    "maximum": 2147483647,
                    "exclusiveMaximum": false,
                    "xsdType": "int",
                    "xsdMapping": "restrictive",
                    "minimum": -2147483648,
                    "exclusiveMinimum": false,
                    "isAttribute": true,
                    "type": "integer"
                  }
                },
                "additionalProperties": false,
                "type": "object"
              },
              "minItems": 0,
              "type": "array"
            }
          },
          "additionalProperties": false,
          "type": "object"
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "owned-vocabulary-list"
      ]
    }
  ]
}

Or, if I use the simpleType as the type of the attribute id of owned-vocabulary, but don't define owned-vocabulary-list:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
    targetNamespace="http://vocabs.ands.org.au/registry/schema/2017/01/vocabulary"
    xmlns="http://vocabs.ands.org.au/registry/schema/2017/01/vocabulary"
    xmlns:common="http://vocabs.ands.org.au/registry/schema/2017/01/common">
    <xs:simpleType name="id-type">
        <xs:restriction base="xs:int">
            <xs:minInclusive value="1"/>
        </xs:restriction>
    </xs:simpleType>
    <xs:element name="owned-vocabulary">
        <xs:complexType>
            <xs:attribute name="id" type="id-type"/>
            <xs:attribute name="title" use="required" type="xs:token"/>
        </xs:complexType>
    </xs:element>
</xs:schema>

the script works, and produces this output:

Prefix loaded: "common"
Prefix loaded: ""
Prefix loaded: "xs"
Prefix loaded: "xml"
{
  "definitions": {
    "id-type": {
      "maximum": 2147483647,
      "exclusiveMaximum": false,
      "xsdType": "int",
      "xsdMapping": "restrictive",
      "minimum": 1,
      "exclusiveMinimum": false,
      "type": "integer"
    }
  },
  "$schema": "http:\/\/json-schema.org\/draft-04\/schema#",
  "properties": {
    "owned-vocabulary": {
      "properties": {
        "@title": {
          "xsdType": "token",
          "xsdMapping": "restrictive",
          "isAttribute": true,
          "type": "string"
        },
        "@id": {
          "maximum": 2147483647,
          "exclusiveMaximum": false,
          "xsdType": "int",
          "xsdMapping": "restrictive",
          "minimum": 1,
          "exclusiveMinimum": false,
          "isAttribute": true,
          "type": "integer"
        }
      },
      "additionalProperties": false,
      "type": "object"
    }
  },
  "version": "0.0.1",
  "additionalProperties": false,
  "type": "object",
  "id": "output.json#",
  "required": [
    "owned-vocabulary"
  ]
}

But if use my simpleType as the type of the attribute id of owned-vocabulary and define owned-vocabulary-list:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
    targetNamespace="http://vocabs.ands.org.au/registry/schema/2017/01/vocabulary"
    xmlns="http://vocabs.ands.org.au/registry/schema/2017/01/vocabulary"
    xmlns:common="http://vocabs.ands.org.au/registry/schema/2017/01/common">
    <xs:simpleType name="id-type">
        <xs:restriction base="xs:int">
            <xs:minInclusive value="1"/>
        </xs:restriction>
    </xs:simpleType>
    <xs:element name="owned-vocabulary">
        <xs:complexType>
            <xs:attribute name="id" type="id-type"/>
            <xs:attribute name="title" use="required" type="xs:token"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="owned-vocabulary-list">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded" minOccurs="0" ref="owned-vocabulary"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

the script fails, with this output:

Prefix loaded: "common"
Prefix loaded: ""
Prefix loaded: "xs"
Prefix loaded: "xml"
nullStopped at /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 191/18:
[XPTY0004] Cannot convert empty-sequence() to node(): ().

Stack Trace:
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 1315/50
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 410/65
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 469/41
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 2018/58
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 2081/37
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 412/57
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 469/41
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 2018/58
- /Users/rwalker/Documents/2018/xsd2json/xsd2json.xqy, 152/65
- ., 12/13

I don't have Saxon PE (or other XQuery processor) in order to see if this is a problem with BaseX.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment