跳到主要内容

2. 数据结构规范的风格

CDDL 关注的是规范编写风格, 这些风格用于采用由 JSON 开创并由 CBOR 进一步完善的数据模型的社区.

CBOR 数据模型包含若干或多或少属于原子的元素, 例如数字, 简单值 (false, true, nil), 文本字符串, 以及字节字符串; CDDL 的重点并不是规定它们的结构. 当然, CDDL 也允许向数据项添加 CBOR 标签.

除了这些原子元素之外, 数据结构定义语言的进一步组成部分是用于组合的数据类型: CBOR 中的数组和映射 (在 JSON 中称为 "arrays" 和 "objects"). 虽然它们只是两种表示格式, 但可用于规定四种大体可区分的组合风格:

  • 向量 (A vector): 由语义大多相同的元素组成的数组. 与某个已签名数据项关联的一组签名就是向量的典型应用.

  • 记录 (A record): 一种数组, 其元素具有不同的, 由位置定义的语义, 具体语义在数据结构定义中详细说明. 一个 2D 点可被规定为由 x 坐标 (位于第一位) 和 y 坐标 (位于第二位) 组成的数组, 这是记录的一个示例; CBOR 十进制分数中指数 (第一位) 和尾数 (第二位) 的二元组也是记录.

  • 表 (A table): 从映射键的域到映射值的域的映射, 这些映射值的语义大多相同. 一组语言标签就是表的一个示例, 其中每个语言标签都映射到翻译为该特定语言的文本字符串. 键域通常不会被规范限制为某个特定集合, 而是向应用开放, 例如在将 IP 地址映射到媒体访问控制 (Media Access Control, MAC) 地址的表中, 规范并不试图预见所有可能的 IP 地址. 在 JavaScript 这样的语言中, 通常会使用 "Map" (而不是普通的 "Object") 来实现键域的通用性.

  • 结构体 (A struct): 从规范所定义的映射键域到映射值域的映射, 每个映射值的语义都绑定到特定映射键. 这就是许多人想到 JSON 对象时脑中浮现的东西; CBOR 又增加了使用非文本字符串映射键的能力. 结构体可用于解决与记录类似的问题; 使用显式映射键有助于实现可选性和可扩展性.

两个重要概念为 CDDL 提供基础:

  1. CDDL 并不分别定义所有四种组合类型, 甚至也不为数组 (向量和记录) 定义一种类型, 为映射 (表和结构体) 定义另一种类型. CDDL 中只有一种组合方式: 组 (group) (Section 2.1).

  2. 另一个重要概念是 类型 (type). 整个 CDDL 规范定义了一个类型 (由其第一条 rule 定义的类型), 它在形式上是可作为该规范 "instances" 接受的 CBOR 数据项集合. CDDL 预定义了若干基本类型, 例如 "uint" (unsigned integer) 或 "tstr" (text string), 并经常使用一种用于 CBOR 数据项的简单形式记法. 每个可表达为 CBOR 数据项的值本身也是一种类型, 例如 "1". 一种类型可被构造为其他类型的 choice, 例如 "int" 要么是 "uint", 要么是 "nint" (negative integer). 最后, 类型也可从一个组构造为数组或映射.

本节其余部分介绍 CDDL 的若干基本概念, Section 3 定义其他语法. Appendix C 简要总结了 CDDL 的语义.

2.1. CDDL 中的组和组合

CDDL 组是组 entries 的列表, 每个条目可以是名称/值对, 也可以是更复杂的组表达式 (该表达式随后又代表一个名称/值对序列). CDDL 组是语法中的一个产生式, 它匹配某些名称/值对序列, 而不匹配其他序列. 该语法基于解析表达式语法 (Parsing Expression Grammars, PEGs) 的概念 (见 Appendix A).

在数组上下文中, 只表示名称/值对中的值; 名称只是注释 (如果不需要, 可从组规范中省略). 在映射上下文中, 名称会成为映射键 ("member keys").

在数组上下文中, 组中元素的实际顺序很重要, 因为该顺序提供了将实际数组元素与组中条目关联起来的信息. 在映射上下文中, 组中条目的顺序并不相关 (但仍然需要按某个序列写下组条目).

当某个组匹配一个名称/值对序列, 且该序列中值部分按顺序与数组元素完全匹配时, 该数组就匹配以该组给出的规范.

当某个组匹配一个名称/值对序列, 并且所有这些名称/值对都存在于映射中, 且该映射没有任何未由该组覆盖的名称/值对时, 该映射就匹配以该组给出的规范.

下面是一个在映射定义中直接使用组的简单示例:

person = {
age: int,
name: tstr,
employer: tstr,
}

图 1: 在映射中直接使用组

