mirror of
https://github.com/Start9Labs/patch-db.git
synced 2026-03-31 12:23:40 +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:
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