Last active
May 29, 2019 14:00
-
-
Save lcahlander/9111ce9330f80458dce6eded9b40ebbf to your computer and use it in GitHub Desktop.
This library module walks an XML Schema and generates the stubs for a typeswitch transform to process a document from that schema.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(: | |
Copyright (c) 2017. EasyMetaHub, LLC | |
This work is licensed under a | |
<a rel="license" href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. | |
:) | |
xquery version "3.1"; | |
(:~ | |
This library module walks an XML Schema and generates the stubs for a typeswitch | |
transform to process a document from that schema. | |
Here is an example where the transform is running against the XML Schema's XSD. | |
xquery version "3.1"; | |
import module namespace tsgen="http://easymetahub.com/ns/tsgen" at "tsgen.xqm"; | |
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization"; | |
declare option output:method "text"; | |
tsgen:schema(fn:doc('https://www.w3.org/2001/XMLSchema.xsd')/xs:schema, map { | |
'modulePrefix': 'xsd2json', | |
'moduleNamespace': 'http://easymetahub.com/ns/xsd2json', | |
'schemaPrefix': 'xs', | |
'schemaNamespace': 'http://www.w3.org/2001/XMLSchema' | |
}) | |
:) | |
module namespace tsgen="http://easymetahub.com/ns/tsgen"; | |
declare namespace map="http://www.w3.org/2005/xpath-functions/map"; | |
declare namespace xs="http://www.w3.org/2001/XMLSchema"; | |
declare function tsgen:process-node($node as node()?, $model as map(*)) as map(*) { | |
if ($node) then | |
typeswitch($node) | |
case element(xs:attribute) return tsgen:attribute($node, $model) | |
case element(xs:attributeGroup) return tsgen:attributeGroup($node, $model) | |
case element(xs:complexType) return tsgen:complexType($node, $model) | |
case element(xs:element) return tsgen:element($node, $model) | |
case element(xs:extension) return tsgen:extension($node, $model) | |
case element(xs:import) return tsgen:import($node, $model) | |
case element(xs:include) return tsgen:include($node, $model) | |
(: | |
case element(xs:schema) return tsgen:schema($node, $model) | |
:) | |
default return tsgen:recurse($node, $model) | |
else $model | |
}; | |
declare function tsgen:recurse($node as node()?, $model as map(*)) as map(*) { | |
if ($node) | |
then map:merge(for $cnode in $node/node() return tsgen:process-node($cnode, $model)) | |
else $model | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:attribute($node as node(), $model as map(*)) as map(*) { | |
(: Attributes: | |
Child Elements: | |
annotation | |
simpleType | |
:) | |
map:merge(($model, map:entry('attributes', (map:get($model, 'attributes'), $node/@name/string())))) | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:attributeGroup($node as node(), $model as map(*)) as map(*) { | |
(: Attributes: | |
Child Elements: | |
annotation | |
:) | |
map:merge(($model, | |
if ($node/@ref) | |
then | |
let $name := $node/@ref/string() | |
let $doc := map:get($model, 'schema') | |
let $refGroup := $doc//attributeGroup[@name/string() eq $name] | |
return if ($refGroup) then tsgen:attributeGroup($refGroup, $model) else $model | |
else tsgen:recurse($node, $model) | |
))}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:complexType($node as node()?, $model as map(*)) as map(*) { | |
(: Attributes: | |
Child Elements: | |
annotation | |
:) | |
tsgen:recurse($node, $model) | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:element($node as node(), $model as map(*)) as map(*) { | |
(: Attributes: | |
Child Elements: | |
annotation | |
complexType | |
simpleType | |
:) | |
if ($node/@name) | |
then map:merge(($model, map:entry('elements', (map:get($model, 'elements'), $node/@name/string())))) | |
else map:merge(($model, map:entry('elements', (map:get($model, 'elements'), $node/@ref/string())))) | |
}; | |
declare function tsgen:formatted-list($string-list as xs:string*) | |
{ | |
let $nl := " " | |
let $indented-list := for $string-item in distinct-values($string-list) | |
order by $string-item | |
return concat(': ', $string-item) | |
return string-join($indented-list, $nl) | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:element-function($node as node(), $model as map(*)) as xs:string { | |
(: Attributes: | |
Child Elements: | |
annotation | |
complexType | |
simpleType | |
:) | |
let $nl := " " | |
let $name := $node/@name/string() | |
let $model := map:merge(($model, tsgen:recurse($node, $model))) | |
let $attribute-names := map:get($model, 'attributes') | |
let $child-names := map:get($model, 'elements') | |
let $subelements := concat("(: Attributes:", $nl, tsgen:formatted-list($attribute-names), $nl, | |
" : Child Elements:", $nl, tsgen:formatted-list($child-names), $nl, | |
" :)", $nl) | |
let $xqdoc := string-join(("(:~", $nl, | |
for $doc in $node//xs:annotation/xs:documentation return (xs:string($doc), $nl, $nl), | |
" : @author Auto generated", $nl, | |
" : @version 1.0", $nl, | |
" : @param $node the current node being processed" , $nl, | |
" : @param $model a map(*) used for passing additional information between the levels", $nl, | |
" :)", $nl), "") | |
return concat($xqdoc, 'declare function ', map:get($model, 'modulePrefix'), ':', $name, '($node as node(), $model as map(*)) {', $nl, ' ', $subelements, ' ', map:get($model, 'modulePrefix'), ':recurse($node, $model)', $nl, '};', $nl) | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:extension($node as node(), $model as map(*)) as map(*) { | |
(: Attributes: | |
Child Elements: | |
annotation | |
:) | |
let $name := $node/@base/string() | |
let $doc := map:get($model, 'schema') | |
return tsgen:complexType($doc//complexType[@name/string() eq $name], $model) | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:import($node as node(), $model as map(*)) as map(*) { | |
(: Attributes: | |
namespace | |
schemaLocation | |
Child Elements: | |
:) | |
$model | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:include($node as node(), $model as map(*)) as map(*) { | |
(: Attributes: | |
schemaLocation | |
Child Elements: | |
:) | |
$model | |
}; | |
(:~ | |
: @author Auto generated | |
: @version 1.0 | |
: @param $node the current node being processed | |
: @param $model a map(*) used for passing additional information between the levels | |
:) | |
declare function tsgen:schema($node as node(), $model as map(*)) { | |
(: Attributes: | |
attributeFormDefault | |
blockDefault | |
elementFormDefault | |
finalDefault | |
id | |
lang | |
targetNamespace | |
version | |
Child Elements: | |
annotation | |
import | |
include | |
redefine | |
:) | |
let $nl := " " | |
let $doc := $node | |
let $names := distinct-values($doc//xs:element/@name/string()) | |
let $cnames := string-join(for $cname in $names | |
order by $cname | |
return concat(' case element(', map:get($model, 'schemaPrefix'), ':', $cname, ') return ', map:get($model, 'modulePrefix'), ':', $cname, '($node, $model)', $nl)) | |
let $main := concat('xquery version "3.1";', $nl, $nl, | |
'module namespace ', map:get($model, 'modulePrefix'), '="', map:get($model, 'moduleNamespace') ,'";', $nl, $nl, | |
'declare namespace ', map:get($model, 'schemaPrefix'), '="', map:get($model, 'schemaNamespace'), '";', $nl, $nl, | |
'declare namespace map="http://www.w3.org/2005/xpath-functions/map";', $nl, $nl, | |
'declare function ', map:get($model, 'modulePrefix'), ':process-node($node as node()?, $model as map(*)) {', $nl, | |
' if ($node) then ', $nl, | |
' typeswitch($node) ', $nl, | |
' case text() return $node ', $nl, $cnames, | |
' default return ', map:get($model, 'modulePrefix'), ':recurse($node, $model) ', $nl, | |
' else () ', $nl, | |
'};', $nl, $nl, | |
'declare function ', map:get($model, 'modulePrefix'), ':recurse($node as node()?, $model as map(*)) as item()* {', $nl, | |
' if ($node) then for $cnode in $node/node() return ', map:get($model, 'modulePrefix'), ':process-node($cnode, $model) else ()', $nl, | |
'};', $nl, $nl | |
) | |
let $new-model := map:merge(($model, map:entry('schema', $node))) | |
let $functions := for $element in $doc//xs:element[@name] | |
let $name := $element/@name/string() | |
where $element/node() | |
order by $name | |
return tsgen:element-function($element, $model) | |
return concat($main, string-join($functions, $nl)) | |
}; | |
declare function tsgen:walk-schemas($doc as node()?, $depth as xs:integer) as node()* | |
{ | |
if ($doc) then | |
let $schema := $doc//xs:schema | |
let $includes := $schema//xs:include | |
let $imports := $schema//xs:import | |
let $prefixes := in-scope-prefixes($schema) | |
let $namespace := $schema/@targetNamespace/string() | |
return for $prefix in $prefixes | |
let $namespaceURI := namespace-uri-for-prefix($prefix, $schema) | |
let $import := $imports[@namespace/string() = $namespaceURI] | |
let $schemaLoc := $schema//xs:import[@namespace/string() = $namespaceURI]/@schemaLocation/string() | |
return if (exists($prefix) and string-length($prefix) > 0) | |
then (<schemaEntry> | |
<modulePrefix/> | |
<moduleNamespace/> | |
<schemaPrefix>{$prefix}</schemaPrefix> | |
<schemaNamespace>{$namespaceURI}</schemaNamespace> | |
<schemaLocation>{resolve-uri($schemaLoc, base-uri($doc))}</schemaLocation> | |
</schemaEntry>, | |
tsgen:walk-schemas(doc($schemaLoc), $depth + 1)) | |
else if ($depth = 0) | |
then (<schemaEntry> | |
<modulePrefix/> | |
<moduleNamespace/> | |
<schemaPrefix>{$prefix}</schemaPrefix> | |
<schemaNamespace>{$namespaceURI}</schemaNamespace> | |
<schemaLocation>{resolve-uri($schemaLoc, base-uri($doc))}</schemaLocation> | |
</schemaEntry>, | |
tsgen:walk-schemas(doc($schemaLoc), $depth + 1)) | |
else () | |
else () | |
}; | |
declare function tsgen:preload($schemaLocation as xs:string) as node() | |
{ | |
let $doc := doc($schemaLocation) | |
let $schemaEntries := for $schemaEntry in tsgen:walk-schemas($doc, 0) | |
order by $schemaEntry/schemaNamespace/text() | |
return $schemaEntry | |
let $namespaces := distinct-values($schemaEntries/schemaNamespace/text()) | |
return <schemas>{for $namespace in $namespaces | |
order by $namespace | |
return $schemaEntries[schemaNamespace/text() = $namespace][1]}</schemas> | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This code will generate an XQuery typeswitch transform stubs. https://en.wikibooks.org/wiki/XQuery/Typeswitch_Transformations