Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ default = ["diff"]
diff = []

[dependencies]
jsonptr = "0.6.0"
jsonptr = "0.7.1"
serde = { version = "1.0.159", features = ["derive"] }
serde_json = "1.0.95"
thiserror = "1.0.40"
Expand Down
137 changes: 61 additions & 76 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,10 @@
//! ```
#![warn(missing_docs)]

use jsonptr::{Pointer, PointerBuf};
use jsonptr::{index::Index, Pointer, PointerBuf};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::{
borrow::Cow,
fmt::{self, Display, Formatter},
};
use std::fmt::{self, Display, Formatter};
use thiserror::Error;

#[cfg(feature = "diff")]
Expand All @@ -95,7 +92,7 @@ pub use self::diff::diff;

struct WriteAdapter<'a>(&'a mut dyn fmt::Write);

impl<'a> std::io::Write for WriteAdapter<'a> {
impl std::io::Write for WriteAdapter<'_> {
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
let s = std::str::from_utf8(buf).unwrap();
self.0
Expand Down Expand Up @@ -287,6 +284,18 @@ pub enum PatchErrorKind {
CannotMoveInsideItself,
}

impl From<jsonptr::index::ParseIndexError> for PatchErrorKind {
fn from(_: jsonptr::index::ParseIndexError) -> Self {
Self::InvalidPointer
}
}

impl From<jsonptr::index::OutOfBoundsError> for PatchErrorKind {
fn from(_: jsonptr::index::OutOfBoundsError) -> Self {
Self::InvalidPointer
}
}

/// This type represents all possible errors that can occur when applying JSON patch
#[derive(Debug, Error)]
#[error("operation '/{operation}' failed at path '{path}': {kind}")]
Expand All @@ -308,92 +317,67 @@ fn translate_error(kind: PatchErrorKind, operation: usize, path: &Pointer) -> Pa
}
}

fn unescape(s: &str) -> Cow<str> {
if s.contains('~') {
Cow::Owned(s.replace("~1", "/").replace("~0", "~"))
} else {
Cow::Borrowed(s)
}
}

fn parse_index(str: &str, len: usize) -> Result<usize, PatchErrorKind> {
// RFC 6901 prohibits leading zeroes in index
if (str.starts_with('0') && str.len() != 1) || str.starts_with('+') {
return Err(PatchErrorKind::InvalidPointer);
}
match str.parse::<usize>() {
Ok(index) if index < len => Ok(index),
_ => Err(PatchErrorKind::InvalidPointer),
}
}

fn split_pointer(pointer: &str) -> Result<(&str, &str), PatchErrorKind> {
pointer
.rfind('/')
.ok_or(PatchErrorKind::InvalidPointer)
.map(|idx| (&pointer[0..idx], &pointer[idx + 1..]))
}

