mirror of
https://github.com/Start9Labs/patch-db.git
synced 2026-03-26 10:21:53 +00:00
audit fixes, repo restructure, and documentation
Soundness and performance audit (17 fixes): - See AUDIT.md for full details and @claude comments in code Repo restructure: - Inline json-ptr and json-patch submodules as regular directories - Remove cbor submodule, replace serde_cbor with ciborium - Rename patch-db/ -> core/, patch-db-macro/ -> macro/, patch-db-macro-internals/ -> macro-internals/, patch-db-util/ -> util/ - Purge upstream CI/CD, bench, and release cruft from json-patch - Remove .gitmodules Test fixes: - Fix proptest doesnt_crash (unique file paths, proper close/cleanup) - Add PatchDb::close() for clean teardown Documentation: - Add README.md, ARCHITECTURE.md, CONTRIBUTING.md, CLAUDE.md, AUDIT.md - Add TSDocs to TypeScript client exports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Submodule json-patch deleted from ba38c78e4d
4
json-patch/.gitignore
vendored
Normal file
4
json-patch/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/.idea/
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
27
json-patch/Cargo.toml
Normal file
27
json-patch/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "json-patch"
|
||||
version = "0.2.7-alpha.0"
|
||||
authors = ["Ivan Dubrov <dubrov.ivan@gmail.com>"]
|
||||
categories = []
|
||||
keywords = ["json", "json-patch"]
|
||||
description = "RFC 6902, JavaScript Object Notation (JSON) Patch"
|
||||
repository = "https://github.com/idubrov/json-patch"
|
||||
license = "MIT/Apache-2.0"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["diff"]
|
||||
nightly = []
|
||||
diff = []
|
||||
|
||||
[dependencies]
|
||||
imbl-value = "0.4.1"
|
||||
json-ptr = { path = "../json-ptr" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.9.1"
|
||||
serde_json = { version = "1.0.60", features = ["preserve_order"] }
|
||||
proptest = "1"
|
||||
imbl-value = { version = "0.4.1", features = ["arbitrary"] }
|
||||
92
json-patch/specs/merge_tests.json
Normal file
92
json-patch/specs/merge_tests.json
Normal file
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"comment": "1. introduction",
|
||||
"doc": {
|
||||
"a": "b",
|
||||
"c": {
|
||||
"d": "e",
|
||||
"f": "g"
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"a": "z",
|
||||
"c": {
|
||||
"f": null
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"a": "z",
|
||||
"c": {
|
||||
"d": "e"
|
||||
}
|
||||
},
|
||||
"merge": true
|
||||
},
|
||||
{
|
||||
"comment": "3. example",
|
||||
"doc": {
|
||||
"title": "Goodbye!",
|
||||
"author": {
|
||||
"givenName": "John",
|
||||
"familyName": "Doe"
|
||||
},
|
||||
"tags": [
|
||||
"example",
|
||||
"sample"
|
||||
],
|
||||
"content": "This will be unchanged"
|
||||
},
|
||||
"patch": {
|
||||
"title": "Hello!",
|
||||
"phoneNumber": "+01-123-456-7890",
|
||||
"author": {
|
||||
"familyName": null
|
||||
},
|
||||
"tags": [
|
||||
"example"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"title": "Hello!",
|
||||
"author": {
|
||||
"givenName": "John"
|
||||
},
|
||||
"tags": [
|
||||
"example"
|
||||
],
|
||||
"content": "This will be unchanged",
|
||||
"phoneNumber": "+01-123-456-7890"
|
||||
},
|
||||
"merge": true
|
||||
},
|
||||
{
|
||||
"comment": "replacing non-object",
|
||||
"doc": {
|
||||
"title": "Goodbye!",
|
||||
"author": {
|
||||
"givenName": "John"
|
||||
},
|
||||
"tags": [
|
||||
"example",
|
||||
"sample"
|
||||
],
|
||||
"content": "This will be unchanged"
|
||||
},
|
||||
"patch": {
|
||||
"tags": {
|
||||
"kind": "example"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"title": "Goodbye!",
|
||||
"author": {
|
||||
"givenName": "John"
|
||||
},
|
||||
"tags": {
|
||||
"kind": "example"
|
||||
},
|
||||
"content": "This will be unchanged"
|
||||
},
|
||||
"merge": true
|
||||
}
|
||||
]
|
||||
286
json-patch/specs/revert_tests.json
Normal file
286
json-patch/specs/revert_tests.json
Normal file
@@ -0,0 +1,286 @@
|
||||
[
|
||||
{
|
||||
"comment": "Can revert add (replace key)",
|
||||
"doc": {
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/foo",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert add (insert into array)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/foo/1",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert add (insert last element into array)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/foo/-",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert remove (object)",
|
||||
"doc": {
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo"
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert remove (array)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/1"
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert replace (replace key)",
|
||||
"doc": {
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/foo",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert replace (replace array element)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/foo/1",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert move (move into key)",
|
||||
"doc": {
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
},
|
||||
"abc": {
|
||||
"def": {
|
||||
"ghi": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "move",
|
||||
"from": "/abc",
|
||||
"path": "/foo",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert move (move into array)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3],
|
||||
"abc": {
|
||||
"def": {
|
||||
"ghi": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "move",
|
||||
"path": "/foo/1",
|
||||
"from": "/abc"
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert move (move into last element of an array)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3],
|
||||
"abc": {
|
||||
"def": {
|
||||
"ghi": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "move",
|
||||
"path": "/foo/-",
|
||||
"from": "/abc"
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert copy (copy into key)",
|
||||
"doc": {
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
},
|
||||
"abc": {
|
||||
"def": {
|
||||
"ghi": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "copy",
|
||||
"from": "/abc",
|
||||
"path": "/foo",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert copy (copy into array)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3],
|
||||
"abc": {
|
||||
"def": {
|
||||
"ghi": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "copy",
|
||||
"path": "/foo/1",
|
||||
"from": "/abc"
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
},
|
||||
{
|
||||
"comment": "Can revert copy (copy into last element of an array)",
|
||||
"doc": {
|
||||
"foo": [1, 2, 3],
|
||||
"abc": {
|
||||
"def": {
|
||||
"ghi": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "copy",
|
||||
"path": "/foo/-",
|
||||
"from": "/abc"
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
],
|
||||
"error": "invalid pointer"
|
||||
}
|
||||
]
|
||||
343
json-patch/specs/spec_tests.json
Normal file
343
json-patch/specs/spec_tests.json
Normal file
@@ -0,0 +1,343 @@
|
||||
[
|
||||
{
|
||||
"comment": "4.1. add with missing object",
|
||||
"doc": {
|
||||
"q": {
|
||||
"bar": 2
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/a/b",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"error": "path /a does not exist -- missing objects are not created recursively"
|
||||
},
|
||||
{
|
||||
"comment": "A.1. Adding an Object Member",
|
||||
"doc": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/baz",
|
||||
"value": "qux"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"baz": "qux",
|
||||
"foo": "bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.2. Adding an Array Element",
|
||||
"doc": {
|
||||
"foo": [
|
||||
"bar",
|
||||
"baz"
|
||||
]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/foo/1",
|
||||
"value": "qux"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": [
|
||||
"bar",
|
||||
"qux",
|
||||
"baz"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.3. Removing an Object Member",
|
||||
"doc": {
|
||||
"baz": "qux",
|
||||
"foo": "bar"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/baz"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": "bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.4. Removing an Array Element",
|
||||
"doc": {
|
||||
"foo": [
|
||||
"bar",
|
||||
"qux",
|
||||
"baz"
|
||||
]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/foo/1"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": [
|
||||
"bar",
|
||||
"baz"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.5. Replacing a Value",
|
||||
"doc": {
|
||||
"baz": "qux",
|
||||
"foo": "bar"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/baz",
|
||||
"value": "boo"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"baz": "boo",
|
||||
"foo": "bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.6. Moving a Value",
|
||||
"doc": {
|
||||
"foo": {
|
||||
"bar": "baz",
|
||||
"waldo": "fred"
|
||||
},
|
||||
"qux": {
|
||||
"corge": "grault"
|
||||
}
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "move",
|
||||
"from": "/foo/waldo",
|
||||
"path": "/qux/thud"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
},
|
||||
"qux": {
|
||||
"corge": "grault",
|
||||
"thud": "fred"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.7. Moving an Array Element",
|
||||
"doc": {
|
||||
"foo": [
|
||||
"all",
|
||||
"grass",
|
||||
"cows",
|
||||
"eat"
|
||||
]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "move",
|
||||
"from": "/foo/1",
|
||||
"path": "/foo/3"
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": [
|
||||
"all",
|
||||
"cows",
|
||||
"eat",
|
||||
"grass"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.8. Testing a Value: Success",
|
||||
"doc": {
|
||||
"baz": "qux",
|
||||
"foo": [
|
||||
"a",
|
||||
2,
|
||||
"c"
|
||||
]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/baz",
|
||||
"value": "qux"
|
||||
},
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/foo/1",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"baz": "qux",
|
||||
"foo": [
|
||||
"a",
|
||||
2,
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.9. Testing a Value: Error",
|
||||
"doc": {
|
||||
"baz": "qux"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/baz",
|
||||
"value": "bar"
|
||||
}
|
||||
],
|
||||
"error": "string not equivalent"
|
||||
},
|
||||
{
|
||||
"comment": "A.10. Adding a nested Member Object",
|
||||
"doc": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/child",
|
||||
"value": {
|
||||
"grandchild": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": "bar",
|
||||
"child": {
|
||||
"grandchild": {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.11. Ignoring Unrecognized Elements",
|
||||
"doc": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/baz",
|
||||
"value": "qux",
|
||||
"xyz": 123
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": "bar",
|
||||
"baz": "qux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.12. Adding to a Non-existent Target",
|
||||
"doc": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/baz/bat",
|
||||
"value": "qux"
|
||||
}
|
||||
],
|
||||
"error": "add to a non-existent target"
|
||||
},
|
||||
{
|
||||
"comment": "A.13 Invalid JSON Patch Document",
|
||||
"doc": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/baz",
|
||||
"value": "qux",
|
||||
"op": "remove"
|
||||
}
|
||||
],
|
||||
"error": "operation has two 'op' members",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"comment": "A.14. ~ Escape Ordering",
|
||||
"doc": {
|
||||
"/": 9,
|
||||
"~1": 10
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/~01",
|
||||
"value": 10
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"/": 9,
|
||||
"~1": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A.15. Comparing Strings and Numbers",
|
||||
"doc": {
|
||||
"/": 9,
|
||||
"~1": 10
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/~01",
|
||||
"value": "10"
|
||||
}
|
||||
],
|
||||
"error": "number is not equal to string"
|
||||
},
|
||||
{
|
||||
"comment": "A.16. Adding an Array Value",
|
||||
"doc": {
|
||||
"foo": [
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
"patch": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/foo/-",
|
||||
"value": [
|
||||
"abc",
|
||||
"def"
|
||||
]
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"foo": [
|
||||
"bar",
|
||||
[
|
||||
"abc",
|
||||
"def"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
1877
json-patch/specs/tests.json
Normal file
1877
json-patch/specs/tests.json
Normal file
File diff suppressed because it is too large
Load Diff
293
json-patch/src/diff.rs
Normal file
293
json-patch/src/diff.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use imbl_value::Value;
|
||||
use json_ptr::JsonPointer;
|
||||
|
||||
use crate::{AddOperation, PatchOperation, RemoveOperation, ReplaceOperation};
|
||||
|
||||
struct PatchDiffer {
|
||||
path: JsonPointer,
|
||||
patch: super::Patch,
|
||||
}
|
||||
|
||||
impl PatchDiffer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
path: JsonPointer::default(),
|
||||
patch: super::Patch(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Diff two JSON documents and generate a JSON Patch (RFC 6902).
|
||||
///
|
||||
/// # Example
|
||||
/// Diff two JSONs:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[macro_use]
|
||||
/// extern crate imbl_value;
|
||||
/// extern crate json_patch;
|
||||
///
|
||||
/// use json_patch::{patch, diff, from_value};
|
||||
///
|
||||
/// # pub fn main() {
|
||||
/// let left = json!({
|
||||
/// "title": "Goodbye!",
|
||||
/// "author" : {
|
||||
/// "givenName" : "John",
|
||||
/// "familyName" : "Doe"
|
||||
/// },
|
||||
/// "tags":[ "example", "sample" ],
|
||||
/// "content": "This will be unchanged"
|
||||
/// });
|
||||
///
|
||||
/// let right = json!({
|
||||
/// "title": "Hello!",
|
||||
/// "author" : {
|
||||
/// "givenName" : "John"
|
||||
/// },
|
||||
/// "tags": [ "example" ],
|
||||
/// "content": "This will be unchanged",
|
||||
/// "phoneNumber": "+01-123-456-7890"
|
||||
/// });
|
||||
///
|
||||
/// let p = diff(&left, &right);
|
||||
/// assert_eq!(p, from_value(json!([
|
||||
/// { "op": "remove", "path": "/author/familyName" },
|
||||
/// { "op": "add", "path": "/phoneNumber", "value": "+01-123-456-7890" },
|
||||
/// { "op": "remove", "path": "/tags/1" },
|
||||
/// { "op": "replace", "path": "/title", "value": "Hello!" },
|
||||
/// ])).unwrap());
|
||||
///
|
||||
/// let mut doc = left.clone();
|
||||
/// patch(&mut doc, &p).unwrap();
|
||||
/// assert_eq!(doc, right);
|
||||
///
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn diff(from: &Value, to: &Value) -> super::Patch {
|
||||
let mut differ = PatchDiffer::new();
|
||||
diff_mut(&mut differ, from, to);
|
||||
differ.patch
|
||||
}
|
||||
|
||||
fn diff_mut(differ: &mut PatchDiffer, from: &Value, to: &Value) {
|
||||
match (from, to) {
|
||||
(Value::Object(f), Value::Object(t)) if !f.ptr_eq(t) => {
|
||||
for key in f
|
||||
.keys()
|
||||
.chain(t.keys())
|
||||
.map(|k| &**k)
|
||||
.collect::<BTreeSet<_>>()
|
||||
{
|
||||
differ.path.push_end(key);
|
||||
match (f.get(key), to.get(key)) {
|
||||
(Some(f), Some(t)) if f != t => {
|
||||
diff_mut(differ, f, t);
|
||||
}
|
||||
(Some(_), None) => {
|
||||
differ.patch.0.push(PatchOperation::Remove(RemoveOperation {
|
||||
path: differ.path.clone(),
|
||||
}));
|
||||
}
|
||||
(None, Some(t)) => {
|
||||
differ.patch.0.push(PatchOperation::Add(AddOperation {
|
||||
path: differ.path.clone(),
|
||||
value: t.clone(),
|
||||
}));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
differ.path.pop_end();
|
||||
}
|
||||
}
|
||||
(Value::Array(f), Value::Array(t)) if !f.ptr_eq(t) => {
|
||||
if f.len() < t.len() {
|
||||
let mut f_idx = 0;
|
||||
let mut t_idx = 0;
|
||||
while t_idx < t.len() {
|
||||
if f_idx == f.len() {
|
||||
differ.patch.0.push(PatchOperation::Add(AddOperation {
|
||||
path: differ.path.clone().join_end_idx(t_idx),
|
||||
value: t[t_idx].clone(),
|
||||
}));
|
||||
t_idx += 1;
|
||||
} else {
|
||||
if !f[f_idx].ptr_eq(&t[t_idx]) {
|
||||
if t.iter().skip(t_idx + 1).any(|t| f[f_idx].ptr_eq(t)) {
|
||||
differ.patch.0.push(PatchOperation::Add(AddOperation {
|
||||
path: differ.path.clone().join_end_idx(t_idx),
|
||||
value: t[t_idx].clone(),
|
||||
}));
|
||||
t_idx += 1;
|
||||
continue;
|
||||
} else {
|
||||
differ.path.push_end_idx(t_idx);
|
||||
diff_mut(differ, &f[f_idx], &t[t_idx]);
|
||||
differ.path.pop_end();
|
||||
}
|
||||
}
|
||||
f_idx += 1;
|
||||
t_idx += 1;
|
||||
}
|
||||
}
|
||||
while f_idx < f.len() {
|
||||
differ.patch.0.push(PatchOperation::Remove(RemoveOperation {
|
||||
path: differ.path.clone().join_end_idx(t_idx),
|
||||
}));
|
||||
f_idx += 1;
|
||||
}
|
||||
} else if f.len() > t.len() {
|
||||
let mut f_idx = 0;
|
||||
let mut t_idx = 0;
|
||||
while f_idx < f.len() {
|
||||
if t_idx == t.len() {
|
||||
differ.patch.0.push(PatchOperation::Remove(RemoveOperation {
|
||||
path: differ.path.clone().join_end_idx(t_idx),
|
||||
}));
|
||||
f_idx += 1;
|
||||
} else {
|
||||
if !f[f_idx].ptr_eq(&t[t_idx]) {
|
||||
if f.iter().skip(f_idx + 1).any(|f| t[t_idx].ptr_eq(f)) {
|
||||
differ.patch.0.push(PatchOperation::Remove(RemoveOperation {
|
||||
path: differ.path.clone().join_end_idx(t_idx),
|
||||
}));
|
||||
f_idx += 1;
|
||||
continue;
|
||||
} else {
|
||||
differ.path.push_end_idx(t_idx);
|
||||
diff_mut(differ, &f[f_idx], &t[t_idx]);
|
||||
differ.path.pop_end();
|
||||
}
|
||||
}
|
||||
f_idx += 1;
|
||||
t_idx += 1;
|
||||
}
|
||||
}
|
||||
while t_idx < t.len() {
|
||||
differ.patch.0.push(PatchOperation::Add(AddOperation {
|
||||
path: differ.path.clone().join_end_idx(t_idx),
|
||||
value: t[t_idx].clone(),
|
||||
}));
|
||||
t_idx += 1;
|
||||
}
|
||||
} else {
|
||||
for i in 0..f.len() {
|
||||
if !f[i].ptr_eq(&t[i]) {
|
||||
differ.path.push_end_idx(i);
|
||||
diff_mut(differ, &f[i], &t[i]);
|
||||
differ.path.pop_end();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(f, t) if f != t => differ
|
||||
.patch
|
||||
.0
|
||||
.push(PatchOperation::Replace(ReplaceOperation {
|
||||
path: differ.path.clone(),
|
||||
value: t.clone(),
|
||||
})),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use imbl_value::Value;
|
||||
|
||||
#[test]
|
||||
pub fn replace_all() {
|
||||
let left = json!({"title": "Hello!"});
|
||||
let p = super::diff(&left, &Value::Null);
|
||||
assert_eq!(
|
||||
p,
|
||||
imbl_value::from_value(json!([
|
||||
{ "op": "replace", "path": "", "value": null },
|
||||
]))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn add_all() {
|
||||
let right = json!({"title": "Hello!"});
|
||||
let p = super::diff(&Value::Null, &right);
|
||||
assert_eq!(
|
||||
p,
|
||||
imbl_value::from_value(json!([
|
||||
{ "op": "replace", "path": "", "value": { "title": "Hello!" } },
|
||||
]))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn remove_all() {
|
||||
let left = json!(["hello", "bye"]);
|
||||
let right = json!([]);
|
||||
let p = super::diff(&left, &right);
|
||||
assert_eq!(
|
||||
p,
|
||||
imbl_value::from_value(json!([
|
||||
{ "op": "remove", "path": "/0" },
|
||||
{ "op": "remove", "path": "/0" },
|
||||
]))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn remove_tail() {
|
||||
let left = json!(["hello", "bye", "hi"]);
|
||||
let right = json!(["hello"]);
|
||||
let p = super::diff(&left, &right);
|
||||
assert_eq!(
|
||||
p,
|
||||
imbl_value::from_value(json!([
|
||||
{ "op": "remove", "path": "/1" },
|
||||
{ "op": "remove", "path": "/1" },
|
||||
]))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
pub fn replace_object() {
|
||||
let left = json!(["hello", "bye"]);
|
||||
let right = json!({"hello": "bye"});
|
||||
let p = super::diff(&left, &right);
|
||||
assert_eq!(
|
||||
p,
|
||||
imbl_value::from_value(json!([
|
||||
{ "op": "replace", "path": "", "value": &right },
|
||||
]))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_json_keys() {
|
||||
let mut left = json!({
|
||||
"/slashed/path": 1
|
||||
});
|
||||
let right = json!({
|
||||
"/slashed/path": 2,
|
||||
});
|
||||
let patch = super::diff(&left, &right);
|
||||
|
||||
eprintln!("{:?}", patch);
|
||||
|
||||
crate::patch(&mut left, &patch).unwrap();
|
||||
assert_eq!(left, right);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn test_diff(mut from: Value, to: Value) {
|
||||
let patch = super::diff(&from, &to);
|
||||
crate::patch(&mut from, &patch).unwrap();
|
||||
assert_eq!(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
603
json-patch/src/lib.rs
Normal file
603
json-patch/src/lib.rs
Normal file
@@ -0,0 +1,603 @@
|
||||
//! A [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902) and
|
||||
//! [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/html/rfc7396) implementation for Rust.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Add this to your *Cargo.toml*:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! json-patch = "*"
|
||||
//! ```
|
||||
//!
|
||||
//! # Examples
|
||||
//! Create and patch document using JSON Patch:
|
||||
//!
|
||||
//! ```rust
|
||||
//! #[macro_use]
|
||||
//! extern crate imbl_value;
|
||||
//! extern crate json_patch;
|
||||
//!
|
||||
//! use json_patch::patch;
|
||||
//! use serde_json::from_str;
|
||||
//!
|
||||
//! # pub fn main() {
|
||||
//! let mut doc = json!([
|
||||
//! { "name": "Andrew" },
|
||||
//! { "name": "Maxim" }
|
||||
//! ]);
|
||||
//!
|
||||
//! let p = from_str(r#"[
|
||||
//! { "op": "test", "path": "/0/name", "value": "Andrew" },
|
||||
//! { "op": "add", "path": "/0/happy", "value": true }
|
||||
//! ]"#).unwrap();
|
||||
//!
|
||||
//! patch(&mut doc, &p).unwrap();
|
||||
//! assert_eq!(doc, json!([
|
||||
//! { "name": "Andrew", "happy": true },
|
||||
//! { "name": "Maxim" }
|
||||
//! ]));
|
||||
//!
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Create and patch document using JSON Merge Patch:
|
||||
//!
|
||||
//! ```rust
|
||||
//! #[macro_use]
|
||||
//! extern crate imbl_value;
|
||||
//! extern crate json_patch;
|
||||
//!
|
||||
//! use json_patch::merge;
|
||||
//!
|
||||
//! # pub fn main() {
|
||||
//! let mut doc = json!({
|
||||
//! "title": "Goodbye!",
|
||||
//! "author" : {
|
||||
//! "givenName" : "John",
|
||||
//! "familyName" : "Doe"
|
||||
//! },
|
||||
//! "tags":[ "example", "sample" ],
|
||||
//! "content": "This will be unchanged"
|
||||
//! });
|
||||
//!
|
||||
//! let patch = json!({
|
||||
//! "title": "Hello!",
|
||||
//! "phoneNumber": "+01-123-456-7890",
|
||||
//! "author": {
|
||||
//! "familyName": null
|
||||
//! },
|
||||
//! "tags": [ "example" ]
|
||||
//! });
|
||||
//!
|
||||
//! merge(&mut doc, &patch);
|
||||
//! assert_eq!(doc, json!({
|
||||
//! "title": "Hello!",
|
||||
//! "author" : {
|
||||
//! "givenName" : "John"
|
||||
//! },
|
||||
//! "tags": [ "example" ],
|
||||
//! "content": "This will be unchanged",
|
||||
//! "phoneNumber": "+01-123-456-7890"
|
||||
//! }));
|
||||
//! # }
|
||||
//! ```
|
||||
#![deny(warnings)]
|
||||
#![warn(missing_docs)]
|
||||
#[cfg_attr(test, macro_use)]
|
||||
extern crate imbl_value;
|
||||
|
||||
use imbl_value::{InOMap as Map, Value};
|
||||
use json_ptr::{JsonPointer, SegList};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use std::{fmt, mem};
|
||||
|
||||
/// Representation of JSON Patch (list of patch operations)
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct Patch(pub Vec<PatchOperation>);
|
||||
impl Patch {
|
||||
/// Prepend a path to a patch.
|
||||
/// This is useful if you run a diff on a JSON document that is a small member of a larger document
|
||||
pub fn prepend<S: AsRef<str>, V: SegList>(&mut self, ptr: &JsonPointer<S, V>) {
|
||||
for op in self.0.iter_mut() {
|
||||
match op {
|
||||
PatchOperation::Add(ref mut op) => {
|
||||
op.path.prepend(ptr);
|
||||
}
|
||||
PatchOperation::Remove(ref mut op) => {
|
||||
op.path.prepend(ptr);
|
||||
}
|
||||
PatchOperation::Replace(ref mut op) => {
|
||||
op.path.prepend(ptr);
|
||||
}
|
||||
PatchOperation::Move(ref mut op) => {
|
||||
op.path.prepend(ptr);
|
||||
op.from.prepend(ptr);
|
||||
}
|
||||
PatchOperation::Copy(ref mut op) => {
|
||||
op.path.prepend(ptr);
|
||||
op.from.prepend(ptr);
|
||||
}
|
||||
PatchOperation::Test(ref mut op) => {
|
||||
op.path.prepend(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Checks whether or not the data at a path could be affected by a patch
|
||||
pub fn affects_path<S: AsRef<str>, V: SegList>(&self, ptr: &JsonPointer<S, V>) -> bool {
|
||||
for op in self.0.iter() {
|
||||
match op {
|
||||
PatchOperation::Add(ref op) => {
|
||||
if op.path.starts_with(ptr) || ptr.starts_with(&op.path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
PatchOperation::Remove(ref op) => {
|
||||
if op.path.starts_with(ptr) || ptr.starts_with(&op.path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
PatchOperation::Replace(ref op) => {
|
||||
if op.path.starts_with(ptr) || ptr.starts_with(&op.path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
PatchOperation::Move(ref op) => {
|
||||
if op.path.starts_with(ptr)
|
||||
|| ptr.starts_with(&op.path)
|
||||
|| op.from.starts_with(ptr)
|
||||
|| ptr.starts_with(&op.from)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
PatchOperation::Copy(ref op) => {
|
||||
if op.path.starts_with(ptr) || ptr.starts_with(&op.path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
PatchOperation::Test(_) => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
/// Returns whether the patch is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON Patch 'add' operation representation
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct AddOperation {
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// within the target document where the operation is performed.
|
||||
pub path: JsonPointer<String>,
|
||||
/// Value to add to the target location.
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// JSON Patch 'remove' operation representation
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct RemoveOperation {
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// within the target document where the operation is performed.
|
||||
pub path: JsonPointer<String>,
|
||||
}
|
||||
|
||||
/// JSON Patch 'replace' operation representation
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct ReplaceOperation {
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// within the target document where the operation is performed.
|
||||
pub path: JsonPointer<String>,
|
||||
/// Value to replace with.
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// JSON Patch 'move' operation representation
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct MoveOperation {
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// to move value from.
|
||||
pub from: JsonPointer<String>,
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// within the target document where the operation is performed.
|
||||
pub path: JsonPointer<String>,
|
||||
}
|
||||
|
||||
/// JSON Patch 'copy' operation representation
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct CopyOperation {
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// to copy value from.
|
||||
pub from: JsonPointer<String>,
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// within the target document where the operation is performed.
|
||||
pub path: JsonPointer<String>,
|
||||
}
|
||||
|
||||
/// JSON Patch 'test' operation representation
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct TestOperation {
|
||||
/// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location
|
||||
/// within the target document where the operation is performed.
|
||||
pub path: JsonPointer<String>,
|
||||
/// Value to test against.
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// JSON Patch single patch operation
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(tag = "op")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PatchOperation {
|
||||
/// 'add' operation
|
||||
Add(AddOperation),
|
||||
/// 'remove' operation
|
||||
Remove(RemoveOperation),
|
||||
/// 'replace' operation
|
||||
Replace(ReplaceOperation),
|
||||
/// 'move' operation
|
||||
Move(MoveOperation),
|
||||
/// 'copy' operation
|
||||
Copy(CopyOperation),
|
||||
/// 'test' operation
|
||||
Test(TestOperation),
|
||||
}
|
||||
|
||||
/// This type represents all possible errors that can occur when applying JSON patch
|
||||
#[derive(Debug)]
|
||||
pub enum PatchError {
|
||||
/// One of the pointers in the patch is invalid
|
||||
InvalidPointer,
|
||||
|
||||
/// 'test' operation failed
|
||||
TestFailed,
|
||||
}
|
||||
|
||||
impl Error for PatchError {}
|
||||
|
||||
impl fmt::Display for PatchError {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
PatchError::InvalidPointer => write!(fmt, "invalid pointer"),
|
||||
PatchError::TestFailed => write!(fmt, "test failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add<S: AsRef<str>, V: SegList>(
|
||||
doc: &mut Value,
|
||||
path: &JsonPointer<S, V>,
|
||||
value: Value,
|
||||
) -> Result<Option<Value>, PatchError> {
|
||||
path.insert(doc, value, false)
|
||||
.map_err(|_| PatchError::InvalidPointer)
|
||||
}
|
||||
|
||||
fn remove<S: AsRef<str>, V: SegList>(
|
||||
doc: &mut Value,
|
||||
path: &JsonPointer<S, V>,
|
||||
allow_last: bool,
|
||||
) -> Result<Value, PatchError> {
|
||||
path.remove(doc, allow_last)
|
||||
.ok_or(PatchError::InvalidPointer)
|
||||
}
|
||||
|
||||
fn replace<S: AsRef<str>, V: SegList>(
|
||||
doc: &mut Value,
|
||||
path: &JsonPointer<S, V>,
|
||||
value: Value,
|
||||
) -> Result<Value, PatchError> {
|
||||
if let Some(target) = path.get_mut(doc) {
|
||||
Ok(mem::replace(target, value))
|
||||
} else {
|
||||
Ok(add(doc, path, value)?.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
fn mov<S0: AsRef<str>, S1: AsRef<str>>(
|
||||
doc: &mut Value,
|
||||
from: &JsonPointer<S0>,
|
||||
path: &JsonPointer<S1>,
|
||||
allow_last: bool,
|
||||
) -> Result<Option<Value>, PatchError> {
|
||||
if path == from {
|
||||
return Ok(None);
|
||||
}
|
||||
// Check we are not moving inside own child
|
||||
if path.starts_with(from) || from.is_empty() {
|
||||
return Err(PatchError::InvalidPointer);
|
||||
}
|
||||
let val = remove(doc, from, allow_last)?;
|
||||
add(doc, path, val)
|
||||
}
|
||||
|
||||
fn copy<S0: AsRef<str>, S1: AsRef<str>>(
|
||||
doc: &mut Value,
|
||||
from: &JsonPointer<S0>,
|
||||
path: &JsonPointer<S1>,
|
||||
) -> Result<Option<Value>, PatchError> {
|
||||
let source = from.get(doc).ok_or(PatchError::InvalidPointer)?.clone();
|
||||
add(doc, path, source)
|
||||
}
|
||||
|
||||
fn test<S: AsRef<str>, V: SegList>(
|
||||
doc: &Value,
|
||||
path: &JsonPointer<S, V>,
|
||||
expected: &Value,
|
||||
) -> Result<(), PatchError> {
|
||||
let target = path.get(doc).ok_or(PatchError::InvalidPointer)?;
|
||||
if *target == *expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(PatchError::TestFailed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create JSON Patch from JSON Value
|
||||
/// # Examples
|
||||
///
|
||||
/// Create patch from `imbl_value::Value`:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[macro_use]
|
||||
/// extern crate imbl_value;
|
||||
/// extern crate json_patch;
|
||||
///
|
||||
/// use json_patch::{Patch, from_value};
|
||||
///
|
||||
/// # pub fn main() {
|
||||
/// let patch_value = json!([
|
||||
/// { "op": "test", "path": "/0/name", "value": "Andrew" },
|
||||
/// { "op": "add", "path": "/0/happy", "value": true }
|
||||
/// ]);
|
||||
/// let patch: Patch = from_value(patch_value).unwrap();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Create patch from string:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[macro_use]
|
||||
/// extern crate serde_json;
|
||||
/// extern crate json_patch;
|
||||
///
|
||||
/// use json_patch::Patch;
|
||||
/// use serde_json::from_str;
|
||||
///
|
||||
/// # pub fn main() {
|
||||
/// let patch_str = r#"[
|
||||
/// { "op": "test", "path": "/0/name", "value": "Andrew" },
|
||||
/// { "op": "add", "path": "/0/happy", "value": true }
|
||||
/// ]"#;
|
||||
/// let patch: Patch = from_str(patch_str).unwrap();
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_value(value: Value) -> Result<Patch, imbl_value::Error> {
|
||||
let patch = imbl_value::from_value::<Vec<PatchOperation>>(value)?;
|
||||
Ok(Patch(patch))
|
||||
}
|
||||
|
||||
/// Patch provided JSON document (given as `imbl_value::Value`) in-place. If any of the patch is
|
||||
/// failed, all previous operations are reverted. In case of internal error resulting in panic,
|
||||
/// document might be left in inconsistent state.
|
||||
///
|
||||
/// # Example
|
||||
/// Create and patch document:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[macro_use]
|
||||
/// extern crate imbl_value;
|
||||
/// extern crate json_patch;
|
||||
///
|
||||
/// use json_patch::patch;
|
||||
/// use serde_json::from_str;
|
||||
///
|
||||
/// # pub fn main() {
|
||||
/// let mut doc = json!([
|
||||
/// { "name": "Andrew" },
|
||||
/// { "name": "Maxim" }
|
||||
/// ]);
|
||||
///
|
||||
/// let p = from_str(r#"[
|
||||
/// { "op": "test", "path": "/0/name", "value": "Andrew" },
|
||||
/// { "op": "add", "path": "/0/happy", "value": true }
|
||||
/// ]"#).unwrap();
|
||||
///
|
||||
/// patch(&mut doc, &p).unwrap();
|
||||
/// assert_eq!(doc, json!([
|
||||
/// { "name": "Andrew", "happy": true },
|
||||
/// { "name": "Maxim" }
|
||||
/// ]));
|
||||
///
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn patch<'a>(doc: &mut Value, patch: &'a Patch) -> Result<Undo<'a>, PatchError> {
|
||||
let mut undo = Undo(Vec::with_capacity(patch.0.len()));
|
||||
apply_patches(doc, &patch.0, &mut undo).map(|_| undo)
|
||||
}
|
||||
|
||||
/// Object that can be used to undo a patch if successful
|
||||
pub struct Undo<'a>(Vec<Box<dyn FnOnce(&mut Value) + Send + Sync + 'a>>);
|
||||
impl<'a> Undo<'a> {
|
||||
/// Apply the undo to the document
|
||||
pub fn apply(mut self, doc: &mut Value) {
|
||||
while let Some(undo) = self.0.pop() {
|
||||
undo(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply patches while tracking all the changes being made so they can be reverted back in case
|
||||
// subsequent patches fail. Uses heap allocated closures to keep the state.
|
||||
fn apply_patches<'a>(
|
||||
doc: &mut Value,
|
||||
patches: &'a [PatchOperation],
|
||||
undo: &mut Undo<'a>,
|
||||
) -> Result<(), PatchError> {
|
||||
let (patch, tail) = match patches.split_first() {
|
||||
None => return Ok(()),
|
||||
Some((patch, tail)) => (patch, tail),
|
||||
};
|
||||
|
||||
let res = match *patch {
|
||||
PatchOperation::Add(ref op) => {
|
||||
let prev = add(doc, &op.path, op.value.clone())?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
match prev {
|
||||
None => remove(doc, &op.path, true).unwrap(),
|
||||
Some(v) => add(doc, &op.path, v).unwrap().unwrap(),
|
||||
};
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Remove(ref op) => {
|
||||
let prev = remove(doc, &op.path, false)?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
assert!(add(doc, &op.path, prev).unwrap().is_none());
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Replace(ref op) => {
|
||||
let prev = replace(doc, &op.path, op.value.clone())?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
replace(doc, &op.path, prev).unwrap();
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Move(ref op) => {
|
||||
let prev = mov(doc, &op.from, &op.path, false)?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
mov(doc, &op.path, &op.from, true).unwrap();
|
||||
if let Some(prev) = prev {
|
||||
assert!(add(doc, &op.path, prev).unwrap().is_none());
|
||||
}
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Copy(ref op) => {
|
||||
let prev = copy(doc, &op.from, &op.path)?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
match prev {
|
||||
None => remove(doc, &op.path, true).unwrap(),
|
||||
Some(v) => add(doc, &op.path, v).unwrap().unwrap(),
|
||||
};
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Test(ref op) => {
|
||||
test(doc, &op.path, &op.value)?;
|
||||
undo.0.push(Box::new(move |_| ()));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
};
|
||||
if res.is_err() {
|
||||
undo.0.pop().unwrap()(doc);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Patch provided JSON document (given as `imbl_value::Value`) in place.
|
||||
/// Operations are applied in unsafe manner. If any of the operations fails, all previous
|
||||
/// operations are not reverted.
|
||||
pub fn patch_unsafe(doc: &mut Value, patch: &Patch) -> Result<(), PatchError> {
|
||||
for op in &patch.0 {
|
||||
match *op {
|
||||
PatchOperation::Add(ref op) => {
|
||||
add(doc, &op.path, op.value.clone())?;
|
||||
}
|
||||
PatchOperation::Remove(ref op) => {
|
||||
remove(doc, &op.path, false)?;
|
||||
}
|
||||
PatchOperation::Replace(ref op) => {
|
||||
replace(doc, &op.path, op.value.clone())?;
|
||||
}
|
||||
PatchOperation::Move(ref op) => {
|
||||
mov(doc, &op.from, &op.path, false)?;
|
||||
}
|
||||
PatchOperation::Copy(ref op) => {
|
||||
copy(doc, &op.from, &op.path)?;
|
||||
}
|
||||
PatchOperation::Test(ref op) => {
|
||||
test(doc, &op.path, &op.value)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Patch provided JSON document (given as `imbl_value::Value`) in place with JSON Merge Patch
|
||||
/// (RFC 7396).
|
||||
///
|
||||
/// # Example
|
||||
/// Create and patch document:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[macro_use]
|
||||
/// extern crate imbl_value;
|
||||
/// extern crate json_patch;
|
||||
///
|
||||
/// use json_patch::merge;
|
||||
///
|
||||
/// # pub fn main() {
|
||||
/// let mut doc = json!({
|
||||
/// "title": "Goodbye!",
|
||||
/// "author" : {
|
||||
/// "givenName" : "John",
|
||||
/// "familyName" : "Doe"
|
||||
/// },
|
||||
/// "tags":[ "example", "sample" ],
|
||||
/// "content": "This will be unchanged"
|
||||
/// });
|
||||
///
|
||||
/// let patch = json!({
|
||||
/// "title": "Hello!",
|
||||
/// "phoneNumber": "+01-123-456-7890",
|
||||
/// "author": {
|
||||
/// "familyName": null
|
||||
/// },
|
||||
/// "tags": [ "example" ]
|
||||
/// });
|
||||
///
|
||||
/// merge(&mut doc, &patch);
|
||||
/// assert_eq!(doc, json!({
|
||||
/// "title": "Hello!",
|
||||
/// "author" : {
|
||||
/// "givenName" : "John"
|
||||
/// },
|
||||
/// "tags": [ "example" ],
|
||||
/// "content": "This will be unchanged",
|
||||
/// "phoneNumber": "+01-123-456-7890"
|
||||
/// }));
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn merge(doc: &mut Value, patch: &Value) {
|
||||
if !patch.is_object() {
|
||||
*doc = patch.clone();
|
||||
return;
|
||||
}
|
||||
|
||||
if !doc.is_object() {
|
||||
*doc = Value::Object(Map::new());
|
||||
}
|
||||
let map = doc.as_object_mut().unwrap();
|
||||
for (key, value) in patch.as_object().unwrap() {
|
||||
if value.is_null() {
|
||||
map.remove(&*key);
|
||||
} else {
|
||||
merge(map.entry(key.clone()).or_insert(Value::Null), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "diff")]
|
||||
mod diff;
|
||||
|
||||
#[cfg(feature = "diff")]
|
||||
pub use self::diff::diff;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
83
json-patch/src/tests/mod.rs
Normal file
83
json-patch/src/tests/mod.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
#![allow(unused)]
|
||||
extern crate rand;
|
||||
|
||||
mod util;
|
||||
|
||||
use super::*;
|
||||
use serde_json::from_str;
|
||||
|
||||
#[test]
|
||||
fn parse_from_value() {
|
||||
use PatchOperation::*;
|
||||
|
||||
let json = json!([{"op": "add", "path": "/a/b", "value": 1}, {"op": "remove", "path": "/c"}]);
|
||||
let patch: Patch = from_value(json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
patch,
|
||||
Patch(vec![
|
||||
Add(AddOperation {
|
||||
path: "/a/b".parse().unwrap(),
|
||||
value: json!(1),
|
||||
}),
|
||||
Remove(RemoveOperation {
|
||||
path: "/c".parse().unwrap(),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
let _patch: Patch =
|
||||
from_str(r#"[{"op": "add", "path": "/a/b", "value": 1}, {"op": "remove", "path": "/c"}]"#)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_from_string() {
|
||||
use PatchOperation::*;
|
||||
|
||||
let patch: Patch =
|
||||
from_str(r#"[{"op": "add", "path": "/a/b", "value": 1}, {"op": "remove", "path": "/c"}]"#)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
patch,
|
||||
Patch(vec![
|
||||
Add(AddOperation {
|
||||
path: "/a/b".parse().unwrap(),
|
||||
value: json!(1),
|
||||
}),
|
||||
Remove(RemoveOperation {
|
||||
path: "/c".parse().unwrap()
|
||||
}),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_patch() {
|
||||
let s = r#"[{"op":"add","path":"/a/b","value":1},{"op":"remove","path":"/c"}]"#;
|
||||
let patch: Patch = from_str(s).unwrap();
|
||||
|
||||
let serialized = serde_json::to_string(&patch).unwrap();
|
||||
assert_eq!(serialized, s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests() {
|
||||
util::run_specs("specs/tests.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_tests() {
|
||||
util::run_specs("specs/spec_tests.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revert_tests() {
|
||||
util::run_specs("specs/revert_tests.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_tests() {
|
||||
util::run_specs("specs/merge_tests.json");
|
||||
}
|
||||
103
json-patch/src/tests/util.rs
Normal file
103
json-patch/src/tests/util.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use imbl_value::Value;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Write;
|
||||
use std::{fs, io};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TestCase {
|
||||
comment: Option<String>,
|
||||
doc: Value,
|
||||
patch: Value,
|
||||
expected: Option<Value>,
|
||||
error: Option<String>,
|
||||
#[serde(default)]
|
||||
disabled: bool,
|
||||
#[serde(default)]
|
||||
merge: bool,
|
||||
}
|
||||
|
||||
fn run_case(doc: &Value, patches: &Value, merge_patch: bool) -> Result<Value, String> {
|
||||
let mut actual = doc.clone();
|
||||
if merge_patch {
|
||||
crate::merge(&mut actual, &patches);
|
||||
} else {
|
||||
let patches: crate::Patch =
|
||||
imbl_value::from_value(patches.clone()).map_err(|e| e.to_string())?;
|
||||
|
||||
// Patch and verify that in case of error document wasn't changed
|
||||
crate::patch(&mut actual, &patches)
|
||||
.map_err(|e| {
|
||||
assert_eq!(
|
||||
*doc, actual,
|
||||
"no changes should be made to the original document"
|
||||
);
|
||||
e
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(actual)
|
||||
}
|
||||
|
||||
fn run_case_patch_unsafe(doc: &Value, patches: &Value) -> Result<Value, String> {
|
||||
let mut actual = doc.clone();
|
||||
let patches: crate::Patch =
|
||||
imbl_value::from_value(patches.clone()).map_err(|e| e.to_string())?;
|
||||
crate::patch_unsafe(&mut actual, &patches).map_err(|e| e.to_string())?;
|
||||
Ok(actual)
|
||||
}
|
||||
|
||||
pub fn run_specs(path: &str) {
|
||||
let file = fs::File::open(path).unwrap();
|
||||
let buf = io::BufReader::new(file);
|
||||
let cases: Vec<TestCase> = serde_json::from_reader(buf).unwrap();
|
||||
|
||||
for (idx, tc) in cases.into_iter().enumerate() {
|
||||
print!("Running test case {}", idx);
|
||||
if let Some(comment) = tc.comment {
|
||||
print!(" ({})... ", comment);
|
||||
} else {
|
||||
print!("... ");
|
||||
}
|
||||
|
||||
if tc.disabled {
|
||||
println!("disabled...");
|
||||
continue;
|
||||
}
|
||||
|
||||
match run_case(&tc.doc, &tc.patch, tc.merge) {
|
||||
Ok(actual) => {
|
||||
if let Some(ref error) = tc.error {
|
||||
println!("expected to fail with '{}'", error);
|
||||
panic!("expected to fail, got document {:?}", actual);
|
||||
}
|
||||
println!();
|
||||
if let Some(ref expected) = tc.expected {
|
||||
assert_eq!(*expected, actual);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("failed with '{}'", err);
|
||||
tc.error.as_ref().expect("patch expected to succeed");
|
||||
}
|
||||
}
|
||||
|
||||
if !tc.merge {
|
||||
match run_case_patch_unsafe(&tc.doc, &tc.patch) {
|
||||
Ok(actual) => {
|
||||
if let Some(ref error) = tc.error {
|
||||
println!("expected to fail with '{}'", error);
|
||||
panic!("expected to fail, got document {:?}", actual);
|
||||
}
|
||||
println!();
|
||||
if let Some(ref expected) = tc.expected {
|
||||
assert_eq!(*expected, actual);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("failed with '{}'", err);
|
||||
tc.error.as_ref().expect("patch expected to succeed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user