该组的三个条目写在创建映射的花括号之间: 这里, "age", "name", 和 "employer" 是会变成映射键文本字符串的名称, 而 "int" 和 "tstr" (text string) 是这些键下映射值的类型.

一个单独的组 (不在其外部创建映射) 可以放在圆括号中, 并通过在规则中使用它而被赋予名称:

pii = (
age: int,
name: tstr,
employer: tstr,
)

图 2: 基本组

这个单独的具名组定义允许我们将图 1 改写为:

person = {
pii
}

图 3: 按名称使用组

注意, 花括号表示创建一个映射; 组本身对于其将用于映射还是数组保持中立.

如图 1 所示, 当存在其他某组括号时, 组的圆括号是可选的. 注意, 它们仍然可以使用, 从而得到下面这个不太现实但完全有效的示例:

person = {(
age: int,
name: tstr,
employer: tstr,
)}

图 4: 在映射中使用带圆括号的组

组可用于提取结构体的公共部分, 例如, 不用像图 5 那样以复制/粘贴方式编写规范, 可以提取公共子组, 为它选择一个名称, 并只把各映射中的特定部分写入各自的映射 (图 6).

person = {
age: int,
name: tstr,
employer: tstr,
}

dog = {
age: int,
name: tstr,
leash-length: float,
}

图 5: 使用复制/粘贴的映射

person = {
identity,
employer: tstr,
}

dog = {
identity,
leash-length: float,
}

identity = (
age: int,
name: tstr,
)

图 6: 使用组进行因式分解

注意, 上述定义中花括号内的列表构成 (匿名) 组, 而 "identity" 是具名组, 随后可作为其他组的一部分被包含进去 (这些其他组可以像示例中那样是匿名的, 也可以自身就是具名的).

2.1.1. 用法

组是在 CDDL 中组合数据结构的工具. 在定义这些结构时, 是直接在上下文中 (匿名地) 定义组, 还是在单独规则中定义组并用相应名称引用它们 (可能不止一次), 是一种风格选择.

借助这一点, 可以定义数据结构的所有小部件, 并用这些部件组合出更大的协议数据单元; 也可以只定义一个大的协议数据单元, 并在需要处临时给出所有定义.

2.1.2. 语法

组合语法旨在简洁且易于阅读:

  • 组的开始和结束可用 "(" 和 ")" 标记.

  • 组内条目的定义按如下方式表示: keytype => valuetype, (读作 "keytype 映射到 valuetype"). 逗号实际上是可选的 (并不只是最后一个条目可省略), 但设置逗号被认为是良好风格. 在直接使用文本字符串或整数字面量作为键的常见情况下, 双箭头可由冒号替代; 见 Section 3.5.1. 这也是仅为文档目的命名数组元素的常见方式; 见 Section 3.4.

一个基本条目由 keytypevaluetype 组成, 二者都是类型 (Section 2.2); 此条目匹配任意名称/值对, 其中名称属于 keytype, 值属于 valuetype.

定义为一系列组条目的组会匹配任意名称/值对序列, 该序列由各条目所匹配内容按顺序连接而成.

组定义还可以包含组之间的选择; 见 Section 2.2.2.

2.2. 类型

2.2.1. 值

数字和字符串等值可以用于代替类型. (例如, 对键类型来说, 这是一种很常见的做法, 常见到 CDDL 为此提供了额外的便利语法.)

值记法基于 C 语言, 但并不提供所有语法变体 (详见 Appendix B). 数字的值记法继承了 C 中对整数值 (未给出小数部分或指数 -- NR1 [ISO6093]; "NR" 表示 "numerical representation") 和浮点值 (存在小数部分, 指数, 或二者同时存在 -- NR2 或 NR3) 的区分, 因此类型 "1" 不包含任何浮点数, 而类型 "1e3" 和 "1.5" 都是浮点数, 且不包含任何整数.

2.2.2. 选择

许多允许类型出现的位置也允许类型之间的选择, 这些选择由 "/" (slash) 分隔. 如果需要使构造无歧义, 可以将整个选择构造放入圆括号中 (关于 CDDL grammar 的细节, 请见 Appendix B).

值的选择可用于表达枚举:

attire = "bow tie" / "necktie" / "Internet attire"
protocol = 6 / 17

类似于类型, CDDL 也允许组之间的选择, 由 "//" (double slash) 分隔. 注意, "//" 操作符的绑定强度远弱于其他 CDDL 操作符, 因此下面示例中 "delivery" 内的每一行都是组选择中的一个独立备选项:

address = { delivery }

delivery = (
street: tstr, ? number: uint, city //
po-box: uint, city //
per-pickup: true
)

city = (
name: tstr, zip-code: uint
)

