2

我正在尝试以 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')属于一起的参数并保存以查询正确的预期 json 对象。有问题的线路:

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

2 回答 2

2

将 URI 查询转换为 JSON

这篇文章将提供更通用(规范)的方法来解决从 URI 字符串中提取变量的问题。

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

TL;博士

为了确保我们能够实现规范以适应未来的扩展,将查询转换为 JSON 的算法应该分步进行,每一步都逐步构建查询的规范化形式,然后才能将其转换为 JSON目的。为此,我们需要以下步骤:

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

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

转换步骤

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

  2. 从 URI 中提取查询字符串后,需要解释查询所传达的信息。正如我们将在下面看到的,查询在 RFC3986 中有一个非常松散的定义,查询传递变量的情况在 RFC6570 中进一步阐述在提取过程中,算法应提取值(形式为.key=value

  3. 在变量被分离并以 的形式放置之后key=value,下一阶段是对 进行归一化key。对 的正确解释key将允许我们从结构中构建 JSON 对象的层次key=value结构。RFC6570没有提供太多关于如何规范化key的信息,但是OpenAPI 规范提供了如何处理不同类型的key. 规范化将在规范化密钥部分中进一步阐述

  4. 接下来,我们需要通过继续构建在多个级别定义变量类型的RFC6570来规范化变量。这将在规范化值部分中进一步阐述

  5. 最后阶段是使用cJSON_AddItemToObject(query, name, cJSON_CreateString(value));. 更多细节将在构建 JSON 对象部分讨论。

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

提取查询字符串

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

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

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

... 查询组件由第一个问号 ("?") 字符指示,并以数字符号 ("#") 字符或 URI 的结尾结束。...

query段的正式语法定义为:

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

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

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

...但是,由于查询组件通常用于以“key=value”对的形式携带识别信息...

此外,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 所说

... URI 模板既提供了 URI 空间的结构描述,又提供了在提供变量值时如何构造与这些值对应的 URI 的机器可读指令。...

从 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 规范JSON API 规范)等可以帮助解释,但它们没有提供完整的规则集,而是提供了一个子集。为了使事情顺利进行,一些 SDK(例如 PHP SDK)有自己的构建密钥的规则。

在这种情况下,最好的方法是为密钥规范化创建一个分层规则,将密钥转换为统一格式,类似于json 路径点表示法。分层规则将允许我们控制模棱两可的情况(在规范之间发生冲突的情况下),但控制规则的顺序。json 路径表示法将允许我们在最后一步构建对象,而无需具有正确的key=value配对顺序。

以下是规范化格式的语法:

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

此语法将允许使用诸如foo, foo.baz, foo[0].baz,foo.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 )

该语法将允许我们将值采用a, a,b,a,b,c等形式。

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

构建 JSON 对象

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

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

替代方法

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

有用的链接

于 2021-03-08T08:57:57.747 回答
1

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

不必过多担心相关类型的保存/推断的准确性,或者将其形式化/概括为重量级解决方案,因为 1)您可能遇到的语法任意性(不一定符合任何标准,Web 服务器可以真正做他们想做的任何事情,因此查询参数通常不符合引用的 swagger 标准)和 2)查看查询参数只会给你提供这么多信息——实现任何东西的好处/价值不只是模糊的近似值(如前所述,根据您自己定义的规则)很难被发现。想想即使是最简单的情况,它们是多么模糊:你必须假装在 x=something&x=something 爆炸的情况下,数组必须至少有两个元素。如果只有一个元素——x=something——你把它当作一个字符串,你怎么知道它是一个数组还是一个字符串?x=1 情况如何,1 是字符串还是数字,原始/预期类型?另外, x=foo&y=1 | 怎么样?2 | 3? 或者当您看到带有空格的“1、2、3”时?是否应该忽略空格,它们是数组分隔符本身,还是它们实际上是数组元素的一部分。最后,你怎么知道预期的字符串不是“1 | 2 | 3”本身,这意味着它不是一个数组!它们是数组分隔符本身,还是它们实际上是数组元素的一部分。最后,你怎么知道预期的字符串不是“1 | 2 | 3”本身,这意味着它不是一个数组!它们是数组分隔符本身,还是它们实际上是数组元素的一部分。最后,你怎么知道预期的字符串不是“1 | 2 | 3”本身,这意味着它不是一个数组!

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

于 2021-03-11T00:01:59.793 回答