将 URI 查询转换为 JSON
这篇文章将提供更通用(规范)的方法来解决从 URI 字符串中提取变量的问题。
查询是跨多个描述性标准(RFC 和规范)定义的,因此如果采用规范方法,我们需要使用规范来创建查询的规范化形式,然后才能构建对象。
TL;博士
为了确保我们能够实现规范以适应未来的扩展,将查询转换为 JSON 的算法应该分步进行,每一步都逐步构建查询的规范化形式,然后才能将其转换为 JSON目的。为此,我们需要以下步骤:
- 从 URI 中提取查询
- 拆分为
key=value
- 规范化
key(构建对象层次结构)
- 规范化
value(填充对象属性并构建属性数组)
- 基于规范化构建 JSON 对象
key=value
步骤的这种分离将允许更容易地采用规范中的未来更改。可以使用 RegEx 或解析器(BNF、PEG 等)对值进行解析。
转换步骤
首先要做的是从 URI 中提取查询字符串。这在RFC3986中进行了描述,并将在其自己的部分提取查询字符串中进行解释。正如我们稍后将看到的,查询段的提取可以使用 RegEx 轻松完成。
从 URI 中提取查询字符串后,需要解释查询所传达的信息。正如我们将在下面看到的,查询在 RFC3986 中有一个非常松散的定义,查询传递变量的情况在 RFC6570 中进一步阐述。在提取过程中,算法应提取值(形式为.key=value
在变量被分离并以 的形式放置之后key=value,下一阶段是对 进行归一化key。对 的正确解释key将允许我们从结构中构建 JSON 对象的层次key=value结构。RFC6570没有提供太多关于如何规范化key的信息,但是OpenAPI 规范提供了如何处理不同类型的key. 规范化将在规范化密钥部分中进一步阐述
接下来,我们需要通过继续构建在多个级别定义变量类型的RFC6570来规范化变量。这将在规范化值部分中进一步阐述
最后阶段是使用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。
以下是设置规则和转换的良好起点
- 平键 (
key-> key)
- 属性键 (
key.atr-> key.atr)
- 数组键 (
key[]-> key[0])
- 对象数组键 (
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 对象,但是由于格式的多种变化以及未来扩展的能力,这种方法将不太灵活。
有用的链接