5 个版本
0.4.2 | 2023年11月7日 |
---|---|
0.4.1 | 2023年1月12日 |
0.3.2 | 2022年11月3日 |
0.3.1 |
|
0.3.0 | 2022年4月18日 |
#170 in 编码
每月21次下载
160KB
4K SLoC
隐式数据标记
IDM是一种非自描述的数据序列化格式,旨在由人类编写和阅读。
它使用基于缩进的大纲语法,缩进具有语义意义。它需要一个外部类型模式来指导解析输入,如果预期不同的类型,相同的输入可以以不同的方式解析。在Rust版本中,类型模式由Serde数据模型提供。因为格式不是自描述的,IDM文件可以使用比几乎所有其他人类可写的数据序列化语言更少的语法。
其动机是提供一个接近自由形式手写纯文本笔记的记法。IDM可以被视为一个程序的长期用户界面,而不仅仅是计算机之间的数据交换协议。
它基本上是一个固定自行车序列化格式。简单、有趣,并且可能最终会导致可怕的崩溃。预计用户在使用时控制数据和类型,可以解决它无法处理的边缘情况。
如果您需要任何Rust数据结构的健壮序列化,多个人员之间无需紧密沟通即可轻松协作,或者只是需要一般的高可靠性,那么您可能需要一个更详尽的序列化语言,如JSON、YAML或RON。
用法
IDM实现了Serde序列化。
使用idm::from_str
来反序列化Serde可反序列化数据。使用idm::to_string
来序列化Serde可序列化数据。
IDM是一种非自描述的数据格式
根据预期的类型,相同的输入可以以几种方式解析。以下是一个例子:
1 2 3
4 5 6
7 8 9
当期望一个单独的 String
时,整个内容将逐字读取为一个三行段落。当期望一个列表 Vec<String>
序列时,它是三行,["1 2 3", "4 5 6", "7 8 9"]
。当期望一个矩阵 Vec<Vec<i32>>
时,它是三个包含三个数字的列表,[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
。
序列可以垂直读取为条目概要,或者水平读取为空白分隔的词序列。对于水平序列,空白是唯一的识别分隔符。如果序列元素的表示包含空格,则始终可以使用垂直序列。
基本语法
IDM 概要 文档由一行或多行文本组成,以换行符终止。它必须包含至少一个换行符,否则它将被视为内联 片段 文档。IDM文档中的所有缩进必须仅使用ASCII空格或仅使用ASCII制表符,不能以任何方式混合。
IDM只将ASCII制表符和空格(U+0009和U+0020)视为空白。在随后的文本中,“空白”仅指这些字符和新行。例如,NBSP(U+00A0)字符被视为非空白。
概要被递归定义为由 条目 组成,这些条目由缩进到概要缩进深度的行组成,该行下可以有可选的具有更深缩进的主体概要。如果条目仅由行组成,它称为 行,如果它有一个非空的主体概要,它称为 部分,其顶部行称为 标题,子概要称为 正文。
一个文档中正文条目的缩进必须要么仅使用ASCII空格,要么仅使用ASCII制表符,不能以任何方式混合。部分主体的所有条目必须缩进到相同的深度。以下概要有无效的语法
Headline
Item 1 at indent depth 4
Item 2 at indent depth 2, inconsistent dedentation!
空白行被解释为具有它们之后的第一个非空白行的缩进深度,或者如果不存在非空白行,则为0。这意味着空白行永远不能是部分标题,因为标题必须比紧随其后的行的缩进深度更浅。
对于单行文档,尾部换行符的语法上有意义。带有尾部换行符的单行被读取为单项概要,而没有尾部换行符的单行被读取为可能被解释为水平序列的片段。
// No trailing newline, parses as horizontal sequence.
assert_eq!(
idm::from_str::<Vec<String>>("a b").unwrap(),
vec!["a".to_string(), "b".to_string()]);
// Trailing newline present, parses as single-item vertical sequence.
assert_eq!(
idm::from_str::<Vec<String>>("a b\n").unwrap(),
vec!["a b".to_string()]);
IDM 文档的部分内容可能与序列化用户数据的多行字符串值对应,但它们仍然必须遵循文档的缩进约定。带有前导空格、不一致的缩进或混合制表符和空格的字符串值不能进行序列化。如果序列化使用的缩进风格与多行字符串不同(制表符代替空格或反之),则字符串将被重写以使用不同的缩进风格。在这种情况下,可能丢失原始缩进的精确深度,尽管应该始终保留缩进的逻辑结构。
特殊语法
除了显著的空白外,IDM 只有两个内置语法元素:注释和冒号块。
注释始终是行上的唯一元素,它们必须是 "--"
或以 "-- "
开头,后面跟着空格后的任意文本。除了在文档中留下注释的常规作用外,注释还充当语法分隔符。序列中的一系列垂直序列必须用缩进的注释行分隔
1 2
3 4
--
4 5
6 7
冒号行以冒号开头,后跟一个非空白字符。冒号块是一系列连续的冒号行,其中只允许有注释和空行。
:a 1
:b 2
Not part of the attribute block
冒号块是额外的缩进级别和分隔无标题概要的注释行的语法糖。上面的片段相当于
--
a 1
b 2
Not part of the attribute block
特殊形式
因为希望您不会为其他任何事情使用它们,IDM 将单例元组((A,)
)作为 IDM 文档中特殊形式的标记。特殊形式是必需的,这样 IDM 就可以将整个文件(包括注释和空白)读取到标准概要结构中,保留所有文件内容。
对中头位置的 String
单例匹配 原始模式 中的行。当前条目的标题(即使它是注释或空白行)将被读取到对的头字符串中。章节的主体将正常解析到对的尾部。
-- This gets read into the String at pair head (even with comment syntax)
These lines get
Read into the body
Of the pair type
头部单例中具有类似映射类型(结构体或映射)的对将读取一个概要(不是像字符串头部的对那样读取项),它将期望在缩进的块中找到初始映射值,然后将其余的概要项作为单个块读取到对的尾部。因此,类似于 ((BTreeMap<String, String>,), Vec<String>)
的类型将期望如下内容
--
key1 value1
key2 value2
First element in pair tail
Second element in pair tail...
然而,这正是冒号块所设计的。而不是显式地编写缩进的块,编写值的习惯用法是
:key1 value1
:key2 value2
First element in pair tail
Second element in pair tail...
所有对头部位置的映射值都必须是垂直形式。如果类型是结构体,则不能在此处使用水平内联结构体形式。
元组和序列
IDM支持两种简单的集合类型:同质项类型且长度未知的序列,以及长度已知且异质项类型的元组。序列与垂直序列线或块的标准化形式以及水平序列单词的标准化形式相匹配。元组与相同的模式相匹配,但也与其他一些模式相匹配。由于单例元组被保留用于IDM的特殊形式,实际使用的元组长度至少为2。
由于元组的长度已知,可以对其最后一个元素应用特殊规则。虽然单行水平序列的所有元素都必须是单个单词,但元组的最后一个元素是整行剩余部分,可以包含空白。行 key A multi-line value
可以与 (String, String)
匹配,值 ("key", "A multi-line value")
。元组的最后一个值也可以是项目主体,因此对偶元组也可以匹配
head
Multiple lines
of body
结构体和映射
结构体和映射(类似映射的类型)处理得非常相似。它们的实际语法是无修饰键的轮廓,后跟属性,但它们通常放在冒号块中,给人一种冒号前缀是编写映射键的语法的错觉。为了维持这个假象,任何独立的类似映射值都可以写成冒号块,解析器将检测额外的块嵌套并弹出内部值。
解析映射或垂直结构体的项目等同于解析映射的键和值类型的元组序列,或者对于结构体的字段和相应的字段值类型为字符串。
与IDM的大多数其他类型不同,映射只有垂直形式。这很重要,因为它使得在解析映射的特殊形式中,映射在配对的开头解析时,可以解析映射的缺失。
结构体确实有水平形式。在这个形式中,结构体值仅由结构体字段的值组成,字段名称不包含在内。值以与结构体声明中出现的顺序完全相同的方式列出。这种形式通常用于编写表格数据。用户需要对此形式谨慎,因为它不包含字段名称,因此结构体类型中字段数量或顺序的任何变化都将使针对先前版本的类型编写的内联值无效。
IDM表示缺失值的能里非常有限,因此对于 Option
值的惯例是在映射或结构体中省略整个项(键和值),如果值为 None
。水平形式编写的结构体不能有缺失值。完全空的结构体或映射可以在特殊配对头单例位置匹配,但不能在其他地方。
复杂结构示例
可以使用从对象名称到对象的映射来编写复杂结构,这些映射转换为章节轮廓,并使用具有此类映射的结构体对,这些对成为冒号块,后跟子元素。以下示例展示了如何表示星系及其围绕运行的行星的数据库。行星数据使用内联结构体紧凑表示。
IDM数据
Sol
:age 4.6e9
:mass 1.0
-- :orbit :mass
Earth 1.0 1.0
Mars 1.52 0.1
Alpha Centauri
:age 5.3e9
:mass 1.1
-- :orbit :mass
Chiron 1.32 1.33
类型签名和解析测试
use serde::Deserialize;
use std::collections::BTreeMap;
type StarSystem = ((Star,), BTreeMap<String, Planet>);
type Starmap = BTreeMap<String, StarSystem>;
#[derive(PartialEq, Debug, Deserialize)]
struct Star {
age: f32,
mass: f32,
}
#[derive(PartialEq, Debug, Deserialize)]
struct Planet {
orbit: f32,
mass: f32,
}
assert_eq!(
idm::from_str::<Starmap>("\
Sol
:age 4.6e9
:mass 1.0
-- :orbit :mass
Earth 1.0 1.0
Mars 1.52 0.1
Alpha Centauri
:age 5.3e9
:mass 1.1
-- :orbit :mass
Chiron 1.32 1.33
").unwrap(),
BTreeMap::from([
("Sol".into(),
((Star { age: 4.6e9, mass: 1.0 },),
BTreeMap::from([
("Earth".into(), Planet { orbit: 1.0, mass: 1.0 }),
("Mars".into(), Planet { orbit: 1.52, mass: 0.1 })]))),
("Alpha Centauri".into(),
((Star { age: 5.3e9, mass: 1.1 },),
BTreeMap::from([
("Chiron".into(), Planet { orbit: 1.32, mass: 1.33 })])))]));
有关如何使用用户定义类型向IDM添加语法的示例,请参阅 内联映射 示例。
轮廓形式
目前看到的IDM用途都期望你有用于序列化的特定应用程序类型。然而,IDM也可以序列化通用大纲类型,这可以解析大多数纯文本文件。
简单大纲的类型签名如下
struct Outline(Vec<((String,), Outline)>);
这符合通用IDM大纲的形式,大纲条目是以下代码对:((String,), Outline)
对。带有字符串头的对使IDM以原始模式解析每个条目,因此注释和空行将包含在数据结构中,并在结构重新序列化时被回显。
更丰富的结构是数据大纲,它为每个条目支持任意数据映射
use indexmap::IndexMap;
struct DataOutline((IndexMap<String, String>,), Vec<((String,), DataOutline)>);
现在您可以从大纲中的任何条目读取命名属性。所有值都将为字符串,但知道特定属性预期类型的应用程序可以使用IDM将属性值再次反序列化为更合适的数据类型。
use indexmap::IndexMap;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct DataOutline(
(IndexMap<String, String>,),
Vec<((String,), DataOutline)>,
);
let outline = idm::from_str::<DataOutline>(
"\
Example outline
Stuff
:tags foo bar
This part has stuff
Things",
)
.unwrap();
// Raw access patterns are pretty rough.
// A proper app would need some sort of selection API here,
// the explicit indexing gets very rough very fast.
assert_eq!(outline.1[0].1 .1[0].0 .0, "Stuff"); // On the right track...
let tags = &outline.1[0].1 .1[0].1 .0 .0["tags"]; // Grab tags field.
assert_eq!(tags, "foo bar");
// Cast to a more appropriate format.
let tags: Vec<String> = idm::from_str(tags).unwrap();
assert_eq!(tags, vec!["foo", "bar"]);
关于数据大纲的重新序列化有一个注意事项,即属性块不是以原始模式读取。因此,在将大纲从内存反序列化和序列化时,属性中的注释行将不会被保留,因此当编写打算通过IDM重写的大纲时应该避免使用它们。
大纲类型故意不包括在IDM存储库中。实际上,除了类型签名外,它们没有任何操作,并且您预期在自己的应用程序中复制这些签名。您还可能希望为该类型实现自己特定于应用程序的方法,如果您拥有该类型,这将更容易实现。
关于具有数据混合的大纲结构的另一个示例,请参阅示例中的最小博客引擎和相应的内容文件。
接受的形状
类型 | 垂直值 | 水平值 |
---|---|---|
atom | 块,部分 | 行,单词 |
特殊字符串头 | 部分,行作为部分 | - |
特殊映射头 | 块 | - |
映射 | 块 | - |
元组/映射元素 | 块,部分 | 行 |
结构体 | 块 | 行 |
序列 | 块 | 行 |
某些类型支持垂直(每个条目独占一行)和水平(每个条目在同一行上)值,其余类型仅支持垂直值。
第一个位置是单例字符串元组的对触发原始模式。原始模式具有独特的行作为部分的解析模式,其中它将行解释为具有空体的部分。通常行被解释为结构化类型的水平变体。
注意
-
Serde的
#[serde(flatten)]
属性在与结构一起使用时与IDM不兼容。它从类似结构的解析切换到类似映射的解析,并停止为值提供类型。结果是,所有展开的结构值都作为字符串提供,并且无法正确反序列化。它仍然可以用于收集字符串值或所有字段都是字符串值(或通过字符串反序列化)的映射或结构。 -
第一部分是一个冒号缩进的结构体或映射的特殊对不能有第二部分是另一个具有映射头的特殊对。由于第二部分在第一部分中融合,第二个映射无法从第一个映射中语法上区分。
-
原始类型、字符、数值类型和布尔值在解析之前会被去除Unicode空白字符,如NBSP。主要的IDM算法将NBSP视为内容而不是缩进。这允许你使用NBSP进行左填充的表格行,而不会因为使用NBSP而破坏IDM的解析,同时仍然可以正确地从最左边的表格列解析原始元素。具有自定义字符串值解析的用户类型可能需要自行修剪输入字符串。
许可协议
IDM采用Apache-2.0和MIT双重许可。
依赖项
~0.4–1MB
~23K SLoC