json-traits
Server-side helper for frontends that address JSON the way leaf-validator does, and that send a PATCH of the leaves that changed.
leaf-validator is the client library this crate was written to match. A screen binds a control to a dotted location such as person.contact.phoneNumber. Its leafDiff turns an edit into one entry per leaf, { location, updatedValue }, instead of one blob for a whole new object. The client sends that list as a PATCH. Concurrent editors then overwrite only the leaves they changed. Those locations are the same dotted paths MongoDB calls dot notation. mongo-leaf-validator-example shows a server applying them. leaf-validator encourages the normalized state shape: each value lives in one place, and an update names that place.
This crate is the Rust server helper for that PATCH. JsonElementExtensions is the .NET one. Both flatten a stored document into the same leaf paths the client uses, diff two documents, and match paths against patterns so the server can accept only the leaves a caller is allowed to change. * matches one path segment.
serde_json::Value stands in for .NET's JsonElement. The methods are traits. Import a trait and its methods are available on Value, the same way a using brings an extension method into scope.
use *;
use json;
let left = json!;
let right = json!;
// "prop1.prop2", "contacts.0.info.name", "contacts.1.info.number"
let _paths = left.paths_and_values;
// prop1.prop2 and contacts.1.info.number differ
let _diff = left.diff_with;
let patterns = ;
assert!;
Rust on this machine
Stable Rust is installed with rustup. New terminals pick it up from your shell profile. In a terminal that was already open, run:
On another machine:
|
This repository pins the stable toolchain in rust-toolchain.toml, including rustfmt and clippy. The first cargo command inside the directory installs that toolchain if it is missing.
Useful commands while learning the layout:
| Command | What it does |
|---|---|
cargo test |
Runs the unit, integration, and documentation tests |
cargo bench |
Runs the Criterion benchmarks |
cargo doc --open |
Builds the API docs and opens them |
cargo run --example list_paths < document.json |
Prints every leaf path in a file |
cargo fmt --all -- --check |
Checks formatting |
cargo clippy --all-targets --all-features -- -D warnings |
Lints the crate |
cargo package --all-features |
Builds the crate archive |
cargo publish --dry-run |
Checks that crates.io would accept the archive |
Add it to another project
Once version 0.1.0 is on crates.io:
[]
= "0.1"
= "1"
Until then, depend on the git repository:
[]
= { = "https://github.com/stewie1570/JsonTraits" }
= "1"
Bring the methods into scope with use json_traits::prelude::*;, or import JsonPaths, DiffJson, and PathPattern individually.
What the methods do
A leaf-validator PATCH is a list of leaf locations. These methods are how the server speaks that same shape.
paths_and_values walks a value and returns a BTreeMap<String, JsonScalar> of leaves. Object keys and array indexes become path segments:
prop1.prop2
contacts.0.info.name
contacts.1.info.number
contacts.2.info.isAwesome
A root scalar such as "value" is stored at the empty path "". Empty objects and empty arrays have no leaves, so they produce no paths.
diff_with compares those leaves. Each difference is (left, right). A path that exists on only one side uses JsonScalar::Undefined for the other side. That is the server's view of the same leaf diff the client would send. The method also works on the maps returned by paths_and_values, which is how you diff after filtering.
is_supported_by reports whether a PATCH path is one the server allows. is_a_path_match_with is called on the pattern, not the path:
"contacts.*.info.name".is_a_path_match_with;
* matches one whole segment. It does not match across dots, so contacts.*.info.name does not match contacts.0.info.name.last.
Behavior worth knowing
- Paths are sorted, because the map is a
BTreeMap. - Dots inside an object key are not escaped.
"a.b"and{"a":{"b": ...}}become the same path. That matches the C# library. - JSON
nullequals JSONnull. The C#DiffWithcalls.Equalson the boxed value and throws when that value is null; this port treats null as a real leaf. - Integer
1and decimal1.0are the same value. Two integers still compare exactly.
Checks before a release
.github/workflows/ci.yml gives each check its own job. Format, Clippy, tests, the list_paths example, and benchmarks start together. Packaging waits until format, Clippy, tests, and the example have succeeded. The packaged crate is tested in a later job. The publish dry run waits until that packaged test and the benchmarks have both succeeded.
.github/workflows/publish.yml runs when a pull request is merged into master. It reads version from Cargo.toml. If the tag v plus that version is not already on the repository, the version is new: the same checks run, then cargo publish, then the workflow pushes that tag. A later merge that leaves the version unchanged finds the tag and does not publish again.
Publish the crate
- Create the GitHub repository
stewie1570/JsonTraitsand push this project, including the workflow files. - Create a crates.io account and verify your email.
- Configure publishing in one of these ways:
- Trusted publishing (no long-lived token). On crates.io, add a trusted publisher for repository
stewie1570/JsonTraits, workflowpublish.yml, and environmentcrates-io. Create a GitHub environment namedcrates-ioon the repository. The workflow already requestsid-token: write. - API token. Create a crates.io token and save it as the
CARGO_REGISTRY_TOKENrepository secret.
- Trusted publishing (no long-lived token). On crates.io, add a trusted publisher for repository
- Merge a pull request into
master. The workflow publishes whenvplus theCargo.tomlversion is not already a tag. The first such merge publishes0.1.0and pushesv0.1.0. To release0.2.0later, changeversioninCargo.tomland merge that pull request.
A crates.io version cannot be overwritten. If a published version is broken, cargo yank --version 0.1.0 hides it from new dependents without deleting it.
Project map
src/lib.rs crate root
src/scalar.rs JsonScalar, one JSON leaf
src/flatten.rs JsonPaths::paths_and_values
src/diff.rs DiffJson::diff_with
src/path_pattern.rs PathPattern
tests/ the C# cases, ported, plus edge cases
benches/json_paths.rs Criterion benchmarks
examples/list_paths.rs read JSON from stdin, print paths
.github/workflows/ CI jobs and crates.io publish