4 个版本 (2 个重大更新)
0.3.0 | 2024 年 6 月 26 日 |
---|---|
0.2.1 | 2024 年 6 月 13 日 |
0.2.0 | 2024 年 2 月 22 日 |
0.1.0 | 2024 年 2 月 20 日 |
#306 在 解析器实现
75,305 每月下载量
用于 15 个 crate (3 个直接)
120KB
2.5K SLoC
与 toml
的区别
首先,我想坦率地说明这个 crate 与 toml
之间的区别/限制。
- 没有
serde
反序列化支持,虽然有一个serde
功能,但它仅启用Value
和Spanned
类型的序列化。 - 没有 toml 序列化。这个 crate 仅旨在作为保留跨度信息的反序列化器,没有打算为 toml 提供序列化,尤其是
toml-edit
提供的保留高级格式的序列化。 - 没有日期时间反序列化。很容易添加对此的支持(通过可选功能),但我目前没有这个需求。欢迎 PR。
这个 crate 为什么存在?
问题
这个 crate 是专门为了满足 cargo-deny 的需求而制作的,即它总能检索它想要的任何 toml 项的跨度。虽然 toml crate 也可以通过 toml::Spanned 生成跨度信息,但有一个相当显著的限制,即它必须通过 serde。虽然在简单的情况下,Spanned
类型工作得相当好,例如。
#[derive(serde::Deserialize)]
struct Simple {
/// This works just fine
simple_string: toml::Spanned<String>,
}
一旦你有一个 更复杂的情况,toml
用于获取跨度信息的机制就会失效。
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum Ohno {
Integer(u32),
SpannedString(toml::Spanned<String>),
}
#[derive(serde::Deserialize)]
struct Root {
integer: Ohno,
string: Ohno
}
fn main() {
let toml = r#"
integer = 42
string = "we want this to be spanned"
"#;
let parsed: Root = toml::from_str(toml).expect("failed to deserialize toml");
}
thread 'main' panicked at src/main.rs:20:45:
failed to deserialize toml: Error { inner: Error { inner: TomlError { message: "data did not match any variant of untagged enum Ohno", original: Some("\ninteger = 42\nstring = \"we want this to be spanned\"\n"), keys: ["string"], span: Some(23..51) } } }
为了了解为什么失败,我们可以看看 #[derive(serde::Deserialize)]
在 HIR 中对 Ohno
的扩展。
#[allow(unused_extern_crates, clippy :: useless_attribute)]
extern crate serde as _serde;
#[automatically_derived]
impl <'de> _serde::Deserialize<'de> for Ohno {
fn deserialize<__D>(__deserializer: __D)
-> _serde::__private::Result<Self, __D::Error> where
__D: _serde::Deserializer<'de> {
let __content =
match #[lang = "branch"](<_serde::__private::de::Content as
_serde::Deserialize>::deserialize(__deserializer)) {
#[lang = "Break"] { 0: residual } =>
#[allow(unreachable_code)]
return #[lang = "from_residual"](residual),
#[lang = "Continue"] { 0: val } =>
#[allow(unreachable_code)]
val,
};
let __deserializer =
_serde::__private::de::ContentRefDeserializer<, ,
__D::Error>::new(&__content);
if let _serde::__private::Ok(__ok) =
_serde::__private::Result::map(<u32 as
_serde::Deserialize>::deserialize(__deserializer),
Ohno::Integer) { return _serde::__private::Ok(__ok); }
if let _serde::__private::Ok(__ok) =
_serde::__private::Result::map(<toml::Spanned<String> as
_serde::Deserialize>::deserialize(__deserializer),
Ohno::SpannedString) { return _serde::__private::Ok(__ok); }
_serde::__private::Err(_serde::de::Error::custom("data did not match any variant of untagged enum Ohno"))
}
}
在未标记情况下,serde所做的首先是将数据反序列化到内部的API容器_serde::__private::de::Content
中,可以将其类比为serde_json::Value
。这是因为serde会推测性地解析每个枚举变体,直到通过传递一个ContentRefDeserializer
(该解序列化器仅从之前反序列化的Content
借用数据)来满足serde解序列化API的Deserializer
消费需求。问题源于toml::Spanned
的工作方式,即它使用一种技巧来规避serde API的限制,通过Spanned
对象特别请求一组键来“反序列化”项以及其跨度信息,以便将跨度信息编码为结构体以满足serde。但是,serde并不知道在反序列化Content
对象时,它只知道解序列化器报告它有一个字符串、整数或其他数据,并对其进行反序列化,“丢失”了跨度信息。这个问题也影响了诸如#[serde(flatten)]
等功能,原因略有不同,但它们基本上都归结为serde API并不真正支持跨度信息,也没有计划。
toml-span
的不同之处
这个crate通过完全不使用serde
来工作。该crate的核心是基于basic-toml的,而basic-toml本身是toml v0.5
的分支,在它增加了大量功能和复杂性之前,这些功能和复杂性……嗯,对于cargo-deny或其他只需要反序列化的crate来说并不是必需的。
移除serde
支持意味着虽然必须手动编写反序列化,这在某些情况下可能会很繁琐,但在将cargo-deny迁移的过程中,我实际上越来越欣赏这一点,原因有几个。
- 最大程度的控制。
toml-span
进行初始的反序列化遍历到toml_span::value::Value
,该遍历保留了键和值的跨度信息,并提供了一些辅助函数(特别是TableHelper
),但除了满足toml_span::Deserialize
特质外,它不会限制你如何反序列化你的值,而且如果你不想使用,你甚至可以不使用它。 - 虽然手动编写反序列化代码比仅仅添加几个serde属性要慢,但事实是,这种初始的便利性会带来编译时间的成本,包括
serde_derive
及其所有依赖项,以及所有生成的代码,永远都是如此。当你处于原型设计阶段时,这还可以接受,但一旦你的数据格式(主要是/某种程度上)稳定下来,就会变得非常浪费。 - (可选)基于Span的错误。
toml-span
提供了reporting
功能,可以启用以将toml_span::Error
转换为Diagnostic,如果你使用codespan-reporting
包,这将提供良好的错误输出。
用法
简单
toml-span
最简单的用法就是一个比toml
更精简的版本,它还提供了一个类似于serde_json的指针API,允许轻松地逐部分反序列化TOML文档。
toml
版本
fn is_crates_io_sparse(config: &toml::Value) -> Option<bool> {
config
.get("registries")
.and_then(|v| v.get("crates-io"))
.and_then(|v| v.get("protocol"))
.and_then(|v| v.as_str())
.and_then(|v| match v {
"sparse" => Some(true),
"git" => Some(false),
_ => None,
})
}
toml-span
版本
fn is_crates_io_sparse(config: &toml_span::Value) -> Option<bool> {
match config.pointer("/registries/crates-io/protocol").and_then(|p| p.as_str())? {
"sparse" => Some(true),
"git" => Some(false),
_ => None
}
}
常见
当然,最常见的用例是将TOML反序列化到Rust容器中。
toml
版本
#[derive(Deserialize, Clone)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct CrateBan {
pub name: Spanned<String>,
pub version: Option<VersionReq>,
/// One or more crates that will allow this crate to be used if it is a
/// direct dependency
pub wrappers: Option<Spanned<Vec<Spanned<String>>>>,
/// Setting this to true will only emit an error if multiple
// versions of the crate are found
pub deny_multiple_versions: Option<Spanned<bool>>,
}
toml-span
版本
以下代码在proc宏运行之前更为冗长,但它展示了将cargo-deny迁移到toml-span
允许的某种东西,即PackageSpec
。
在toml-span
之前,所有用户指定crate规范(即,名称+可选版本要求)的情况都是通过两个独立的字段完成的,分别是name
和version
。这相当冗长,因为在许多情况下,不仅version
没有指定,而且如果用户不需要/不想要提供其他字段,它可能只是一个字符串。通常人们会使用string or struct惯例,但由于我希望将数据重新组织为字符串或结构,并且将可选数据展平到与包规范相同的级别,这不可能实现。但是,由于toml-span
改变了反序列化的方式,在完成建立crate的初始工作之后,这种改变变得相当简单。
pub type CrateBan = PackageSpecOrExtended<CrateBanExtended>;
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct CrateBanExtended {
/// One or more crates that will allow this crate to be used if it is a
/// direct dependency
pub wrappers: Option<Spanned<Vec<Spanned<String>>>>,
/// Setting this to true will only emit an error if multiple versions of the
/// crate are found
pub deny_multiple_versions: Option<Spanned<bool>>,
/// The reason for banning the crate
pub reason: Option<Reason>,
/// The crate to use instead of the banned crate, could be just the crate name
/// or a URL
pub use_instead: Option<Spanned<String>>,
}
impl<'de> Deserialize<'de> for CrateBanExtended {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
// The table helper provides convenience wrappers around a Value::Table, which
// is just a BTreeMap<Key, Value>
let mut th = TableHelper::new(value)?;
// Since we specify the keys manually there is no need for serde(rename/rename_all)
let wrappers = th.optional("wrappers");
let deny_multiple_versions = th.optional("deny-multiple-versions");
let reason = th.optional_s("reason");
let use_instead = th.optional("use-instead");
// Specifying None means that any keys that still exist in the table are
// unknown, producing an error the same as with serde(deny_unknown_fields)
th.finalize(None)?;
Ok(Self {
wrappers,
deny_multiple_versions,
reason: reason.map(Reason::from),
use_instead,
})
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct PackageSpec {
pub name: Spanned<String>,
pub version_req: Option<VersionReq>,
}
impl<'de> Deserialize<'de> for PackageSpec {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
use std::borrow::Cow;
struct Ctx<'de> {
inner: Cow<'de, str>,
split: Option<(usize, bool)>,
span: Span,
}
impl<'de> Ctx<'de> {
fn from_str(bs: Cow<'de, str>, span: Span) -> Self {
let split = bs
.find('@')
.map(|i| (i, true))
.or_else(|| bs.find(':').map(|i| (i, false)));
Self {
inner: bs,
split,
span,
}
}
}
let ctx = match value.take() {
ValueInner::String(s) => Ctx::from_str(s, value.span),
ValueInner::Table(tab) => {
let mut th = TableHelper::from((tab, value.span));
if let Some(mut val) = th.table.remove(&"crate".into()) {
let s = val.take_string(Some("a crate spec"))?;
th.finalize(Some(value))?;
Ctx::from_str(s, val.span)
} else {
// Encourage user to use the 'crate' spec instead
let name = th.required("name").map_err(|e| {
if matches!(e.kind, toml_span::ErrorKind::MissingField(_)) {
(toml_span::ErrorKind::MissingField("crate"), e.span).into()
} else {
e
}
})?;
let version = th.optional::<Spanned<Cow<'_, str>>>("version");
// We return all the keys we haven't deserialized back to the value,
// so that further deserializers can use them as this spec is
// always embedded in a larger structure
th.finalize(Some(value))?;
let version_req = if let Some(vr) = version {
Some(vr.value.parse().map_err(|e: semver::Error| {
toml_span::Error::from((
toml_span::ErrorKind::Custom(e.to_string()),
vr.span,
))
})?)
} else {
None
};
return Ok(Self { name, version_req });
}
}
other => return Err(expected("a string or table", other, value.span).into()),
};
let (name, version_req) = if let Some((i, make_exact)) = ctx.split {
let mut v: VersionReq = ctx.inner[i + 1..].parse().map_err(|e: semver::Error| {
toml_span::Error::from((
toml_span::ErrorKind::Custom(e.to_string()),
Span::new(ctx.span.start + i + 1, ctx.span.end),
))
})?;
if make_exact {
if let Some(comp) = v.comparators.get_mut(0) {
comp.op = semver::Op::Exact;
}
}
(
Spanned::with_span(
ctx.inner[..i].into(),
Span::new(ctx.span.start, ctx.span.start + i),
),
Some(v),
)
} else {
(Spanned::with_span(ctx.inner.into(), ctx.span), None)
};
Ok(Self { name, version_req })
}
}
pub struct PackageSpecOrExtended<T> {
pub spec: PackageSpec,
pub inner: Option<T>,
}
impl<T> PackageSpecOrExtended<T> {
pub fn try_convert<V, E>(self) -> Result<PackageSpecOrExtended<V>, E>
where
V: TryFrom<T, Error = E>,
{
let inner = if let Some(i) = self.inner {
Some(V::try_from(i)?)
} else {
None
};
Ok(PackageSpecOrExtended {
spec: self.spec,
inner,
})
}
pub fn convert<V>(self) -> PackageSpecOrExtended<V>
where
V: From<T>,
{
PackageSpecOrExtended {
spec: self.spec,
inner: self.inner.map(V::from),
}
}
}
impl<'de, T> toml_span::Deserialize<'de> for PackageSpecOrExtended<T>
where
T: toml_span::Deserialize<'de>,
{
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let spec = PackageSpec::deserialize(value)?;
// If more keys exist in the table (or string) then try to deserialize
// the rest as the "extended" portion
let inner = if value.has_keys() {
Some(T::deserialize(value)?)
} else {
None
};
Ok(Self { spec, inner })
}
}
贡献
我们欢迎社区对这个项目的贡献。
请阅读我们的贡献指南以获取有关如何开始的更多信息。在您做出任何贡献之前,也请阅读我们的贡献条款。
任何有意提交以包含在Embark Studios项目中的贡献,都必须遵守Rust标准许可模型(MIT OR Apache 2.0),因此将按照以下方式双许可,没有任何额外的条款或条件
许可
此贡献根据以下任一项双许可
- Apache许可证第2版,(LICENSE-APACHE或https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT许可证(LICENSE-MIT或http://opensource.org/licenses/MIT)
任选其一。
为了明确起见,“你的”指的是Embark或任何其他贡献者/使用者。
依赖项
~0–7MB
~43K SLoC