diff --git a/context.xqm b/context.xqm index 852aee1..dcd1f3c 100644 --- a/context.xqm +++ b/context.xqm @@ -19,11 +19,13 @@ declare variable $c:ALL_PATTERNS as xs:string := '#ALL'; : @param instance the document instance : @param schema the Schematron schema : @param phase the active phase + : @param options map of options :) declare function c:get-context( $instance as node(), $schema as element(sch:schema), - $phase as xs:string? + $phase as xs:string?, + $options as map(*)? ) as map(*) { @@ -39,20 +41,27 @@ as map(*) $instance, $namespaces, $schema/sch:ns, - map{} + map{}, + $options ) else map{} - return map{ - 'phase' : $active-phase, - 'patterns' : $active-patterns, - 'ns-decls' : $namespaces, - 'globals' : $globals, - 'instance' : $instance, - 'diagnostics' : $schema/sch:diagnostics/sch:diagnostic, - 'properties' : $schema/sch:properties/sch:property, - 'functions' : $schema/xqy:function - } + return + map:merge( + ( + $options, + map{ + 'phase' : $active-phase, + 'patterns' : $active-patterns, + 'ns-decls' : $namespaces, + 'globals' : $globals, + 'instance' : $instance, + 'diagnostics' : $schema/sch:diagnostics/sch:diagnostic, + 'properties' : $schema/sch:properties/sch:property, + 'functions' : $schema/xqy:function + } + ) + ) }; (:NAMESPACE DECLARATIONS:) @@ -140,13 +149,15 @@ as element(sch:let)* : @param instance the document instance : @param namespaces namespace declarations : @param bindings global variable bindings + : @param options map of options :) declare function c:evaluate-global-variables( $variables as element(sch:let)*, $instance as node(), $namespaces as xs:string?, $ns-elems as element(sch:ns)*, - $bindings as map(*) + $bindings as map(*), + $options as map(*)? ) as map(*) { @@ -167,7 +178,8 @@ as map(*) $instance, $prolog || '$' || $var/@name, $ns-elems, - $bindings + $bindings, + $options ) (: let $_ := trace('[5]$bindings='||serialize($binding, map{'method':'adaptive'})) :) @@ -177,7 +189,8 @@ as map(*) $instance, $namespaces, $ns-elems, - $binding + $binding, + $options ) }; @@ -189,13 +202,15 @@ as map(*) : @param variables pattern variables : @param instance the document instance : @param prolog global variable and namespace declarations + : @param options map of options :) declare function c:evaluate-root-context-variables( $variables as element(sch:let)*, $instance as node()+, $namespaces as xs:string?, $ns-elems as element(sch:ns)*, - $bindings as map(*) + $bindings as map(*), + $options as map(*)? ) as map(*) { @@ -215,7 +230,8 @@ as map(*) $instance, $prolog || utils:local-variable-decls($var) || ' return $' || $var/@name, $ns-elems, - $bindings + $bindings, + $options ) (: let $_ := trace('[5]$bindings='||serialize($binding, map{'method':'adaptive'})) :) @@ -225,7 +241,8 @@ as map(*) $instance, $namespaces, $ns-elems, - $binding + $binding, + $options ) }; @@ -236,22 +253,25 @@ as map(*) : @param query the query to evaluate : @param ns-elems namespace declarations : @param bindings map of global variable bindings + : @param options map of options :) declare function c:evaluate-global-variable( $variable as element(sch:let), $instance as node(), $query as xs:string?, $ns-elems as element(sch:ns)*, - $bindings as map(*) + $bindings as map(*), + $options as map(*)? ) as map(*) { (: let $_ := trace('>>>QUERY='||$query) :) let $value as item()* := if($variable/@value) - then xquery:eval( + then utils:eval( $query => utils:escape(), map:merge(($bindings, map{'':$instance})), - map{'pass':'true'} + map:merge($options, map{'pass':'true'}), + $variable/@value ) else $variable/* let $bindings := map:merge( @@ -283,10 +303,11 @@ as map(*) { if($documents) then - let $uris := xquery:eval( + let $uris := utils:eval( utils:make-query-prolog($context) || $documents => utils:escape(), map:merge(($context?globals, map{'':$context?instance})), - map{'pass':'true'} (:report exception details:) + map{'pass':'true'}, (:report exception details:) + $documents ) return map:put( $context, diff --git a/evaluate.xqm b/evaluate.xqm index 0ade267..087bea7 100644 --- a/evaluate.xqm +++ b/evaluate.xqm @@ -11,9 +11,40 @@ import module namespace output = 'http://www.andrewsales.com/ns/xqs-output' at import module namespace utils = 'http://www.andrewsales.com/ns/xqs-utils' at 'utils.xqm'; +declare namespace xqy = 'http://www.w3.org/2012/xquery'; declare namespace sch = "http://purl.oclc.org/dsdl/schematron"; declare namespace svrl = "http://purl.oclc.org/dsdl/svrl"; +(:~ Evaluates the schema to produce SVRL output, applying the processing options + : specified. + : @param instance the document instance + : @param schema the Schematron schema + : @param phase the active phase + : @param options map of processing options + :) +declare function eval:schema( + $instance as node(), + $schema as element(sch:schema), + $phase as xs:string?, + $options as map(*)? +) +{ + if($options?dry-run eq 'true') + then + + {output:schema-title($schema/sch:title)} + {$schema/@schemaVersion} + {output:namespace-decls-as-svrl($schema/sch:ns)} + + {$schema/xqy:function ! utils:parse-function(., $options)[self::svrl:*]} + {for $phase in ($schema/sch:phase/@id, '') + let $context as map(*) := context:get-context($instance, $schema, $phase, $options) + return eval:phase($context)} + + else + eval:schema($instance, $schema, $phase) +}; + (:~ Evaluates the schema to produce SVRL output. : @param instance the document instance : @param schema the Schematron schema @@ -25,7 +56,7 @@ declare function eval:schema( $phase as xs:string? ) { - let $context as map(*) := context:get-context($instance, $schema, $phase) + let $context as map(*) := context:get-context($instance, $schema, $phase, map{}) return @@ -57,7 +88,8 @@ declare function eval:pattern( $context?instance, $context?ns-decls, $pattern/../sch:ns, - $context?globals + $context?globals, + map{'dry-run':$context?dry-run} ) (: let $_ := trace('PATTERN $globals='||serialize($globals, map{'method':'adaptive'})) :) let $context := map:put($context, 'globals', $globals) @@ -67,19 +99,59 @@ declare function eval:pattern( (: let $_ := trace('PATTERN '||$pattern/@id||' prolog='||$prolog) :) (: let $_ := trace('PATTERN $bindings '||serialize($context?globals, map{'method':'adaptive'})) :) - return (:TODO active-pattern/@name:)( - - {$pattern/(@id, @name, @role), - if($pattern/@documents) then attribute{'documents'}{$context?instance ! base-uri(.)} else()} - , - $context?instance ! eval:rules( - $pattern/sch:rule, - utils:make-query-prolog($context), - map:put($context, 'instance', .) + let $rules := $pattern/sch:rule + return ( + if($context?dry-run eq 'true') + then + ($context?globals?*[self::svrl:*], eval:all-rules($rules, $context)) + else ( + + {$pattern/(@id, @name, @role), + if($pattern/@documents) then attribute{'documents'}{$context?instance ! base-uri(.)} else()} + , + eval:rules($rules, $context) ) ) }; +(:~ Evaluates all the rules in a pattern. + : Initially added for use in dry-run mode, to check for syntax errors. + : N.B. we don't need to map the instance each time for this purpose, since we + : are not evaluating @documents, but this approach could be used for + : evaluating sch:rule-set (see https://github.com/AndrewSales/XQS/tree/%234). + :) +declare function eval:all-rules( + $rules as element(sch:rule)*, + $context as map(*) +) +as element()* +{ + $context?instance + ! + (for $rule in $rules + return + eval:rule( + $rule, + utils:make-query-prolog($context), + map:put($context, 'instance', .) + )) +}; + +declare function eval:rules( + $rules as element(sch:rule)*, + $context as map(*) +) +as element()* +{ + $context?instance + ! + eval:rules( + $rules, + utils:make-query-prolog($context), + map:put($context, 'instance', .) + ) +}; + (:~ Evaluate rules, stopping once one fires. : (Necessitated by ISO2020 6.5.) : @param rules the rules to evaluate @@ -116,26 +188,43 @@ declare function eval:rule( as element()* { let $_ := utils:check-duplicate-variable-names($rule/sch:let) + let $variable-errors := utils:evaluate-rule-variables( + $rule/sch:let, + $prolog, + map:merge((map{'':$context?instance}, $context?globals)), + $context, + () + ) let $query := string-join( ($prolog, utils:local-variable-decls($rule/sch:let), if($rule/sch:let) then 'return ' else '', $rule/@context), ' ' ) (: let $_ := trace('[2]RULE query='||$query) :) - let $rule-context := xquery:eval( + let $rule-context := utils:eval( $query => utils:escape(), map:merge((map{'':$context?instance}, $context?globals)), - map{'pass':'true'} (:report exception details:) + map{'dry-run':$context?dry-run}, + $rule/@context ) return if($rule-context) - then( - - {$rule/(@id, @name, @context, @role, @flag), - if($rule/../@documents) then attribute{'document'}{$context?instance/base-uri()} else ()} - , - eval:assertions($rule, $prolog, $rule-context, $context) - ) + then + if($context?dry-run eq 'true') + then + ( + $variable-errors[self::svrl:*], + $rule-context[self::svrl:*], + eval:assertions($rule, $prolog, <_/>, $context) (:pass dummy context node:) + ) + else + ( + + {$rule/(@id, @name, @context, @role, @flag), + if($rule/../@documents) then attribute{'document'}{$context?instance/base-uri()} else ()} + , + eval:assertions($rule, $prolog, $rule-context, $context) + ) else () }; @@ -181,12 +270,20 @@ declare function eval:assertion( $context as map(*) ) { - let $result := xquery:eval( + let $result := utils:eval( $prolog || $assertion/@test => utils:escape(), map:merge((map{'':$rule-context}, $context?globals)), - map{'pass':'true'} + map{'dry-run':$context?dry-run}, + $assertion/@test ) return + if($context?dry-run eq 'true') + then + ( + $result[self::svrl:*], + output:assertion-message($assertion, $prolog, $rule-context, $context) + ) + else typeswitch($assertion) case element(sch:assert) return if($result) then () @@ -206,15 +303,18 @@ declare function eval:phase($context as map(*)) let $phase := $context?phase let $_ := utils:check-duplicate-variable-names($phase/sch:let) + let $dry-run as map(*) := map{'dry-run':$context?dry-run} + (:add phase variables to context:) let $globals as map(*) := context:evaluate-root-context-variables( $phase/sch:let, $context?instance, $context?ns-decls, $phase/../sch:ns, - $context?globals + $context?globals, + $dry-run ) let $context := map:put($context, 'globals', $globals) - - return $context?patterns ! eval:pattern(., $context) + + return $context?patterns ! eval:pattern(., map:merge(($context, $dry-run))) }; \ No newline at end of file diff --git a/svrl.xqm b/svrl.xqm index ce08e2a..e80912c 100644 --- a/svrl.xqm +++ b/svrl.xqm @@ -3,6 +3,9 @@ :) module namespace output = 'http://www.andrewsales.com/ns/xqs-output'; +import module namespace utils = 'http://www.andrewsales.com/ns/xqs-utils' at + 'utils.xqm'; + declare namespace sch = "http://purl.oclc.org/dsdl/schematron"; declare namespace svrl = "http://purl.oclc.org/dsdl/svrl"; @@ -30,6 +33,14 @@ declare function output:assertion-message( $context as map(*) ) { + if($context?dry-run eq 'true') + then output:assertion-message-content( + $assertion/node(), + $prolog, + $rule-context, + $context + ) + else element{ QName("http://purl.oclc.org/dsdl/svrl", if($assertion/self::sch:assert) then 'failed-assert' else 'successful-report')} @@ -106,23 +117,34 @@ declare function output:assertion-message-content( $rule-context as node(), $context as map(*) ) +as item()* { + if($context?dry-run eq 'true') + then + for $node in $content/(self::sch:name|self::sch:value-of) + return + typeswitch($node) + case element(sch:name) + return if($node/@path) + then output:name-value-of($node/@path, $prolog, $rule-context, $context) + [self::svrl:*] + else () + case element(sch:value-of) + return output:name-value-of($node/@select, $prolog, $rule-context, $context) + [self::svrl:*] + default return () + else {(:TODO attributes:) for $node in $content return typeswitch($node) case element(sch:name) return if($node/@path) - then xquery:eval( - $prolog || $node/@path, - map:merge((map{'':$rule-context}, $context?globals)) - ) + then output:name-value-of($node/@path, $prolog, $rule-context, $context) + => string() else name($rule-context) case element(sch:value-of) - return xquery:eval( - $prolog || $node/@select, - map:merge((map{'':$rule-context}, $context?globals)) - ) + return output:name-value-of($node/@select, $prolog, $rule-context, $context) => string() case element(sch:emph) return output:assertion-child-elements($node) @@ -132,4 +154,19 @@ declare function output:assertion-message-content( return output:assertion-child-elements($node) default return $node } +}; + +declare function output:name-value-of( + $attr as attribute(), + $prolog as xs:string?, + $rule-context as node(), + $context as map(*) +) +{ + utils:eval( + $prolog || $attr, + map:merge((map{'':$rule-context}, $context?globals)), + map{'dry-run':$context?dry-run}, + $attr + ) }; \ No newline at end of file diff --git a/test/global-variable-syntax-error.sch b/test/global-variable-syntax-error.sch new file mode 100644 index 0000000..3a93ad9 --- /dev/null +++ b/test/global-variable-syntax-error.sch @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/test/test-compile.xqm b/test/test-compile.xqm index af513b5..bf5e8b4 100644 --- a/test/test-compile.xqm +++ b/test/test-compile.xqm @@ -1328,6 +1328,7 @@ declare %unit:test function _:global-variable-bindings() document{}, '', (), + map{}, map{} ) return ( diff --git a/test/test-context.xqm b/test/test-context.xqm index 3f204d4..5a5c094 100644 --- a/test/test-context.xqm +++ b/test/test-context.xqm @@ -271,7 +271,8 @@ declare %unit:test function _:get-context-patterns() let $patterns := c:get-context( , $schema, - 'phase1' + 'phase1', + map{} )?patterns return unit:assert-equals( @@ -296,7 +297,8 @@ declare %unit:test function _:get-context-active-phase() let $phase := c:get-context( , $schema, - 'phase1' + 'phase1', + map{} )?phase return unit:assert-equals( @@ -322,7 +324,8 @@ declare %unit:test function _:get-context-namespaces() let $ns-decls := c:get-context( , $schema, - '' + '', + map{} )?ns-decls return unit:assert-equals( @@ -350,7 +353,8 @@ declare %unit:test function _:get-context-globals() let $globals := c:get-context( , $schema, - 'phase1' + 'phase1', + map{} )?globals return unit:assert-equals( diff --git a/test/test-evaluate.xqm b/test/test-evaluate.xqm index dc1b87e..12e20fb 100644 --- a/test/test-evaluate.xqm +++ b/test/test-evaluate.xqm @@ -6,6 +6,7 @@ module namespace _ = 'http://www.andrewsales.com/ns/xqs-evaluation-tests'; declare namespace sch = "http://purl.oclc.org/dsdl/schematron"; declare namespace svrl = "http://purl.oclc.org/dsdl/svrl"; +declare namespace xqy = 'http://www.w3.org/2012/xquery'; import module namespace eval = 'http://www.andrewsales.com/ns/xqs-evaluate' at '../evaluate.xqm'; @@ -814,6 +815,7 @@ declare %unit:test function _:global-variable-bindings() document{}, '', (), + map{}, map{} ) return ( @@ -1298,4 +1300,280 @@ declare %unit:test function _:map-rule-variable() unit:assert(empty($result/svrl:failed-assert)), unit:assert(empty($result/svrl:successful-report)) ) -}; \ No newline at end of file +}; + +declare %unit:test function _:global-variable-syntax-error() +{ + let $result := eval:schema( + document{}, + doc('global-variable-syntax-error.sch')/*, + '', + map{'dry-run':'true'} + ) + return ( + unit:assert-equals( + $result/svrl:failed-assert/@location/data(), + "/Q{http://purl.oclc.org/dsdl/schematron}schema[1]/Q{http://purl.oclc.org/dsdl/schematron}let[1]/@value" + ), + unit:assert-equals( + $result/svrl:failed-assert/@err:code/data(), + "err:XPST0003" + ), + unit:assert-equals( + $result/svrl:failed-assert/svrl:text, + No specifier after lookup operator: ';'. @value='?' + ) + ) +}; + +declare %unit:test function _:pattern-variable-syntax-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + $result/svrl:failed-assert[ends-with(@location ,'/Q{http://purl.oclc.org/dsdl/schematron}pattern[1]/Q{http://purl.oclc.org/dsdl/schematron}let[1]/@value')] + /svrl:text, + Incomplete FLWOR expression, expecting 'return'. @value='$' + ) + ) +}; + +declare %unit:test function _:rule-context-syntax-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + $result/svrl:failed-assert/svrl:text, + Expecting expression. @context='' + ) + ) +}; + +declare %unit:test function _:dry-run-all-rules-processed() +{ + let $result := + eval:schema( + document{}, + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + count($result/svrl:fired-rule), + 2 + ) + ) +}; + +declare %unit:test function _:rule-variable-syntax-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + count($result/svrl:failed-assert[ends-with(@location, '/Q{http://purl.oclc.org/dsdl/schematron}pattern[1]/Q{http://purl.oclc.org/dsdl/schematron}rule[1]/Q{http://purl.oclc.org/dsdl/schematron}let[1]/@value')]), + 1 + ), + unit:assert-equals( + $result/svrl:failed-assert[ends-with(@location, '/Q{http://purl.oclc.org/dsdl/schematron}pattern[1]/Q{http://purl.oclc.org/dsdl/schematron}rule[1]/Q{http://purl.oclc.org/dsdl/schematron}let[1]/@value')]/svrl:text, + Incomplete FLWOR expression, expecting 'return'. @value='' + ) + ) +}; + +declare %unit:test function _:report-test-syntax-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + count($result/svrl:failed-assert[ends-with(@location, '/Q{http://purl.oclc.org/dsdl/schematron}pattern[1]/Q{http://purl.oclc.org/dsdl/schematron}rule[1]/Q{http://purl.oclc.org/dsdl/schematron}report[1]/@test')]), + 1 + ), + unit:assert-equals( + $result/svrl:failed-assert/svrl:text, + Namespace prefix not declared: ns. @test='ns:*' + ) + ) +}; + +declare %unit:test function _:name-path-syntax-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + count($result/svrl:failed-assert[ends-with(@location, '/Q{http://purl.oclc.org/dsdl/schematron}pattern[1]/Q{http://purl.oclc.org/dsdl/schematron}rule[1]/Q{http://purl.oclc.org/dsdl/schematron}report[1]/Q{http://purl.oclc.org/dsdl/schematron}name[1]/@path')]), + 1 + ), + unit:assert-equals( + $result/svrl:failed-assert/svrl:text, + Unexpected end of query: '.'. @path='...' + ) + ) +}; + +declare %unit:test function _:value-of-select-syntax-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + count($result/svrl:failed-assert[ends-with(@location, '/Q{http://purl.oclc.org/dsdl/schematron}pattern[1]/Q{http://purl.oclc.org/dsdl/schematron}rule[1]/Q{http://purl.oclc.org/dsdl/schematron}report[1]/Q{http://purl.oclc.org/dsdl/schematron}value-of[1]/@select')]), + 1 + ), + unit:assert-equals( + $result/svrl:failed-assert/svrl:text, + Unexpected end of query: '.'. @select='...' + ) + ) +}; + +declare %unit:test function _:phase-variable-scope-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + + + + + + + + , + 'two', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + ($result/svrl:failed-assert)[1]/svrl:text, + Undeclared variable: $foo. @select='$foo' + ) + ) +}; + +(:TODO:) +declare %unit:ignore function _:function-syntax-error() +{ + let $result := + eval:schema( + document{}, + + + + + + + + , + '', + map{'dry-run':'true'} + ) + return + ( + unit:assert-equals( + ($result/svrl:failed-assert)[1]/svrl:text, + Calculation is incomplete. xqy:function='declare function local:foo(){{**}};' + ) + ) +}; + +(:TODO +pattern/@documents +diagnostics +properties +functions +:) \ No newline at end of file diff --git a/utils.xqm b/utils.xqm index 907717b..c82aa7a 100644 --- a/utils.xqm +++ b/utils.xqm @@ -6,6 +6,7 @@ declare namespace xqs = 'http://www.andrewsales.com/ns/xqs'; declare namespace sch = "http://purl.oclc.org/dsdl/schematron"; declare namespace svrl = "http://purl.oclc.org/dsdl/svrl"; declare namespace map = "http://www.w3.org/2005/xpath-functions/map"; +declare namespace xqy = 'http://www.w3.org/2012/xquery'; (:~ Builds the string of variable declarations in the prolog, for initial : evaluation. @@ -124,3 +125,85 @@ declare function utils:check-duplicate-variable-names($decls as element(sch:let) || $names[index-of($names, .)[2]] ) else() }; + +(:~ In dry-run mode only, evaluate rule variables. + : Provides more localized information if syntax errors are present in rule + : variable declarations. + :) +declare function utils:evaluate-rule-variables( + $variables as element(sch:let)*, + $prolog as xs:string?, + $bindings as map(*), + $context as map(*), + $errors as element()* +) +as element()* +{ + if($context?dry-run eq 'true') + then + if(exists($variables)) + then + let $var := head($variables) + let $prolog := $prolog || utils:local-variable-decls($var) + let $errs := utils:eval( + $prolog || ' return $' || $var/@name => utils:escape(), + $bindings, + map{'dry-run':$context?dry-run}, + $var/@value + ) + return utils:evaluate-rule-variables( + tail($variables), + $prolog, + $bindings, + $context, + ($errors,$errs) + ) + else $errors + else () +}; + +(:~ Wrapper around xquery:eval(). In "dry-run" mode, the query passed in is + : parsed only, and any errors caught reported as svrl:failed-assert. + : @param $query string of the query to evaluate + : @param bindings map of bindings + : @param options map of options + : @param node the schema node being evaluated + :) +declare function utils:eval( + $query as xs:string, + $bindings as map(*), + $options as map(*), + $node as node() +) as item()* +{ + if($options?dry-run eq 'true') + then + (, + try{ + xquery:parse($query, map{'pass':'true'}) + } + catch * { + + {$err:description}{' @'||$node/name()}='{$node/data()}' + }) + else xquery:eval($query, $bindings, map{'pass':'true'}) +}; + +declare function utils:parse-function( + $node as element(xqy:function), + $options as map(*) +) +as element()+ +{ + , + try{ + xquery:parse($node || 0, map{'pass':'true'}) + } + catch * { + + {$err:description}{' '||$node/name()}='{$node/data()}' + + } +}; \ No newline at end of file