2023 年 09 月 22 日
假设以下文本为 rzeo 的输入:
以下 Rust 程序
@ hello world #
fn main() {
println!("Hello world!");
}
@
可在终端打印「Hello world!」。
rzeo 可将其转化为以下 YAML 格式:
- type: 文档片段
name:
content: '以下 Rust 程序'
- type: 源码片段
name: 'hello world'
content: |-
'fn main() {
println!("Hello world!");
}'- type: 文档片段
name:
content: '可在终端打印「Hello world!」。'
基于 rzeo 的输出,可编写一些程序,将其转化为某些排版软件支持的文档格式,从而实现程序文档的排版。rzeo 不生产程序的文档和源码,只是程序设计思维的搬运工。
该如何解析上述的 YAML 格式呢?在第 6 章「配置文件」中讲述了如何解析 YAML 格式,但所用示例较为简单,不足以令人掌握 Rust 的 Serde 库和 C 的 libcyaml 库的基本用法。可将本章内容视为第 6 章的延续。
若不熟悉上述 YAML 标记,可参考「YAML 语言教程」。
rzeo 输出的 YAML 文件,本质上是一个数组。在 Rust
语言里,与之对应的数据结构是 Vec<T>
,其中
T
在此是结构体类型:
#[derive(Debug, serde::Deserialize)]
struct Node {
type: String,
name: String,
content: String }
Node
类型的实例,当其 type
为
文档片段
时,其 name
是空字符串。不过上述代码是无法通过编译的,因为 type
是 Rust
关键字,不允许作为变量名使用。rustc 建议给 type
增加前缀
r#
,用于指示编译器,所标定的字符串是普通文本,而非关键字。
serde_yaml 库可将 YAML 的数组结构解析为 Vec<T>
实例,例如:
let foo: Vec<Node> = serde_yaml::from_reader(f).unwrap();
for x in foo {
println!("{:?}", x);
}
下面是完整的 YAML 数组反序列化代码:
use std::fs::File;
#[derive(Debug, serde::Deserialize)]
struct Node {
r#type: String,
: String,
name: String
content}
fn main() -> Result<(), std::io::Error> {
let f = File::open("foo.yml")?;
let foo: Vec<Node> = serde_yaml::from_reader(f).unwrap();
for x in foo {
println!("{:?}", x);
}
return Ok(());
}
输出结果为
Node { type: "文档片段", name: "", content: "以下 Rust 程序" }
Node { type: "源码片段", name: "hello world",
content: "'fn main() {\n println!(\"Hello world!\");\n}'" }
Node { type: "文档片段", name: "", content: "可在终端打印「Hello world!」。" }
在 YAML 文件中,值为空的数据项(对象),可以省略。例如
- type: 文档片段
content: '以下 Rust 程序'
- type: 源码片段
name: 'hello world'
content: |-
'fn main() {
println!("Hello world!");
}'- type: 文档片段
content: '可在终端打印「Hello world!」。'
不过,上一节的程序在解析上述 YAML 文件时会出错,因为 serde
库无法为类型为 文档片段
的结点提供 name
的默认值。规避该错误的办法是,将 Node
的定义修改为
#[derive(Debug, serde::Deserialize)]
struct Node {
r#type: String,
#[serde(default)]
: String,
name: String
content}
上述修改可使得 serde 库能够为 name
提供默认值——空字符串。
上文示例中的 YAML 文件,其中类型为 文档片段
的条目不需要
name
,若欲消除数据冗余,可将 YAML 文件可改为
- Doc:
content: '以下 Rust 程序'
- Code:
name: 'hello world'
content: |-
'fn main() {
println!("Hello world!");
}'- Doc:
content: '可在终端打印「Hello world!」。'
注意:serde_yaml 库不支持在 YAML 文件中以中文字符作为对象名。
此时,YAML 数组中的元素类型不相同,故而需要使用 enum
类型定义 Vec<T>
的
T
,将两种类型的元素统一为一种类型:
#[derive(Debug, serde::Deserialize)]
struct DocFragment {
: String
content}
#[derive(Debug, serde::Deserialize)]
struct CodeFragment {
: String,
name: String
content}
#[derive(Debug, serde::Deserialize)]
enum Node {
: DocFragment,
Doc: CodeFragment
Code}
然而,只有使用 YAML 的自定义标签语法方能使得 serde_yaml 库将 YAML
数据项反序列化为 enum
类型的实例,故而上述 YAML
文件内容需修改为
- !Doc
content: '以下 Rust 程序'
- !Code
name: 'hello world'
content: |-
'fn main() {
println!("Hello world!");
}'- !Doc
content: '可在终端打印「Hello world!」。'
解析上述 YAML 文件的完整 Rust 代码如下:
use std::fs::File;
#[derive(Debug, serde::Deserialize)]
struct DocFragment {
: String
content}
#[derive(Debug, serde::Deserialize)]
struct CodeFragment {
: String,
name: String
content}
#[derive(Debug, serde::Deserialize)]
enum Node {
,
Doc(DocFragment)
Code(CodeFragment)}
fn main() -> Result<(), std::io::Error> {
let f = File::open("foo.yml")?;
let foo: Vec<Node> = serde_yaml::from_reader(f).unwrap();
for x in foo {
println!("{:?}", x);
}
return Ok(());
}
C 库 libcyaml(1.0 版本)目前不支持 YAML 单纯的数组结构。例如对于以下 YAML 文件:
- type: 文档片段
content: '以下 Rust 程序'
- type: 源码片段
name: 'hello world'
content: |-
'fn main() {
println!("Hello world!");
}'- type: 文档片段
content: '可在终端打印「Hello world!」。'
目前无法基于 libcyaml 写出相应的解析代码。也有可能是能够写得出,只是我尚不得其门径。若将上述数组结构修改为 YAML 对象(键值对),例如
nodes:
- type: 文档片段
content: '以下 Rust 程序'
- type: 源码片段
name: 'hello world'
content: |-
'fn main() {
println!("Hello world!");
}' - type: 文档片段
content: '可在终端打印「Hello world!」。'
解析代码便可写得出来,例如
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <cyaml/cyaml.h>
typedef struct {
char *type;
char *name;
char *content;
} Node;
typedef struct {
*nodes;
Node size_t nodes_count;
} Foo;
static const cyaml_schema_field_t node_field_schema[] = {
("type", CYAML_FLAG_POINTER,
CYAML_FIELD_STRING_PTR, type, 0, CYAML_UNLIMITED),
Node("name", CYAML_FLAG_POINTER | CYAML_FLAG_OPTIONAL,
CYAML_FIELD_STRING_PTR, name, 0, CYAML_UNLIMITED),
Node("content",
CYAML_FIELD_STRING_PTR, Node, content, 0, CYAML_UNLIMITED),
CYAML_FLAG_POINTER
CYAML_FIELD_END};
static const cyaml_schema_value_t node_schema = {
(CYAML_FLAG_DEFAULT, Node, node_field_schema)
CYAML_VALUE_MAPPING};
static const cyaml_schema_field_t foo_field_schema[] = {
("nodes", CYAML_FLAG_POINTER,
CYAML_FIELD_SEQUENCE, nodes, &node_schema, 0, CYAML_UNLIMITED),
Foo
CYAML_FIELD_END};
static const cyaml_schema_value_t top = {
(CYAML_FLAG_POINTER, Foo, foo_field_schema)
CYAML_VALUE_MAPPING};
/* 解析器设置 */
static const cyaml_config_t config = {
.log_fn = cyaml_log,
.mem_fn = cyaml_mem,
.log_level = CYAML_LOG_WARNING
};
/* 反序列化 */
int main(void) {
*foo;
Foo = cyaml_load_file("foo.yml", &config, &top,
cyaml_err_t err (cyaml_data_t **)&foo, NULL);
if (err != CYAML_OK) {
(stderr, "ERROR: %s\n", cyaml_strerror(err));
fprintfreturn EXIT_FAILURE;
}
for (size_t i = 0; i < foo->nodes_count; i++) {
("- type: %s\n", (foo->nodes[i]).type);
printfif (foo->nodes[i].name) {
(" name: %s\n", (foo->nodes[i]).name);
printf}
(" content: %s\n", (foo->nodes[i]).content);
printf}
(&config, &top, foo, 0);
cyaml_freereturn 0;
}
注意,上述代码中的 CYAML_FLAG_OPTIONAL
相当于 Rust
结构体里的 #[serde(Default)]
标记。
与 Rust 库 serde_cyaml 相比,C 库 libcyaml 库用起来颇为晦涩,且遇到非常规的 YAML 格式,只能使用更为底层的库 libyaml 进行半自动解析。在 C 程序的数据交换文件格式选择方面,我不推荐 YAML,更好的方案是将 Lua 解释器嵌入 C 程序,然后将 Lua 表作为数据交换格式。