查询参数保留为 json

query parameter preserve as json

我正在尝试以 JSON 格式存储 API 请求查询参数,以一种保留参数值的推断原始类型的方式。我在事先不知道这些 API 是什么样子的情况下这样做的。 下面的代码一一处理每个查询参数(由 & 分隔)。

    for (int i = 0; i < url_arg_cnt; i++) {
        const http_arg_t *arg = http_get_arg(http_info, i);
        if (cJSON_GetObjectItem(query, arg->name.p) == NULL) {
            // Currently just treating as a string.
            cJSON_AddItemToObject(query, arg->name.p, cJSON_CreateString(arg->value.p));
            SLOG_INFO("name:value is %s:%s\n", arg->name.p, arg->value.p);
        } else {
            //duplicate key.
        }

用上面的代码,对于输入

?start=0&count=2&format=policyid|second&id%5Bkey1%5D=1&id[key2]=2&object=%7Bone:1,two:2%7D&nested[][foo]=1&nested[][bar]=2

我得到这些照片:

name:value is start:0
name:value is count:2
name:value is format:policyid|second
name:value is id[key1]:1
name:value is id[key2]:2
name:value is object:{one:1, two:2}
name:value is nested[][foo]:1
name:value is nested[][bar]:2

根据这份文件和我研究过的其他地方, https://swagger.io/docs/specification/serialization/

对于如何传递查询参数没有达成共识,因此不能保证我在这里会遇到什么。所以我的目标是支持尽可能多的变体。 这些可能性似乎是最常见的:

数组:

?x = 1,2,3

?x=1&x=2&x=3

?x=1%202%203

?x=1|2|3

?x[]=1&x[]=2

字符串:

?x=1

对象,可以嵌套:

?x[key1]=1&x[key2]=2

?x=%7Bkey1:1,key2:2%7D

?x[][foo]=1&x[][bar]=2

?fields[articles]=title,body&fields[people]=name

?x[0][foo]=bar&x[1][bar]=baz

关于如何最好地解决这个问题有什么想法吗?基本上对于这些查询参数,我想聚合('exploded')属于一起的参数并保存到 query 适当的预期 json 对象。有问题的行:

cJSON_AddItemToObject(query, arg->name.p, cJSON_CreateString(arg->value.p));

正在将 URI 查询转换为 JSON

此 post 将针对从 URI 字符串中提取变量的问题提供更通用(规范)的方法。

查询是跨多个描述性标准(RFC 和规范)定义的,因此如果有规范的方法,我们需要使用规范来创建查询的规范化形式,然后才能构建对象。

TL;DR

为了确保我们能够实施能够满足未来扩展的规范,将查询转换为 JSON 的算法应该分步进行,每个步骤逐渐构建规范化形式query,才可以转换成JSON对象。为此,我们需要执行以下步骤:

  • 从 URI 中提取查询
  • 拆分为 key=value
  • 规范化 key(构建对象层次结构)
  • 规范化 value(填充对象属性并构建属性数组)
  • 基于规范化key=value构建JSON对象

这样的步骤分离将允许更容易地采用规范中的未来更改。值的解析可以使用 RegEx 或解析器(BNF、PEG 等)完成。

转换步骤

  1. 首先要做的是从 URI 中提取查询字符串。这在 RFC3986 中进行了描述,并将在其自己的部分 提取查询字符串 中进行解释。查询段的提取,正如我们稍后将看到的,可以使用 RegEx 轻松完成。

  2. 从URI中提取查询字符串后,需要解释查询所传达的信息。正如我们将在下面看到的,查询在 RFC3986 中有一个非常宽松的定义,而查询传递变量的情况在 RFC6570 中进一步阐述。在提取过程中,算法应提取值(以 key=value 的形式)并将它们存储在映射结构中(一种方法是使用严格的,如以下 SO post 解释查询字符串 部分提供了该过程的概述。

  3. 变量被分离并以key=value的形式放置后,下一步是对key进行归一化。正确解释 key 将使我们能够从 key=value 结构构建 JSON 对象的层次结构。 RFC6570 is not providing much information on how the key can be normalized, however the OpenAPI specification 提供了如何处理不同类型的 key 的很好的信息。规范化将在规范化密钥

    部分进一步阐述
  4. 接下来我们需要通过继续在 RFC6570 的基础上对变量进行规范化,RFC6570 定义了多个级别的变量类型。这将在标准化值

    部分进一步阐述
  5. 最后阶段是用 cJSON_AddItemToObject(query, name, cJSON_CreateString(value)); 构建 JSON 对象。更多详细信息将在 构建 JSON 对象 部分中讨论。

在实施过程中,可以将一些步骤合并为一个步骤以优化实施。

提取查询字符串

作为管理 URI 的主要描述标准的 RFC3986 将 URI 定义为:

URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]

query部分在RFC的3.4节中定义为URI的段如:

... The query component is indicated by the first question mark ("?") character and terminated by a number sign ("#") character or by the end of the URI. ...

query段的正式语法定义为:

query         = *( pchar / "/" / "?" )
pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded   = "%" HEXDIG HEXDIG
sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                 / "*" / "+" / "," / ";" / "="

这意味着在满足 # 之前 query 可以包含更多 ?/ 的实例。实际上,只要第一个出现?之后的字符在没有特殊意义的字符集中,直到遇到第一个#之前找到的所有字符都是query .

同时,这也意味着子分隔符 & 以及 ? 在查询字符串中遇到时根据此 RFC 没有特殊含义,因为只要它在 URI 中的形式和位置正确。这意味着每个实现都可以定义自己的结构。 RFC 第 3.4 章中的语言通过使用 often 而不是 always

将 space 留给其他解释来确认这种含义

... However, as query components are often used to carry identifying information in the form of "key=value" pairs ...

此外,RFC还提供了如下RegEx,可用于从URI中提取查询部分:

regex   : ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
segments:   12            3  4          5       6  7        8 9

捕获 #7 是来自 URI 的查询。

如果我们对 URI 的其余部分不感兴趣,提取查询的最简单方法是使用 RegEx 拆分 URI 并提取不包含前导的查询字符串 ? 也不是终止符 #.

此 RFC3986 使用 RFC3987 进一步扩展以涵盖国际字符,但是 RFC3986 定义的 RegEx 仍然有效

从查询字符串中提取变量

要将查询字符串分解为key=value对,我们需要对RFC6570进行逆向工程,为变量的扩展建立描述标准,构造有效的query.正如 RFC 所述

... A URI Template provides both a structural description of a URI space and, when variable values are provided, machine-readable instructions on how to construct a URI corresponding to those values. ...

从 RFC 中,我们可以为查询中的变量提取以下语法:

query         =  variable *( "&" variable )
variable      =  varname "=" varvalue

varvalue      = *( valchar / "[" / "] / "{" / "}" / "?" )

varname       =  varchar *( ["."] varchar )
varchar       =  ALPHA / DIGIT / "_" / pct-encoded

pct-encoded   = "%" HEXDIG HEXDIG
valchar       = unreserved / pct-encoded / vsub-delims / ":" / "@"
unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
vsub-delims   = "!" / "$" / "'" / "(" / ")"
                 / "*" / "+" / ","

可以使用实现上述语法的解析器执行提取,或者使用以下 RegEx 迭代查询并提取 (key, value) 对。

([\&](([^\&]*)\=([^\&]*)))

如果我们使用 RegEx,请注意在上一节中我们省略了“?”在查询的开头和末尾的“#”,所以我们不需要在变量分隔中处理这个字符。

规范化密钥

描述性标准 RFC6570 提供了密钥格式的通用规则,RFC 在构建对象时解释密钥的规则方面帮助不大。一些规范如OpenAPI规范JSONAPI规范)等。可以帮助解释,但他们没有提供完整的规则集,而是一个子集。为了使事情顺利进行,一些 SDK(例如 PHP SDK)有自己的密钥构建规则。

在这种情况下,最好的方法是为密钥规范化创建一个分层规则,将密钥转换为统一格式,类似于 json path dot notation。分层规则将使我们能够控制歧义情况(在规范之间发生冲突的情况下),但控制规则的顺序。 json 路径符号将允许我们在最后一步构建对象,而无需按 key=value 对的正确顺序。

规范化格式语法如下:

key      = sub-key *("." sub-key )
sub-key  = name [ ("[" index "]") ]
name     = *( varchar )
index    = NONZERO-DIGIT *( DIGIT )

此语法将允许 foofoo.bazfoo[0].bazfoo.baz[0]foo.bar.baz 等键

以下是规则集和转换的良好起点

  1. 平键 (key -> key)
  2. 属性键(key.atr -> key.atr
  3. 数组键(key[] -> key[0]
  4. 对象数组键(key[attribute] -> key.attribute), (key[][attribute] -> key[0].attribute), (key[attribute][] -> key.attribute[0])

可以添加更多规则来解决特殊情况。在转换过程中,算法应从最具体的规则(底层规则)传递到最通用的规则,并尝试找到完全匹配。如果找到完全匹配项,密钥将被正常形式覆盖,其余规则将被跳过。

标准化值

类似于键的规范化,在值表示列表的情况下,值也应该被规范化。我们需要将值从任意列表格式转换为 form 格式(逗号分隔列表),该格式由以下语法定义:

value        = singe-value *( "," singe-value ) 
singe-value  = *( unreserved / pct-encoded )

此语法将允许我们采用 aa,ba,b,c 等形式的值

从值字符串中提取值列表可以通过使用有效分隔符(“、”、“;”、“|”等)拆分字符串并以规范化形式生成列表来完成.

构建 JSON 对象

一旦键和值被规范化,将平面列表(映射结构)转换为 JSON 对象可以通过列表中所有键的单次传递来完成。键的规范化格式将对我们有所帮助,因为键传达了他在对象中的层次结构的全部信息,所以即使我们没有遇到一些中间属性,我们也能够构建对象。

类似地,我们可以从变量本身识别属性的值应该是平面字符串还是数组,因此在这里也不需要额外的信息来创建正确的表示。

替代方法

作为替代方法,我们可以构建一个完整的语法来创建 AST(抽象语法树),并使用该树来生成 JSON 对象,但是由于格式的多种变体和具有未来扩展的能力,这种方法将不太灵活。

有用的链接

我最近 运行 遇到了同样的问题,我将分享从这一集中获得的一些智慧。 我假设您正在 MITM 设备(网络防火墙等)上实现它。 正如问题中所指出的,对于如何传递查询参数没有达成共识。没有一个标准或一组规则来管理这一点——事实上,任何服务器都可以实现自己的语法,只要服务器代码支持该语法。最好的办法是 1) 决定支持哪些查询参数形式(尽你所能,可能尽可能多)和 2) 仅支持这些形式,将其余形式(不支持的)视为字符串值,例如您当前的代码可以。

不值得为所讨论类型的 preservation/inference 或 formalizing/generalizing 重量级解决方案的准确性担心太多,因为 1) 您可能会遇到语法的任意性 (不一定符合任何标准,网络服务器真的可以做任何他们想做的事,因此查询参数通常不符合,比如说,引用的 swagger 标准)和 2)查看查询参数只会给你这么多信息—— benefit/value 除了模糊的近似值(按照你自己定义的规则,如前所述)之外的任何实现都是很难看到的。想想即使是最简单的情况,它们也可能是多么模糊:你必须假装在 x=something&x=something 爆炸的情况下,数组必须至少有两个元素。如果只有一个元素——x=something——你把它当作一个字符串,否则你怎么知道它是一个数组还是一个字符串? x=1 的情况如何,1 是字符串还是数字,原始/预期类型?另外, x=foo&y=1 | 怎么样? 2 | 3?或者当您看到带空格的“1、2、3”时?空格是否应该被忽略,它们本身是数组定界符,还是它们实际上是数组元素的一部分。最后,你怎么知道预期的字符串本身不是“1 | 2 | 3”,这意味着它不是一个数组!

因此,在解析这些字符串并尝试支持/推断所有这些变体(不同的规则)时,最好的办法是定义自己的规则(okay/happy 的规则)并仅支持这些规则。