Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation #3

Merged
merged 4 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ rustflags = [
"-Wnonstandard_style",
"-Wrust_2018_idioms",
# END - Embark standard lints v6 for Rust 1.55+
"-Dmissing_docs",
]

[target.'cfg(target_env = "musl")']
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: cargo test
run: cargo test

deny-check:
deny:
name: cargo-deny
runs-on: ubuntu-22.04
steps:
Expand All @@ -60,3 +60,9 @@ jobs:
- run: cargo fetch
- name: cargo publish check
run: cargo publish --dry-run -p toml-span

test_success:
runs-on: ubuntu-22.04
needs: [lint,test,deny,publish-check]
steps:
- run: echo "All test jobs passed"
2 changes: 2 additions & 0 deletions integ-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(missing_docs)]

/// Loads a valid toml file and does a snapshot assertion against `toml`
#[macro_export]
macro_rules! valid {
Expand Down
4 changes: 4 additions & 0 deletions toml-span/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ reporting = ["dep:codespan-reporting"]
codespan-reporting = { version = "0.11", optional = true }
serde = { version = "1.0", optional = true }
smallvec = "1.13"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
18 changes: 9 additions & 9 deletions toml-span/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- Allow this file to not have a first line heading -->
<!-- markdownlint-disable-file MD041 no-emphasis-as-heading -->
<!-- markdownlint-disable-file MD041 no-emphasis-as-heading no-duplicate-heading -->

<!-- inline html -->
<!-- markdownlint-disable-file MD033 -->
Expand Down Expand Up @@ -32,7 +32,7 @@ First off I just want to be up front and clear about the differences/limitations

This crate was specifically made to suit the needs of [cargo-deny], namely, that it can always retrieve the span of any toml item that it wants to. While the [toml](https://docs.rs/toml/latest/toml/) crate can also produce span information via [toml::Spanned](https://docs.rs/toml/latest/toml/struct.Spanned.html) there is one rather significant limitation, namely, that it must pass through [serde](https://docs.rs/serde/latest/serde/). While in simple cases the `Spanned` type works quite well, eg.

```rust
```rust,ignore
#[derive(serde::Deserialize)]
struct Simple {
/// This works just fine
Expand All @@ -42,7 +42,7 @@ struct Simple {

As soon as you have a [more complicated scenario](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=aeb611bbe387538d2ebb6780055b3167), the mechanism that `toml` uses to get the span information breaks down.

```rust
```rust,ignore
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum Ohno {
Expand Down Expand Up @@ -73,7 +73,7 @@ failed to deserialize toml: Error { inner: Error { inner: TomlError { message: "

To understand why this fails we can look at what `#[derive(serde::Deserialize)]` expand to for `Ohno` in HIR.

```rust
```rust,ignore
#[allow(unused_extern_crates, clippy :: useless_attribute)]
extern crate serde as _serde;
#[automatically_derived]
Expand Down Expand Up @@ -103,8 +103,8 @@ impl <'de> _serde::Deserialize<'de> for Ohno {
_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"))
}
} };
}
}
```

What serde does in the untagged case is first deserialize into `_serde::__private::de::Content`, an internal API container that is easiest to think of as something like `serde_json::Value`. This is because serde speculatively parses each enum variant until one succeeds by passing a `ContentRefDeserializer` that just borrows the deserialized `Content` from earlier to satisfy the serde deserialize API consuming the `Deserializer`. The problem comes because of how [`toml::Spanned`](https://docs.rs/serde_spanned/0.6.5/src/serde_spanned/spanned.rs.html#161-212) works, namely that it uses a hack to workaround the limitations of the serde API in order to "deserialize" the item as well as its span information, by the `Spanned` object specifically requesting a set of keys from the `toml::Deserializer` impl so that it can [encode](https://github.com/toml-rs/toml/blob/c4b62fda23343037ebe5ea93db9393cb25fcf233/crates/toml_edit/src/de/spanned.rs#L27-L70) the span information as if it was a struct to satisfy serde. But serde doesn't know that when it deserializes the `Content` object, it just knows that the Deserializer reports it has a string, int or what have you, and deserializes that, "losing" the span information. This problem also affects things like `#[serde(flatten)]` for slightly different reasons, but they all basically come down to the serde API not truly supporting span information, nor [any plans](https://github.com/serde-rs/serde/issues/1811) to.
Expand All @@ -127,7 +127,7 @@ The most simple use case for `toml-span` is just as slimmer version of `toml` th

#### `toml` version

```rust
```rust,ignore
fn is_crates_io_sparse(config: &toml::Value) -> Option<bool> {
config
.get("registries")
Expand Down Expand Up @@ -160,7 +160,7 @@ Of course the most common case is deserializing toml into Rust containers.

#### `toml` version

```rust
```rust,ignore
#[derive(Deserialize, Clone)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
Expand All @@ -182,7 +182,7 @@ The following code is much more verbose (before proc macros run at least), but s

Before `toml-span`, all cases where a user specifies a crate spec, (ie, name + optional version requirement) was done via two separate fields, `name` and `version`. This was quite verbose, as in many cases not only is `version` not specified, but also could be just a string if the user doesn't need/want to provide other fields. Normally one would use the [string or struct](https://serde.rs/string-or-struct.html) idiom but this was impossible due to how I wanted to reorganize the data to have the package spec as either a string or struct, _as well as_ optional data that is flattened to the same level as the package spec. But since `toml-span` changes how deserialization is done, this change was quite trivial after the initial work of getting the crate stood up was done.

```rust
```rust,ignore
pub type CrateBan = PackageSpecOrExtended<CrateBanExtended>;

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
Expand Down
3 changes: 3 additions & 0 deletions toml-span/src/de.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Core deserialization logic that deserializes toml content to [`Value`]

use crate::{
error::{Error, ErrorKind},
tokens::{Error as TokenError, Token, Tokenizer},
Expand All @@ -14,6 +16,7 @@ type DeStr<'de> = Cow<'de, str>;
type TablePair<'de> = (Key<'de>, Val<'de>);
type InlineVec<T> = SmallVec<[T; 5]>;

/// Parses a toml string into a [`ValueInner::Table`]
pub fn parse(s: &str) -> Result<Value<'_>, Error> {
let mut de = Deserializer::new(s);

Expand Down
110 changes: 44 additions & 66 deletions toml-span/src/de_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
//! Provides helpers for deserializing [`Value`]/[`ValueInner`] into Rust types

use crate::{
span::Spanned,
value::{self, Table, Value, ValueInner},
DeserError, Deserialize, Error, ErrorKind, Span,
};
use std::{fmt::Display, str::FromStr};

/// Helper for construction an [`ErrorKind::Wanted`]
#[inline]
pub fn expected(expected: &'static str, found: ValueInner<'_>, span: Span) -> Error {
Error {
Expand All @@ -17,6 +20,8 @@ pub fn expected(expected: &'static str, found: ValueInner<'_>, span: Span) -> Er
}
}

/// Attempts to acquire a [`ValueInner::String`] and parse it, returning an error
/// if the value is not a string, or the parse implementation fails
#[inline]
pub fn parse<T, E>(value: &mut Value<'_>) -> Result<T, Error>
where
Expand All @@ -34,10 +39,17 @@ where
}
}

/// A helper for dealing with [`ValueInner::Table`]
pub struct TableHelper<'de> {
/// The table the helper is operating upon
pub table: Table<'de>,
/// The errors accumulated while deserializing
pub errors: Vec<Error>,
/// The list of keys that have been requested by the user, this is used to
/// show a list of keys that _could_ be used in the case the finalize method
/// fails due to keys still being present in the map
expected: Vec<&'static str>,
/// The span for the table location
span: Span,
}

Expand All @@ -53,6 +65,7 @@ impl<'de> From<(Table<'de>, Span)> for TableHelper<'de> {
}

impl<'de> TableHelper<'de> {
/// Creates a helper for the value, failing if it is not a table
pub fn new(value: &mut Value<'de>) -> Result<Self, DeserError> {
let table = match value.take() {
ValueInner::Table(table) => table,
Expand All @@ -67,21 +80,34 @@ impl<'de> TableHelper<'de> {
})
}

/// Returns true if the table contains the specified key
#[inline]
pub fn contains(&self, name: &'static str) -> bool {
pub fn contains(&self, name: &'de str) -> bool {
self.table.contains_key(&name.into())
}

/// Takes the specified key and its value if it exists
#[inline]
pub fn take(&mut self, name: &'static str) -> Option<(value::Key<'de>, Value<'de>)> {
self.expected.push(name);
self.table.remove_entry(&name.into())
}

/// Attempts to deserialize the specified key
///
/// Errors that occur when calling this method are automatically added to
/// the set of errors that are reported from [`Self::finalize`], so not early
/// returning if this method fails will still report the error by default
///
/// # Errors
/// - The key does not exist
/// - The [`Deserialize`] implementation for the type returns an error
#[inline]
pub fn required<T: Deserialize<'de>>(&mut self, name: &'static str) -> Result<T, Error> {
Ok(self.required_s(name)?.value)
}

/// The same as [`Self::required`], except it returns a [`Spanned`]
pub fn required_s<T: Deserialize<'de>>(
&mut self,
name: &'static str,
Expand All @@ -105,30 +131,16 @@ impl<'de> TableHelper<'de> {
})
}

pub fn with_default<T: Deserialize<'de>>(
&mut self,
name: &'static str,
def: impl FnOnce() -> T,
) -> (T, Span) {
self.expected.push(name);

let Some(mut val) = self.table.remove(&name.into()) else {
return (def(), Span::default());
};

match T::deserialize(&mut val) {
Ok(v) => (v, val.span),
Err(mut err) => {
self.errors.append(&mut err.errors);
(def(), Span::default())
}
}
}

