将 URI 查询转换为 JSON
这篇文章将提供更通用(规范)的方法来解决从 URI 字符串中提取变量的问题。
查询是跨多个描述性标准(RFC 和规范)定义的,因此如果采用规范方法,我们需要使用规范创建查询的规范化形式,然后才能构建对象。
TL;DR
为了确保我们能够实现规范并能够满足未来扩展的需求,将查询转换为 JSON 的算法应该分步进行,每一步逐步构建查询的规范化形式,然后才能实现转换为 JSON 对象。为此,我们需要以下步骤:
- 从 URI 中提取查询
- 拆分为
key=value
- 规范化
key(构建对象层次结构)
- 规范化
value(填充对象属性并构建属性数组)
- 基于规范化的
key=value 构建 JSON 对象
这样的步骤分离将允许更容易地采用规范中的未来更改。可以使用 RegEx 或解析器(BNF、PEG 等)对值进行解析。
转换步骤
-
首先要做的是从 URI 中提取查询字符串。这在RFC3986 中进行了描述,并将在其自己的提取查询字符串部分中进行解释。正如我们稍后将看到的,查询段的提取可以通过 RegEx 轻松完成。
-
从URI中提取查询字符串后,需要对查询所传达的信息进行解释。正如我们将在下面看到的,查询在RFC3986 中有一个非常松散的定义,查询传递变量的情况在RFC6570 中进一步阐述。在提取过程中,算法应该提取值(以key=value 的形式)并将它们存储在映射结构中(一种方法是使用严格,如下面的SO post 所述。解释查询字符串提供了该过程的概述。
-
将变量分离并以key=value的形式放置后,下一阶段是对key进行规范化。对key 的正确解释将允许我们从key=value 结构构建JSON 对象的层次结构。 RFC6570 没有提供太多关于如何规范化key 的信息,但是OpenAPI specification 提供了如何处理不同类型的key 的良好信息。规范化将在规范化密钥
部分中进一步阐述
-
接下来我们需要通过继续构建RFC6570 来规范化变量,该RFC6570 定义了多个级别的变量类型。这将在规范化值
部分中进一步阐述
-
最后阶段是使用cJSON_AddItemToObject(query, name, cJSON_CreateString(value)); 构建 JSON 对象。更多细节将在构建 JSON 对象部分讨论。
在实施过程中,可以将部分步骤合并为一个步骤以优化实施。
提取查询字符串
RFC3986 是管理 URI 的主要描述性标准,它将 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 = "!" / "$" / "'" / "(" / ")"
/ "*" / "+" / ","
可以使用实现上述语法的解析器执行提取,或者使用以下正则表达式迭代查询并提取 (key, value) 对。
([\&](([^\&]*)\=([^\&]*)))
如果我们使用 RegEx,请注意在上一节中我们省略了“?”在查询的开头和结尾的“#”,所以我们不需要在变量的分隔中处理这些字符。
规范化密钥
描述性标准RFC6570 提供了密钥格式的通用规则,当涉及到构造对象时的密钥解释规则时,RFC 并没有多大帮助。一些规范,如OpenAPI specification、JSON API Specification) 等可以帮助解释,但它们没有提供完整的规则集,而是一个子集。为了使事情顺利进行,一些 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 )
此语法将允许使用 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 对象,但是由于格式的多种变化以及未来扩展的能力,这种方法不太灵活。
有用的链接