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解析器实现

Download history 13737/week @ 2024-05-04 13218/week @ 2024-05-11 12773/week @ 2024-05-18 12338/week @ 2024-05-25 12126/week @ 2024-06-01 15138/week @ 2024-06-08 18185/week @ 2024-06-15 14032/week @ 2024-06-22 15549/week @ 2024-06-29 15028/week @ 2024-07-06 16120/week @ 2024-07-13 20589/week @ 2024-07-20 18148/week @ 2024-07-27 18756/week @ 2024-08-03 17987/week @ 2024-08-10 17021/week @ 2024-08-17

75,305 每月下载量
用于 15 个 crate (3 个直接)

MIT/Apache

120KB
2.5K SLoC

↔️ toml-跨度

保留跨度的 toml 反序列化器

Embark Embark Crates.io Docs dependency status Build status

toml 的区别

首先,我想坦率地说明这个 crate 与 toml 之间的区别/限制。

  1. 没有 serde 反序列化支持,虽然有一个 serde 功能,但它仅启用 ValueSpanned 类型的序列化。
  2. 没有 toml 序列化。这个 crate 仅旨在作为保留跨度信息的反序列化器,没有打算为 toml 提供序列化,尤其是 toml-edit 提供的保留高级格式的序列化。
  3. 没有日期时间反序列化。很容易添加对此的支持(通过可选功能),但我目前没有这个需求。欢迎 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迁移的过程中,我实际上越来越欣赏这一点,原因有几个。

  1. 最大程度的控制。 toml-span进行初始的反序列化遍历到toml_span::value::Value,该遍历保留了键和值的跨度信息,并提供了一些辅助函数(特别是TableHelper),但除了满足toml_span::Deserialize特质外,它不会限制你如何反序列化你的值,而且如果你不想使用,你甚至可以不使用它。
  2. 虽然手动编写反序列化代码比仅仅添加几个serde属性要慢,但事实是,这种初始的便利性会带来编译时间的成本,包括serde_derive及其所有依赖项,以及所有生成的代码,永远都是如此。当你处于原型设计阶段时,这还可以接受,但一旦你的数据格式(主要是/某种程度上)稳定下来,就会变得非常浪费。
  3. (可选)基于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规范(即,名称+可选版本要求)的情况都是通过两个独立的字段完成的,分别是nameversion。这相当冗长,因为在许多情况下,不仅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 })
    }
}

贡献

Contributor Covenant

我们欢迎社区对这个项目的贡献。

请阅读我们的贡献指南以获取有关如何开始的更多信息。在您做出任何贡献之前,也请阅读我们的贡献条款

任何有意提交以包含在Embark Studios项目中的贡献,都必须遵守Rust标准许可模型(MIT OR Apache 2.0),因此将按照以下方式双许可,没有任何额外的条款或条件

许可

此贡献根据以下任一项双许可

任选其一。

为了明确起见,“你的”指的是Embark或任何其他贡献者/使用者。

依赖项

~0–7MB
~43K SLoC