使用 XPath 从参数映射构建 URL 查询字符串

Building a URL query string from a map of parameters with XPath

在 XSLT/XPath 3.0 中从 { 'param': 'value' } 映射构建 URL 查询字符串的最可读方法是什么?

以下函数将起作用:

declare function local:build-uri($base-uri as xs:string, $params as map(xs:string, xs:string)) as xs:string {
  if (map:size($params) ne 0) then
    let $param-string := string-join(
        map:keys($params)[. ne ""] ! (encode-for-uri(.) || "=" || encode-for-uri($params?(.))),
        "&"
      )
    return $base-uri || "?" || $param-string
  else
    $base-uri
};

例如:

declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare variable $params := map {
  "one": "1",
  "two": "2",
  "three": "3",
  "four": "4"
};

local:build-uri("http://www.example.com", map{}),
local:build-uri("http://www.example.com", $params),
local:build-uri("", $params),
()

returns:

http://www.example.com
http://www.example.com?four=4&one=1&two=2&three=3
?four=4&one=1&two=2&three=3

编辑: 为了支持多值参数(同时保持函数体与 XPath 兼容),像这样的东西应该可以工作:

declare function local:build-uri(
  $base-uri as xs:string,
  $params as map(xs:string, xs:string*),
  $use-array-for-multivalue-params as xs:boolean (: param[]=value for PHP, etc. :)
) as xs:string {
  if (map:size($params) ne 0) then
    let $param-strings :=
      for $param in map:keys($params)[. ne '']
      return $params?($param) ! string-join((
        encode-for-uri($param),
        if ($use-array-for-multivalue-params and count($params?($param)) gt 1) then "[]" else "",
        "=",
        encode-for-uri(.)
      ), "")
    return $base-uri || "?" || string-join($param-strings, "&")
  else
    $base-uri
};

不短,也不一定好懂。

但是

  • 它处理空值(使用 csv 可以得到 key= 另一个完全省略密钥)
  • 它处理 xs:anyAtomicType (xs:dateTime, xs:decimal, xs:boolean, ...)
  • 添加第三种常用方法来序列化具有多个值的查询字符串参数,并用逗号分隔
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": () })

这里是一个要点,将上面的内容拆分成一个模块并进行测试:

https://gist.github.com/line-o/e492401494a4e003bb01b7a2f884b027

编辑:减少代码重复

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')
      }
   )

结果:?flavour=chocolate&flavour=strawberry&size=large

一个更简洁的版本,不同之处还在于它将空映射转换为零长度字符串而不是空字符串序列:

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'
      }
   )

结果?direction=north&foo=bar&foo=baz