fn add(doc: &mut Value, path: &str, value: Value) -> Result<Option<Value>, PatchErrorKind> {
if path.is_empty() {
fn add(doc: &mut Value, path: &Pointer, value: Value) -> Result<Option<Value>, PatchErrorKind> {
let Some((parent, last)) = path.split_back() else {
// empty path, add is replace the value wholesale
return Ok(Some(std::mem::replace(doc, value)));
}
};

let (parent, last_unescaped) = split_pointer(path)?;
let parent = doc
.pointer_mut(parent)
let mut parent = doc
.pointer_mut(parent.as_str())
.ok_or(PatchErrorKind::InvalidPointer)?;

match *parent {
Value::Object(ref mut obj) => Ok(obj.insert(unescape(last_unescaped).into_owned(), value)),
Value::Array(ref mut arr) if last_unescaped == "-" => {
arr.push(value);
Ok(None)
}
Value::Array(ref mut arr) => {
let idx = parse_index(last_unescaped, arr.len() + 1)?;
match &mut parent {
Value::Object(obj) => Ok(obj.insert(last.decoded().into_owned(), value)),
Value::Array(arr) => {
let idx = last.to_index()?.for_len_incl(arr.len())?;
arr.insert(idx, value);
Ok(None)
}
_ => Err(PatchErrorKind::InvalidPointer),
}
}

fn remove(doc: &mut Value, path: &str, allow_last: bool) -> Result<Value, PatchErrorKind> {
let (parent, last_unescaped) = split_pointer(path)?;
let parent = doc
.pointer_mut(parent)
fn remove(doc: &mut Value, path: &Pointer, allow_last: bool) -> Result<Value, PatchErrorKind> {
let Some((parent, last)) = path.split_back() else {
return Err(PatchErrorKind::InvalidPointer);
};
let mut parent = doc
.pointer_mut(parent.as_str())
.ok_or(PatchErrorKind::InvalidPointer)?;

match *parent {
Value::Object(ref mut obj) => match obj.remove(unescape(last_unescaped).as_ref()) {
match &mut parent {
Value::Object(obj) => match obj.remove(last.decoded().as_ref()) {
None => Err(PatchErrorKind::InvalidPointer),
Some(val) => Ok(val),
},
Value::Array(ref mut arr) if allow_last && last_unescaped == "-" => Ok(arr.pop().unwrap()),
Value::Array(ref mut arr) => {
let idx = parse_index(last_unescaped, arr.len())?;
// XXX: is this really correct? semantically it seems off, `-` refers to an empty position, not the last element
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@idubrov could you share some context here?

Value::Array(arr) if allow_last && matches!(last.to_index(), Ok(Index::Next)) => {
Ok(arr.pop().unwrap())
}
Value::Array(arr) => {
let idx = last.to_index()?.for_len(arr.len())?;
Ok(arr.remove(idx))
}
_ => Err(PatchErrorKind::InvalidPointer),
}
}

fn replace(doc: &mut Value, path: &str, value: Value) -> Result<Value, PatchErrorKind> {
fn replace(doc: &mut Value, path: &Pointer, value: Value) -> Result<Value, PatchErrorKind> {
let target = doc
.pointer_mut(path)
.pointer_mut(path.as_str())
.ok_or(PatchErrorKind::InvalidPointer)?;
Ok(std::mem::replace(target, value))
}

fn mov(
doc: &mut Value,
from: &str,
path: &str,
from: &Pointer,
path: &Pointer,
allow_last: bool,
) -> Result<Option<Value>, PatchErrorKind> {
// Check we are not moving inside own child
if path.starts_with(from) && path[from.len()..].starts_with('/') {
if path.starts_with(from) && path.len() != from.len() {
return Err(PatchErrorKind::CannotMoveInsideItself);
}
let val = remove(doc, from, allow_last).map_err(|err| match err {
Expand All @@ -403,16 +387,18 @@ fn mov(
add(doc, path, val)
}

fn copy(doc: &mut Value, from: &str, path: &str) -> Result<Option<Value>, PatchErrorKind> {
fn copy(doc: &mut Value, from: &Pointer, path: &Pointer) -> Result<Option<Value>, PatchErrorKind> {
let source = doc
.pointer(from)
.pointer(from.as_str())
.ok_or(PatchErrorKind::InvalidFromPointer)?
.clone();
add(doc, path, source)
}

fn test(doc: &Value, path: &str, expected: &Value) -> Result<(), PatchErrorKind> {
let target = doc.pointer(path).ok_or(PatchErrorKind::InvalidPointer)?;
fn test(doc: &Value, path: &Pointer, expected: &Value) -> Result<(), PatchErrorKind> {
let target = doc
.pointer(path.as_str())
.ok_or(PatchErrorKind::InvalidPointer)?;
if *target == *expected {
Ok(())
} else {
Expand Down Expand Up @@ -503,23 +489,22 @@ fn undo_patches(doc: &mut Value, undo_patches: &[PatchOperation]) -> Result<(),
for (operation, patch) in undo_patches.iter().enumerate().rev() {
match patch {
PatchOperation::Add(op) => {
add(doc, op.path.as_str(), op.value.clone())
add(doc, &op.path, op.value.clone())
.map_err(|e| translate_error(e, operation, &op.path))?;
}
PatchOperation::Remove(op) => {
remove(doc, op.path.as_str(), true)
.map_err(|e| translate_error(e, operation, &op.path))?;
remove(doc, &op.path, true).map_err(|e| translate_error(e, operation, &op.path))?;
}
PatchOperation::Replace(op) => {
replace(doc, op.path.as_str(), op.value.clone())
replace(doc, &op.path, op.value.clone())
.map_err(|e| translate_error(e, operation, &op.path))?;
}
PatchOperation::Move(op) => {
mov(doc, op.from.as_str(), op.path.as_str(), true)
mov(doc, &op.from, &op.path, true)
.map_err(|e| translate_error(e, operation, &op.path))?;
}
PatchOperation::Copy(op) => {
copy(doc, op.from.as_str(), op.path.as_str())
copy(doc, &op.from, &op.path)
.map_err(|e| translate_error(e, operation, &op.path))?;
}
_ => unreachable!(),
Expand All @@ -540,7 +525,7 @@ fn apply_patches(
for (operation, patch) in patches.iter().enumerate() {
match patch {
PatchOperation::Add(ref op) => {
let prev = add(doc, op.path.as_str(), op.value.clone())
let prev = add(doc, &op.path, op.value.clone())
.map_err(|e| translate_error(e, operation, &op.path))?;
if let Some(&mut ref mut undo_stack) = undo_stack {
undo_stack.push(match prev {
Expand All @@ -555,7 +540,7 @@ fn apply_patches(
}
}
PatchOperation::Remove(ref op) => {
let prev = remove(doc, op.path.as_str(), false)
let prev = remove(doc, &op.path, false)
.map_err(|e| translate_error(e, operation, &op.path))?;
if let Some(&mut ref mut undo_stack) = undo_stack {
undo_stack.push(PatchOperation::Add(AddOperation {
Expand All @@ -565,7 +550,7 @@ fn apply_patches(
}
}
PatchOperation::Replace(ref op) => {
let prev = replace(doc, op.path.as_str(), op.value.clone())
let prev = replace(doc, &op.path, op.value.clone())
.map_err(|e| translate_error(e, operation, &op.path))?;
if let Some(&mut ref mut undo_stack) = undo_stack {
undo_stack.push(PatchOperation::Replace(ReplaceOperation {
Expand All @@ -575,7 +560,7 @@ fn apply_patches(
}
}
PatchOperation::Move(ref op) => {
let prev = mov(doc, op.from.as_str(), op.path.as_str(), false)
let prev = mov(doc, &op.from, &op.path, false)
.map_err(|e| translate_error(e, operation, &op.path))?;
if let Some(&mut ref mut undo_stack) = undo_stack {
if let Some(prev) = prev {
Expand All @@ -591,7 +576,7 @@ fn apply_patches(
}
}
PatchOperation::Copy(ref op) => {
let prev = copy(doc, op.from.as_str(), op.path.as_str())
let prev = copy(doc, &op.from, &op.path)
.map_err(|e| translate_error(e, operation, &op.path))?;
if let Some(&mut ref mut undo_stack) = undo_stack {
undo_stack.push(match prev {
Expand All @@ -606,7 +591,7 @@ fn apply_patches(
}
}
PatchOperation::Test(ref op) => {
test(doc, op.path.as_str(), &op.value)
test(doc, &op.path, &op.value)
.map_err(|e| translate_error(e, operation, &op.path))?;
}
}
Expand Down
10 changes: 5 additions & 5 deletions tests/errors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@
- op: copy
from: "/third/1~1"
path: "/fourth"
error: "operation '/0' failed at path '/fourth': \"from\" path is invalid"
error: 'operation ''/0'' failed at path ''/fourth'': "from" path is invalid'
- doc: *1
patch:
- op: move
from: "/third/1~1"
path: "/fourth"
error: "operation '/0' failed at path '/fourth': \"from\" path is invalid"
error: 'operation ''/0'' failed at path ''/fourth'': "from" path is invalid'
- doc: *1
patch:
- op: move
Expand Down Expand Up @@ -89,19 +89,19 @@
- op: add
path: "first"
value: true
error: "json pointer is malformed as it does not start with a backslash ('/')"
error: "json pointer failed to parse; does not start with a slash ('/') and is not empty"
- doc: *1
patch:
- op: replace
path: "first"
value: true
error: "json pointer is malformed as it does not start with a backslash ('/')"
error: "json pointer failed to parse; does not start with a slash ('/') and is not empty"
- doc: *1
patch:
- op: remove
path: "first"
value: true
error: "json pointer is malformed as it does not start with a backslash ('/')"
error: "json pointer failed to parse; does not start with a slash ('/') and is not empty"
- doc: *1
patch:
- op: add
Expand Down
3 changes: 1 addition & 2 deletions tests/suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ fn run_patch_test_case(tc: &PatchTestCase, kind: PatchKind) -> Result<Value, Str
// Patch and verify that in case of error document wasn't changed
let patch: Patch = serde_json::from_value(tc.patch.clone()).map_err(|err| err.to_string())?;
json_patch::patch(&mut actual, &patch)
.map_err(|e| {
.inspect_err(|_| {
assert_eq!(
tc.doc, actual,
"no changes should be made to the original document"
);
e
})
.map_err(|err| err.to_string())?;
Ok(actual)
Expand Down
Loading