serde_cursor
This crate allows you to declaratively specify how to fetch the desired parts of a serde-compatible data format (such as JSON) efficiently, without loading it all into memory, using a jq-like language.
= "0.4"
Examples
The Cursor! macro makes it extremely easy to extract nested fields from data.
Get version from Cargo.toml
use Cursor;
let data = r#"
[workspace.package]
version = "0.1"
"#;
let version: String = >?.0;
assert_eq!;
Cursor!(workspace.package.version) is the magic juice - this type-macro expands to a type that implements serde::Deserialize.
Without serde_cursor:
Pain and sufferingโฆ
use Deserialize;
let data = r#"
[workspace.package]
version = "0.1"
"#;
let version = ?.workspace.package.version;
Get names of all dependencies from Cargo.lock
The index-all [] accesses every element in an array:
use Cursor;
let file = r#"
[[package]]
name = "serde"
[[package]]
name = "rand"
"#;
let packages: = >?.0;
assert_eq!;
Syntax
Specify the type Vec<String> after the path package[].name:
let packages = >?.0;
The type can be omitted, in which case it will be inferred:
let packages: = >?.0;
Fields that consist of identifiers and -s can be used without quotes:
Cursor!
Fields that contain spaces or other special characters must be quoted:
Cursor!
You can access specific elements of an array:
Cursor!
serde_cursor + monostate = ๐งก๐๐๐๐
The monostate crate provides the MustBe! macro, which returns a type that implements
serde::Deserialize, and can only ever deserialize from one specific value.
Together, these 2 crates provide an almost jq-like experience of data processing in Rust:
// early exit if the `reason` field is not equal to `"compiler-message"`
get!?;
get!?;
Ok
The jq version of the above processing looks like this:
select(.reason == "compiler-message")
| select(.message.message == "trace_macro")
| {
messages: [.message.children[].message],
byte_start: .message.spans[0].byte_start,
byte_end: .message.spans[0].byte_end
}
The full code for the above example looks like this:
use MustBe;
use Cursor;
For reference, the same logic without serde_cursor or monostate
use Deserialize;
Ranges
Ranges are like [] but for only for elements with an index that falls in the range:
Cursor!;
Cursor!;
Cursor!;
Cursor!;
Interpolations
Itโs not uncommon for multiple queries to get quite repetitive:
let pressure: = >?.0;
let humidity: = >?.0;
let temperature: = >?.0;
serde_cursor supports interpolations. You can factor out a common path into a type Details, and then interpolate it with $Details in the path inside Cursor!:
type Details<RestOfPath> = Path!;
let pressure: = >?.0;
let humidity: = >?.0;
let temperature: = >?.0;
serde_cursor vs serde_query
serde_query also implements jq-like queries, but more verbosely.
Single query
serde_cursor:
use Cursor;
let data = r#"{ "commits": [{"author": "Ferris"}] }"#;
let authors: = >?.0;
serde_query:
use Deserialize;
let data = r#"{ "commits": [{"author": "Ferris"}] }"#;
let data: Data = from_str?;
let authors = data.authors;
Storing queries in a struct
serde_cursor:
use Deserialize;
use Cursor;
let data = r#"{ "count": 1, "commits": [{"author": "Ferris"}] }"#;
let data: Data = from_str?;
serde_query:
use Deserialize;
let data = r#"{ "count": 1, "commits": [{"author": "Ferris"}] }"#;
let data: Data = from_str?;
Great error messages
When deserialization fails, you get the exact path of where the failure occurred:
use Cursor;
let data = json!;
let result = >;
let err = result.unwrap_err.to_string;
assert_eq!;
serde_with integration
If feature = "serde_with" is enabled, the type returned by Cursor! will implement serde_with::DeserializeAs and serde_with::SerializeAs,
meaning you can use it with the #[serde_as] attribute:
use ;
use Cursor;
let toml: CargoToml = from_str?;
assert_eq!;
assert_eq!;
How does it work?
The Cursor! macro expands to a recursive type that implements serde::Deserialize.
Information on how to access the nested fields is stored entirely inside the type system.
Consider this query, which gets the first dependency of every dependency in Cargo.toml:
Cursor!
For this Cargo.lock, it would extract ["libc", "find-msvc-tools"]:
[[]]
= "android_system_properties"
= ["libc"]
[[]]
= "cc"
= ["find-msvc-tools", "shlex"]
That macro is expanded into a Cursor type, which implements serde::Deserialize and serde::Serialize:
, // .package
, // .dependencies
,
>,
>,
>,
>
The above is essentially an equivalent to:
vec!
Except it exists entirely in the type system.
Each time the serde::Deserialize::deserialize() function is called,
the first segment of the path (.package) is processed, and the rest of the path ([].dependencies[0]) is passed to the
serde::Deserialize trait, again, and again - until the path is empty.
Once the path is empty, we finally get to the type of the field - the String in the above example,
and finally call serde::Deserialize::deserialize() on that, to finish things off -
this String is then bubbled up the stack and returned from <Cursor<String, _> as serde::Deserialize>::deserialize.