1个不稳定版本
0.1.0 | 2024年2月26日 |
---|
#2647 in 解析器实现
73KB
1.5K SLoC
这是一个轻量级的serde反序列化器,用于处理以逗号分隔的键值对字符串,通常在命令行参数中找到。
例如,你的程序接受以下形式的命令行选项
--foo type=bar,active,nb_threads=8
此crate提供了一个[from_key_values]函数,将这些键值反序列化为配置结构。由于它使用serde,相同的配置结构也可以从任何其他支持的同名键源(如TOML或YAML配置文件)创建。
通过argh_derive
特性,还提供了与argh命令行解析器的集成。
反序列化器支持解析顶层结构体内部的整数(有符号和无符号)、布尔值、字符串(带引号或不带引号)、路径和枚举。字符串中字段的顺序不重要。
简单示例
use serde_keyvalue::from_key_values;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
path: String,
threads: u8,
active: bool,
}
let config: Config = from_key_values("path=/some/path,threads=16,active=true").unwrap();
assert_eq!(config, Config { path: "/some/path".into(), threads: 16, active: true });
let config: Config = from_key_values("threads=16,active=true,path=/some/path").unwrap();
assert_eq!(config, Config { path: "/some/path".into(), threads: 16, active: true });
作为便利,可以省略结构体第一个字段的名称
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
path: String,
threads: u8,
active: bool,
}
let config: Config = from_key_values("/some/path,threads=16,active=true").unwrap();
assert_eq!(config, Config { path: "/some/path".into(), threads: 16, active: true });
可以省略位于Option
后面的字段,在这种情况下,它们将是None
。
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
path: Option<String>,
threads: u8,
active: bool,
}
let config: Config = from_key_values("path=/some/path,threads=16,active=true").unwrap();
assert_eq!(config, Config { path: Some("/some/path".into()), threads: 16, active: true });
let config: Config = from_key_values("threads=16,active=true").unwrap();
assert_eq!(config, Config { path: None, threads: 16, active: true });
或者,可以在选定的字段或整个结构体上使用serde的default
属性来指定未指定的字段应分配其默认值。在下面的示例中,必须指定path
参数。
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
path: String,
#[serde(default)]
threads: u8,
#[serde(default)]
active: bool,
}
let config: Config = from_key_values("path=/some/path").unwrap();
assert_eq!(config, Config { path: "/some/path".into(), threads: 0, active: false });
还可以指定一个提供默认值的函数,请参阅serde字段属性文档以获取详细信息。
布尔值可以是true
或false
,或者根本不带值,在这种情况下,它们将是true
。结合默认值,可以非常容易地实现标志
#[derive(Debug, Default, PartialEq, Deserialize)]
#[serde(default)]
struct Config {
active: bool,
delayed: bool,
pooled: bool,
}
let config: Config = from_key_values("active=true,delayed=false,pooled=true").unwrap();
assert_eq!(config, Config { active: true, delayed: false, pooled: true });
let config: Config = from_key_values("active,pooled").unwrap();
assert_eq!(config, Config { active: true, delayed: false, pooled: true });
字符串可以带引号,这在需要包含逗号或括号时很有用,这些符号被视为未带引号字符串的分隔符。带引号的字符串还可以包含转义字符,其中任何在\
后面的字符都将按原样重复
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
path: String,
}
let config: Config = from_key_values(r#"path="/some/\"strange\"/pa,th""#).unwrap();
assert_eq!(config, Config { path: r#"/some/"strange"/pa,th"#.into() });
允许使用元组和向量,并且必须使用[
和]
指定
#[derive(Debug, PartialEq, Deserialize)]
struct Layout {
resolution: (u16, u16),
scanlines: Vec<u16>,
}
let layout: Layout = from_key_values("resolution=[320,200],scanlines=[0,64,128]").unwrap();
assert_eq!(layout, Layout { resolution: (320, 200), scanlines: vec![0, 64, 128] });
可以使用名称直接指定枚举。建议使用serde的rename_all
容器属性,以使用蛇形或短横线表示法进行解析。也可以使用serde的rename
和alias
字段属性来提供更短的值
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all="kebab-case")]
enum Mode {
Slow,
Fast,
#[serde(rename="ludicrous")]
LudicrousSpeed,
}
#[derive(Deserialize, PartialEq, Debug)]
struct Config {
mode: Mode,
}
let config: Config = from_key_values("mode=slow").unwrap();
assert_eq!(config, Config { mode: Mode::Slow });
let config: Config = from_key_values("mode=ludicrous").unwrap();
assert_eq!(config, Config { mode: Mode::LudicrousSpeed });
使用枚举与集合结合是一个很好的用法,它可以轻松地指定标志等。
#[derive(Deserialize, PartialEq, Eq, Debug, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
enum Flags {
Awesome,
Fluffy,
Transparent,
}
#[derive(Deserialize, PartialEq, Debug)]
struct TestStruct {
flags: BTreeSet<Flags>,
}
let res: TestStruct = from_key_values("flags=[awesome,fluffy]").unwrap();
assert_eq!(
res,
TestStruct {
flags: BTreeSet::from([Flags::Awesome, Flags::Fluffy]),
}
);
只取单个值的枚举可以使用flatten
字段属性,以便直接从其变体键推断出类型。
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all="kebab-case")]
enum Mode {
// Work with a local file.
File(String),
// Work with a remote URL.
Url(String),
}
#[derive(Deserialize, PartialEq, Debug)]
struct Config {
#[serde(flatten)]
mode: Mode,
}
let config: Config = from_key_values("file=/some/path").unwrap();
assert_eq!(config, Config { mode: Mode::File("/some/path".into()) });
let config: Config = from_key_values("url=https://www.google.com").unwrap();
assert_eq!(config, Config { mode: Mode::Url("https://www.google.com".into()) });
flatten
属性还可以用于在结构体中嵌入另一个结构体,并从同一字符串解析它们。
#[derive(Debug, PartialEq, Deserialize)]
struct BaseConfig {
enabled: bool,
num_threads: u8,
}
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
#[serde(flatten)]
base: BaseConfig,
path: String,
}
let config: Config = from_key_values("path=/some/path,enabled,num_threads=16").unwrap();
assert_eq!(
config,
Config {
path: "/some/path".into(),
base: BaseConfig {
num_threads: 16,
enabled: true,
}
}
);
如果枚举的变体由结构体组成,它可以使用untagged
容器属性来直接从嵌入的结构体的字段推断。
#[derive(Debug, PartialEq, Deserialize)]
#[serde(untagged)]
enum Mode {
// Work with a local file.
File {
path: String,
#[serde(default)]
read_only: bool,
},
// Work with a remote URL.
Remote {
server: String,
port: u16,
}
}
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
#[serde(flatten)]
mode: Mode,
}
let config: Config = from_key_values("path=/some/path").unwrap();
assert_eq!(config, Config { mode: Mode::File { path: "/some/path".into(), read_only: false } });
let config: Config = from_key_values("server=google.com,port=80").unwrap();
assert_eq!(config, Config { mode: Mode::Remote { server: "google.com".into(), port: 80 } });
使用此包,可以精确地报告解析错误以及无效或缺失的字段。
#[derive(Debug, PartialEq, Deserialize)]
struct Config {
path: String,
threads: u8,
active: bool,
}
let config = from_key_values::<Config>("path=/some/path,active=true").unwrap_err();
assert_eq!(format!("{}", config), "missing field `threads`");
大多数serde的容器和字段属性可以应用于您的配置结构体。最有用的包括deny_unknown_fields
,当遇到未知字段时报告错误,以及deserialize_with
,用于为特定字段使用自定义反序列化函数。
请注意,使用flatten
有一些严重的限制。因为反序列化器无法获取类型信息,它将尝试使用输入作为唯一提示来确定字段的类型。例如,任何数字都将返回为整型,如果解析的结构体实际上期望一个字符串作为数字,则将发生错误。结构体枚举也不能扁平化,并且根本不会被识别。
因此,除非嵌入的结构体或扁平化的结构体都没有字符串类型的成员,否则不建议使用flatten
。
大多数情况下,可以通过实现一个自定义反序列化器来获取类似的功能,该反序列化器直接使用key_values::KeyValueDeserializer
接口解析嵌入结构体的成员,然后按特定顺序解析扁平化的结构体。
使用flatten
的另一个限制是,它不允许在嵌入的结构体或扁平化的结构体中使用deny_unknown_fields
。
依赖项
~1.4–2.3MB
~46K SLoC