组选择匹配由该选择中各备选项可匹配的名称/值对序列集合所构成的并集.

对于类型选择和组选择, 都可以稍后在单独规则中分别使用 "/=" 和 "//=" 代替 "=" 来向规则添加更多备选项:

attire /= "swimwear"

delivery //= (
lat: float, long: float, drone-type: tstr
)

如果一个名称第一次就是与 "/=" 或 "//=" 一起使用, 这并不是错误 (无需先用 "=" 来 "创建它").

2.2.2.1. 范围

CDDL 不要求列出构成某个选择的所有值, 而是允许从两个具有排序关系的值构造出一个 range: 下界 (第一个值) 和上界 (第二个值). 范围可以同时包含给定的两个边界 (通过用 ".." 连接两个值来表示), 也可以包含下界但排除上界 (改用 "..." 表示). 如果下界超过上界, 得到的类型就是空集 (在使用泛型 (Section 3.10) 时, 这种行为可能是合乎需要的).

device-address = byte
max-byte = 255
byte = 0..max-byte ; inclusive range
first-non-byte = 256
byte1 = 0...first-non-byte ; byte1 is equivalent to byte

CDDL 目前只允许整数之间的范围 (匹配整数值) 或浮点值之间的范围 (匹配浮点值). 如果某个类型中二者都需要, 可以 (较笨拙地) 使用这两类范围之间的类型选择:

int-range = 0..10 ; only integers match
float-range = 0.0..10.0 ; only floats match
BAD-range1 = 0..10.0 ; NOT DEFINED
BAD-range2 = 0.0..10 ; NOT DEFINED
numeric-range = int-range / float-range

(另见 Section 3.8.6 中的控制操作符 .lt/.ge 和 .le/.gt.)

注意, 点号是 CDDL 中有效的名称延续字符, 因此

min..max

不是范围表达式, 而是单个名称. 当使用名称作为范围操作符左侧时, 请像下面这样使用空格

min .. max

以便将范围操作符分隔出来.

2.2.2.2. 将组转换为选择

有些选择由大量值构成, 通常是整数, 其中每个整数最好都在规范中给出一个语义名称. CDDL 不要求先命名这些整数再将它们累积成一个选择, 而是允许通过在组前加上 "&" 字符来从组构造选择:

terminal-color = &basecolors
basecolors = (
black: 0, red: 1, green: 2, yellow: 3,
blue: 4, magenta: 5, cyan: 6, white: 7,
)
extended-color = &(
basecolors,
orange: 8, pink: 9, purple: 10, brown: 11,
)

与在数组中使用组的情况一样 (Section 3.4), 成员名称只具有文档价值 (特别是, 工具在显示来自该选择的整数时可能会使用它们).

2.2.3. 表示类型

CDDL 允许通过引用 CBOR 表示来规定数据项类型 (具体来说, 是引用主类型和附加信息; 见 [RFC7049] 的 Section 2). 从 prelude (Appendix D) 中应能明显看出这种用法: 一个井号 ("#") 后面可选地跟随一个从 0 到 7 的数字来标识主类型, 随后还可跟随一个点号和一个数字来指定附加信息. 这种构造规定了可在 CBOR 中序列化的值集合 (即 "any"), 如果给出主类型, 则由该主类型规定; 如果主类型和附加信息都给出, 则由二者共同规定. 当使用主类型 6 (Tag) 时, 可通过在其后追加圆括号中的类型来指定被标记项的类型.

注意, 尽管该记法基于 CBOR 序列化, 它描述的是数据模型层面的值集合. 例如, "#7.25" 规定的是可表示为半精度浮点数的值集合; 它并不强制这些值也必须序列化为半精度浮点数: CDDL 没有提供任何语言机制来限制序列化变体的选择. 这也使 CDDL 能与 JSON 一起使用, 而 JSON 使用一种根本不同的方式来序列化 (部分) 相同的值.

可能需要在 prelude 之外使用表示类型, 例如, 某个规范可以从以更具体方式使用既有标签开始, 也可以定义 prelude 中未定义的新标签:

my_breakfast = #6.55799(breakfast)   ; cbor-any is too general!
breakfast = cereal / porridge
cereal = #6.998(tstr)
porridge = #6.999([liquid, solid])
liquid = milk / water
milk = 0
water = 1
solid = tstr

2.2.4. 根类型

CDDL 数据结构定义没有用于标识根的特殊语法: 该角色简单地由文件中定义的第一条规则承担.

这源于定义数据结构时通常采用的自顶向下方法, 即把大的数据结构单元分解为更小的部分; 不过, 除根类型之外, 并不需要严格遵循这一顺序.

(注意, 没有办法将组用作根 -- 根必须是类型.)