What would be the most readable way to build a URL query string from a { 'param': 'value' }
map in XSLT/XPath 3.0?
Building a URL query string from a map of parameters with XPath
830 Views Asked by Martynas Jusevičius At
3
There are 3 best solutions below
0

Not short, nor necessarily easy to understand.
BUT
- it handles null values (with csv you get
key=
the other to omit the key entirely) - it handles xs:anyAtomicType (xs:dateTime, xs:decimal, xs:boolean, ...)
- adds a third, common way to serialize the query string parameters with multiple values separating the with a comma
xquery version "3.1";
declare namespace qs="http://line-o.de/ns/qs";
(:~
: Append nothing to names of parameters with multiple values
: ?single=v1&multi=v2&multi=v3
:)
declare function qs:serialize-query-string($parameters as map(xs:string, xs:anyAtomicType*)) as xs:string? {
qs:serialize(
$parameters,
qs:serialize-parameter(?, ?, ()))
};
(:~
: Append [] to names of parameters with multiple values
: ?single=v1&multi[]=v2&multi[]=v3
:)
declare function qs:serialize-query-string-array($parameters as map(xs:string, xs:anyAtomicType*)) as xs:string? {
qs:serialize(
$parameters,
qs:serialize-parameter(?, ?, '[]'))
};
(:~
: Commma separated values for parameters with multiple values
: ?single=v1&multi=v2,v3
:)
declare function qs:serialize-query-string-csv($parameters as map(xs:string, xs:anyAtomicType*)) as xs:string? {
qs:serialize(
$parameters,
qs:serialize-parameter-csv#2)
};
declare function qs:serialize(
$parameters as map(xs:string, xs:anyAtomicType*),
$serializer as function(xs:string, xs:anyAtomicType*) as xs:string*
) as xs:string? {
if (map:size($parameters) eq 0)
then ()
else
$parameters
=> map:for-each($serializer)
=> string-join('&')
=> qs:prepend-questionmark()
};
declare function qs:serialize-parameter (
$raw-parameter-name as xs:string,
$values as xs:anyAtomicType*,
$appendix as xs:string?
) as xs:string* {
let $parameter-name := concat(
encode-for-uri($raw-parameter-name),
if (exists($values) and count($values)) then $appendix else ()
)
return
for-each($values,
qs:serialize-parameter-value($parameter-name, ?))
};
declare function qs:serialize-parameter-csv ($raw-parameter-name as xs:string, $values as xs:anyAtomicType*) as xs:string* {
concat(
encode-for-uri($raw-parameter-name),
'=',
$values
=> for-each(function ($value) { encode-for-uri(xs:string($value)) })
=> string-join(',')
)
};
declare function qs:serialize-parameter-value (
$parameter as xs:string, $value as xs:anyAtomicType
) as xs:string {
``[`{$parameter}`=`{encode-for-uri($value)}`]``
};
declare function qs:prepend-questionmark ($query-string as xs:string) {
concat('?', $query-string)
};
qs:serialize-query-string(map{}),
qs:serialize-query-string-array(map{}),
qs:serialize-query-string-csv(map{}),
qs:serialize-query-string(map{ "a": ("b0","b1"), "b": "$=@#'" }),
qs:serialize-query-string-array(map{ "a": (xs:date("1970-01-01"),"b1"), "b": "$=@#'" }),
qs:serialize-query-string-csv(map{ "a": ("b0",3.14), "b": "$=@#'" }),
qs:serialize-query-string(map{ "a": ("b0","b1"), "c": () }),
qs:serialize-query-string-array(map{ "a": ("b0","b1"), "c": () }),
qs:serialize-query-string-csv(map{ "a": ("b0","b1"), "c": () })
Here is a gist with the above split into a module and tests:
https://gist.github.com/line-o/e492401494a4e003bb01b7a2f884b027
EDIT: less code duplication
0

let
$encode-parameters-for-uri:= function($parameters as map(*)) as xs:string? {
let
(: serialize each map entry :)
$encoded-parameters:= map:for-each(
$parameters,
function ($key, $values) {
(: serialize the sequence of values for this key :)
for $value in $values return
encode-for-uri($key) || '=' || encode-for-uri($value)
}
),
(: join the URI parameters with ampersands :)
$parameters-string:= string-join(
$encoded-parameters,
codepoints-to-string(38)
)
return
(: prepend '?' if parameters exist :)
if ($parameters-string) then
'?' || $parameters-string
else
()
}
return
$encode-parameters-for-uri(
map{
'size': 'large',
'flavour': ('chocolate', 'strawberry')
}
)
result: ?flavour=chocolate&flavour=strawberry&size=large
A more concise version, also differing in that it converts an empty map into a zero-length string rather than an empty sequence of strings:
let
$encode-parameters-for-uri:= function($parameters as map(*)) as xs:string {
if (map:size($parameters)) then
'?' || string-join(
map:for-each(
$parameters,
function ($key, $values) {
for $value in $values return
encode-for-uri($key) || '=' || encode-for-uri($value)
}
),
codepoints-to-string(38)
)
else
''
}
return
$encode-parameters-for-uri(
map{
'foo': ('bar', 'baz'), 'direction': 'north'
}
)
result ?direction=north&foo=bar&foo=baz
The following function will work:
For example:
returns:
Edit: To support multi-value parameters (while keeping the function body compatible with XPath), something like this should work: