一、简介
serde_yaml 是 Rust 语言中一个用于序列化和反序列化 YAML 数据的库。它以其高性能和灵活性而广受欢迎,特别适用于需要处理 YAML 配置文件的项目。在这篇博客中,我们将探讨 serde_yaml 的基本用法,并提供一些实用的示例代码。
二、使用场景
serde_yaml 常用于以下场景:
- 配置管理:许多应用程序使用 YAML 文件来存储配置参数,serde_yaml 可以轻松解析这些文件。
- 数据交换:在系统之间传递数据时,YAML 格式因其可读性而常被选用。
- 文档生成:一些工具使用 YAML 来描述文档结构,serde_yaml 可以帮助生成和解析这些文档。
三、基本使用
开始使用 serde_yaml 之前,先创建一个 serde_yaml_test 项目文件夹,在文件夹中使用 cargo init
初始化 rust 项目,然后在项目的 Cargo.toml 文件中添加依赖:
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9.34"
要使用最新版本的 serde 的话可以使用命令行来添加依赖,如:
cargo add serde --features="derive"
cargo add serde_yaml
依赖的包都准备完成后,就来看看几个示例,演示如何读取、解析和生成 YAML 文件。
1、读取和解析 YAML 文件
以下是一个基本示例,展示了如何使用 serde_yaml 读取和解析一个名为 以下是一个基本示例,展示了如何使用 serde_yaml 读取和解析一个名为 hello.yml
的文件: 的文件:
# 在项目根目录创建一个 hello.yml 文件
touch hello.yml
# 此时目录结构为
#|-serde_yaml_test
# |--src
# |--target
# |--.gitignore
# |--Cargo.lock
# |--Cargo.toml
# |--hello.yml
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use serde_yaml;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut file = File::open("hello.yml")?; // 打开文件 hello.yml
let mut contents = String::new(); // 创建一个字符串
file.read_to_string(&mut contents)?; // 读取文件内容到字符串缓冲区
println!("YAML file contents:\n{}", contents); // 打印原始的YAML字符串
// 将YAML字符串反序列化为BTreeMap
let deserialized_map: BTreeMap<String, serde_yaml::Value> = serde_yaml::from_str(&contents)?;
println!("Deserialized map: {:#?}", deserialized_map); // 打印反序列化后的数据结构
if let Some(name) = deserialized_map.get("name").and_then(|v| v.as_str()) {
println!("name: {}", name); // 打印 name
} else {
println!("The key 'name' was not found or is not a string.");
}
Ok(())
}
// 输出结果:
// Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
// Running `target\debug\serde_yaml_test.exe`
// YAML file contents:
// name: data
// Deserialized map: {
// "name": String("data"),
// }
// name: data
2、序列化数据结构为 YAML
示例展示了如何将一个 Rust 数据结构序列化为 YAML 格式的字符串:
use std::collections::BTreeMap;
use serde_yaml;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 创建一个 BTreeMap 数据结构
let mut data = BTreeMap::new();
data.insert("name".to_string(), serde_yaml::Value::String("Alice".to_string()));
data.insert("age".to_string(), serde_yaml::Value::Number(serde_yaml::Number::from(30)));
// 将数据结构序列化为 YAML 字符串
let yaml_string = serde_yaml::to_string(&data)?;
// 打印序列化后的 YAML 字符串
println!("Serialized YAML:\n{}", yaml_string);
Ok(())
}
3、使用自定义结构体进行反序列化
使用自定义结构体来解析 YAML 数据会更加方便和直观。以下示例展示了如何定义一个结构体并解析 YAML 数据:
use serde::{Deserialize, Serialize};
use serde_yaml;
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: u8,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 定义一个 YAML 字符串
let yaml_str = "
name: Bob
age: 25
";
// 将 YAML 字符串反序列化为 Person 结构体
let person: Person = serde_yaml::from_str(yaml_str)?;
// 打印反序列化后的结构体
println!("Deserialized person: {:?}", person);
Ok(())
}
4、从文件流中直接解析
有时,为了优化内存使用,可以直接从文件流中进行解析:
use serde::{Deserialize, Serialize};
use std::fs::File;
#[derive(Debug, Serialize, Deserialize)]
struct Config {
database_url: String,
port: u16,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 打开配置文件
let file = File::open("config.yml")?;
// 从文件流中直接反序列化
let config: Config = serde_yaml::from_reader(file)?;
// 打印反序列化后的配置
println!("Config: {:?}", config);
Ok(())
}
四、复杂 YAML 文件解析
基于以上案例,来尝试解析一个复杂的 yaml 文件,既然要复杂,那就尝试解析一下 docker-compose.yml
文件吧!先来准备一个 yaml 文件,放在项目根目录下,如下:
version: "3.9"
services:
redis:
image: redis:latest
restart: always
privileged: true
ports:
- 6379:6379
volumes:
- ./data/redis/data:/data
- ./redis/etc:/usr/local/etc/redis
command: redis-server /usr/local/etc/redis/redis.conf
nginx:
image: nginx:latest
restart: always
privileged: true
volumes:
- ./nginx/etc/nginx:/etc/nginx
- ./var/www:/var/www/html
ports:
- "80:80"
- "443:443"
depends_on:
- redis
networks:
mynet1:
driver: overlay
attachable: false
再写代码之前,先把 rust 引入的包先贴出来,后面贴代码时可以少写一点啰嗦代码,只关注核心逻辑,看起来更清晰易懂,这是整个项目所依赖的包,也没有很多
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::fs::File;
use std::io::Read;
use serde::{Deserialize, Serialize};
1.固定字段解析
整个 yml 文件看下来,并没有很多,但算是一个比较全面的 docker-compose 配置文件了,虽然没有用到所有的配置字段,触类旁通,再多的配置字段都能轻车熟路的解析吧,先一级一级的看,一级有 version
services
networks
,通过以上,就可以定义出需要的结构体了:
#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
version: String,
services: HashMap<String, ServiceCfg>,
}
这里的 services 的类型用的是 HashMap,因为 services 名称是一个变量,而且数量不固定,比如还可以增加 mysql 服务,所以 services 是一个动态,services 里面又引入了 ServiceCfg 类型,现在继续看 services 下一级的配置字段,其中 image 是必须要存在的配置,那 ServiceCfg 结构体可以定义成:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
image: String,
}
好了,就这么简单,先写个 main 函数把代码跑起来看看吧!
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut file = File::open("docker-compose.yml")?; // 打开文件
let mut contents = String::new(); // 创建一个字符串
file.read_to_string(&mut contents)?; // 读取文件内容到字符串缓冲区
// println!("读取到的 YAML 内容:\n{}", contents); // 打印原始的YAML字符串
// 将 YAML 字符串反序列化为 DockerYaml 结构体
let docker_yaml: DockerYaml = serde_yaml::from_str(&contents)?;
// 打印反序列化后的结构体
println!("反序列化后的结构体: {:?}", docker_yaml);
// 读取出 services
let services = &docker_yaml.services;
for (serve, config) in services {
println!("services name: {}", serve);
println!("services config: {:?}", config);
}
Ok(())
}
// 打印的内容:
// 反序列化后的结构体: DockerYaml { version: "3.9", services: {"nginx": ServiceCfg { image: "nginx:latest" }, "redis": ServiceCfg { image: "redis:latest" }} }
// services name: nginx
// services config: ServiceCfg { image: "nginx:latest" }
// services name: redis
// services config: ServiceCfg { image: "redis:latest" }
正常打印了,现在可以完善 ServiceCfg 这个结构体了
2.动态字段解析
docker-compose 配置的 services 中,大部分配置都可选的,如果不配置,就会有个默认值,先不关心这个默认值是什么,但起码要实现一个不配置会有默认值的功能,带着疑问,先去写一下代码,
把 ServiceCfg 结构体修改成以下的样子:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
image: String,
restart: String,
}
只是添加了一个 restart 配置项,main 函数不变,cargo run 一下发现正常打印了如下:
反序列化后的结构体: DockerYaml { version: "3.9", services: {"nginx": ServiceCfg { image: "nginx:latest", restart: "always" }, "redis": ServiceCfg { image: "redis:latest", restart: "always" }} }
services name: nginx
services config: ServiceCfg { image: "nginx:latest", restart: "always" }
services name: redis
services config: ServiceCfg { image: "redis:latest", restart: "always" }
restart 配置项正常读取到了,好像一切都很完美,但 restart 配置是个可选配置,那在 docker-compose.yml
文件中随便挑一个 restart 配置注释掉看看
version: "3.9"
services:
redis:
image: redis:latest
# restart: always
privileged: false
再次运行 cargo run 就会发现报错了,报错很明显,缺少 restart 这个配置
Error: Error("services.redis: missing field `restart`", line: 4, column: 5)
error: process didn't exit successfully: `target\debug\serde_yaml_test.exe` (exit code: 1)
要解决这个问题很简单,serde 提供了一个字段属性可以快速设置这个字段的默认值,
#[serde(default)]
如果反序列化时该值不存在,使用Default::default()
生成默认值
在 ServiceCfg.restart 中加上就可以,所以把 ServiceCfg 结构体修改成以下的样子:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
image: String,
#[serde(default)]
restart: String,
}
// 再次执行 cargo run 后会得到以下输出
// 反序列化后的结构体: DockerYaml { version: "3.9", services: {"nginx": ServiceCfg { image: "nginx:latest", restart: "always" }, "redis": ServiceCfg { image: "redis:latest", restart: "" }} }
// services name: nginx
// services config: ServiceCfg { image: "nginx:latest", restart: "always" }
// services name: redis
// services config: ServiceCfg { image: "redis:latest", restart: "" }
自此,可选配置的默认值问题就解决了,但又有个问题,ports 和 volumes 该使用什么数据类型来承载呢,其实 yml 文件中,所有以 - 开头的,都表示同一级下有多个值,就那 redis 的 volumes 来说,他表示要映射本地多个数据卷到容器中,具体多少个?不知道,那这种结构是不是有点像可变长度的数组,在 rust 中最适合这种结构的类型是不是就是 Vec 了,基于此最终完善 ServiceCfg 结构体的内容如下:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
image: String,
#[serde(default)]
restart: String,
#[serde(default)]
privileged: bool,
#[serde(default)]
ports: Vec<String>,
#[serde(default)]
volumes: Vec<String>,
#[serde(default)]
command: String,
#[serde(default)]
depends_on: Vec<String>,
}
现在可以随意注释掉 services 中的除 image 配置外的所有配置了,而且解析也不会报错,也有默认值了,把 networks 配置也一并实现了吧
#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
version: String,
services: HashMap<String, ServiceCfg>,
#[serde(default)]
networks: HashMap<String, NetworkCfg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
image: String,
#[serde(default)]
restart: String,
#[serde(default)]
privileged: bool,
#[serde(default)]
ports: Vec<String>,
#[serde(default)]
volumes: Vec<String>,
#[serde(default)]
command: String,
#[serde(default)]
depends_on: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NetworkCfg {
#[serde(default)]
driver: Option<String>,
#[serde(default)]
attachable: Option<bool>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut file = File::open("docker-compose.yml")?; // 打开文件
let mut contents = String::new(); // 创建一个字符串
file.read_to_string(&mut contents)?; // 读取文件内容到字符串缓冲区
// println!("读取到的 YAML 内容:\n{}", contents); // 打印原始的YAML字符串
// 将 YAML 字符串反序列化为 DockerYaml 结构体
let docker_yaml: DockerYaml = serde_yaml::from_str(&contents)?;
// 打印反序列化后的结构体
println!("反序列化后的结构体: {:?}", docker_yaml);
// 读取出 services
let services = &docker_yaml.services;
for (serve, config) in services {
println!("services name: {}", serve);
println!("services config: {:?}", config);
}
// 读取出 networks
let networks = &docker_yaml.networks;
for (serve, config) in networks {
println!("networks name: {}", serve);
println!("networks config: {:?}", config);
}
// 将数据结构序列化为 YAML 字符串
let yaml_string = serde_yaml::to_string(&docker_yaml)?;
println!("序列化为 YAML 字符串:\n{}", yaml_string); // 打印序列化后的 YAML 字符串
Ok(())
}
因为 networks 配置也是一个可选配置,所以在 DockerYaml 结构体中要加上 #[serde(default)]
字段属性,至此,读取 yml 文件算是完成了,但看上面的 main 函数,把反序列化的结构体再转换为 yml 文件,就会有点点小问题,实际上上面代码执行完后会生成一个跟读取的 yml 文件不一样内容的 yml 字符串出来,如果期望读取的什么 yml 生成的 yml 也一模一样怎么办?
3.序列化时剔除默认配置
需要可选字段在未设置时在反序列时有生成默认值,但在序列化时把默认值去除,可以使用 serde 的 #[serde(skip_serializing_if = "path")]
调用一个函数来确定是否跳过序列化该字段。给定的函数必须可以作为fn(&T)->bool
调用,尽管它可能是T
上的泛型。例如skip_serializing_if="Option::is_none"
如果值是 None,那么就会跳过。
现在来改造一下上面的 ServiceCfg :
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ServiceCfg {
image: String,
#[serde(skip_serializing_if = "String::is_empty", default)]
restart: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
privileged: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
ports: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
volumes: Vec<String>,
#[serde(skip_serializing_if = "String::is_empty", default)]
command: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
depends_on: Vec<String>,
}
这里有个细节是 privileged 属性由原来的 bool
类型换成了 Option<bool>
, 其中缘由就是 bool 类型不好做类型默认值判断, 像其他的 String,Vec,Option 都有对应的函数给 skip_serializing_if
做默认值判断,换成 Option 后,解析时如果遇到未配置时就会使用 None 代替原来的 bool 默认值,因为 Option 类型的默认值就是 None ,后面使用起来也是很方便的,实际上所有的可选字段配置都可以使用 Option 类型包起来,比如: depends_on : Option<Vec<String>>
,只不过这样写后默认值就不是空的 Vec 了,而是 None,而值存在时就是 Some(Vec[]) 了。
现在还剩一个问题,就是 networks 如果没有配置,但生成的 yml 也会默认带上 networks: {},这个可以使用 HashMap::is_empty 来判断,
#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
version: String,
services: HashMap<String, ServiceCfg>,
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
networks: HashMap<String, NetworkCfg>,
}
那如果是自己定义的类型呢,难道要给自定义类型增加一个判断是否为空的方法吗?可以,但没必要,除非其他逻辑有用到,可以定义一个函数来判断类型是否为空,比如:
#[derive(Debug, Serialize, Deserialize)]
struct DockerYaml {
version: String,
services: HashMap<String, ServiceCfg>,
#[serde(skip_serializing_if = "is_default", default)]
networks: HashMap<String, NetworkCfg>,
}
// 调用这个函数来确定是否跳过序列化该字段
fn is_default<T: Default + PartialEq>(v: &T) -> bool {
v == &T::default()
}
// 因为泛型 T 在函数中用到比较运算,所以要给泛型添加 PartialEq 约束,
// 用于判断是否是默认值,所以 T 要有 default 方法,故也要加上 Default 约束
// 其中 T 表示 HashMap,如果 HashMap 的泛型约束有 PartialEq, 那就表示 HashMap 里面
// 的类型也同样具有相应的约束,所以 NetworkCfg 也要实现 PartialEq trait,
// 而 Default trait 是默认实现的,因为 NetworkCfg 里所有的成员都实现了 Default trait
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // 这行可以省略 Default
struct NetworkCfg {
#[serde(default)]
driver: Option<String>,
#[serde(default)]
attachable: Option<bool>,
}
五、总结
至此,解析 yml 文件算是大功告成了,当然这里只是介绍一小部分 serde 字段属性,但也可以应付大部分场景了,其他的比如字段别名,序列化时处理逻辑,反序列化时处理逻辑,就留在以后去探索吧。