/// Attempts to deserialize the specified key, if it exists
///
/// Note that if the key exists but deserialization fails, an error will be
/// appended and if [`Self::finalize`] is called it will return that error
/// along with any others that occurred
pub fn optional<T: Deserialize<'de>>(&mut self, name: &'static str) -> Option<T> {
self.optional_s(name).map(|v| v.value)
}

/// The same as [`Self::optional`], except it returns a [`Spanned`]
pub fn optional_s<T: Deserialize<'de>>(&mut self, name: &'static str) -> Option<Spanned<T>> {
self.expected.push(name);

Expand All @@ -145,51 +157,17 @@ impl<'de> TableHelper<'de> {
}
}

pub fn parse<T, E>(&mut self, name: &'static str) -> T
where
T: FromStr<Err = E> + Default,
E: Display,
{
self.expected.push(name);

let Some(mut val) = self.table.remove(&name.into()) else {
self.errors.push(Error {
kind: ErrorKind::MissingField(name),
span: self.span,
line_info: None,
});
return T::default();
};

match parse(&mut val) {
Ok(v) => v,
Err(err) => {
self.errors.push(err);
T::default()
}
}
}

pub fn parse_opt<T, E>(&mut self, name: &'static str) -> Option<T>
where
T: FromStr<Err = E>,
E: Display,
{
self.expected.push(name);

let Some(mut val) = self.table.remove(&name.into()) else {
return None;
};

match parse(&mut val) {
Ok(v) => Some(v),
Err(err) => {
self.errors.push(err);
None
}
}
}

/// Called when you are finished with this [`TableHelper`]
///
/// If errors have been accumulated when using this [`TableHelper`], this will
/// return an error with all of those errors.
///
/// Additionally, if [`Option::None`] is passed, any keys that still exist
/// in the table will be added to an [`ErrorKind::UnexpectedKeys`] error,
/// which can be considered equivalent to [`#[serde(deny_unknown_fields)]`](https://serde.rs/container-attrs.html#deny_unknown_fields)
///
/// If you want simulate [`#[serde(flatten)]`](https://serde.rs/field-attrs.html#flatten)
/// you can instead put that table back in its original value during this step
pub fn finalize(mut self, original: Option<&mut Value<'de>>) -> Result<(), DeserError> {
if let Some(original) = original {
original.set(ValueInner::Table(self.table));
Expand Down
Loading