Skip to content

Commit b0954bc

Browse files
authored
Ability to remove JSON fields using JSONPath (#106)
* feat: add JSON element deletion by JSONPath * refactor: integrate deletion functionality into Queryable trait - Move delete_by_path from separate trait to Queryable trait - Use Queried<usize> type alias instead of Result for consistency - Remove delete_single method in favor of unified delete_by_path - Simplify error handling by preserving original query errors * fix formatting issues
1 parent c6b0088 commit b0954bc

File tree

2 files changed

+311
-3
lines changed

2 files changed

+311
-3
lines changed

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ extern crate pest;
9595

9696
use crate::query::queryable::Queryable;
9797
use crate::query::{Queried, QueryPath, QueryRef};
98-
use serde_json::Value;
98+
use serde_json::Value;
9999

100100
/// A trait for types that can be queried with JSONPath.
101101
pub trait JsonPath: Queryable {

src/query/queryable.rs

Lines changed: 310 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::parser::errors::JsonPathError;
22
use crate::parser::model::{JpQuery, Segment, Selector};
33
use crate::parser::{parse_json_path, Parsed};
4-
use crate::query::QueryPath;
4+
use crate::query::{Queried, QueryPath};
5+
use crate::JsonPath;
56
use serde_json::Value;
67
use std::borrow::Cow;
78
use std::fmt::Debug;
@@ -138,6 +139,39 @@ where
138139
{
139140
None
140141
}
142+
143+
/// Deletes all elements matching the given JSONPath
144+
///
145+
/// # Arguments
146+
/// * `path` - JSONPath string specifying elements to delete
147+
///
148+
/// # Returns
149+
/// * `Ok(usize)` - Number of elements deleted
150+
/// * `Err(JsonPathError)` - If the path is invalid or deletion fails
151+
///
152+
/// # Examples
153+
/// ```
154+
/// use serde_json::json;
155+
/// use jsonpath_rust::JsonPath;
156+
/// use crate::jsonpath_rust::query::queryable::Queryable;
157+
///
158+
/// let mut data = json!({
159+
/// "users": [
160+
/// {"name": "Alice", "age": 30},
161+
/// {"name": "Bob", "age": 25},
162+
/// {"name": "Charlie", "age": 35}
163+
/// ]
164+
/// });
165+
///
166+
/// // Delete users older than 30
167+
/// let deleted = data.delete_by_path("$.users[?(@.age > 30)]").unwrap();
168+
/// assert_eq!(deleted, 1);
169+
/// ```
170+
fn delete_by_path(&mut self, _path: &str) -> Queried<usize> {
171+
Err(JsonPathError::InvalidJsonPath(
172+
"Deletion not supported".to_string(),
173+
))
174+
}
141175
}
142176

143177
impl Queryable for Value {
@@ -279,6 +313,168 @@ impl Queryable for Value {
279313
.ok()
280314
.and_then(|p| self.pointer_mut(p.as_str()))
281315
}
316+
317+
fn delete_by_path(&mut self, path: &str) -> Queried<usize> {
318+
let mut deletions = Vec::new();
319+
for query_path in &self.query_only_path(path)? {
320+
if let Some(deletion_info) = parse_deletion_path(query_path)? {
321+
deletions.push(deletion_info);
322+
}
323+
}
324+
325+
// Sort deletions to handle array indices correctly (delete from end to start)
326+
deletions.sort_by(|a, b| {
327+
b.path_depth()
328+
.cmp(&a.path_depth())
329+
.then_with(|| match (a, b) {
330+
(
331+
DeletionInfo::ArrayIndex { index: idx_a, .. },
332+
DeletionInfo::ArrayIndex { index: idx_b, .. },
333+
) => idx_b.cmp(idx_a),
334+
_ => std::cmp::Ordering::Equal,
335+
})
336+
});
337+
338+
// Perform deletions
339+
let deleted_count = deletions.iter().try_fold(0, |c, d| {
340+
execute_deletion(self, d).map(|deleted| if deleted { c + 1 } else { c })
341+
})?;
342+
343+
Ok(deleted_count)
344+
}
345+
}
346+
347+
#[derive(Debug, Clone)]
348+
enum DeletionInfo {
349+
ObjectField {
350+
parent_path: String,
351+
field_name: String,
352+
},
353+
ArrayIndex {
354+
parent_path: String,
355+
index: usize,
356+
},
357+
Root,
358+
}
359+
360+
impl DeletionInfo {
361+
fn path_depth(&self) -> usize {
362+
match self {
363+
DeletionInfo::Root => 0,
364+
DeletionInfo::ObjectField { parent_path, .. }
365+
| DeletionInfo::ArrayIndex { parent_path, .. } => parent_path.matches('/').count(),
366+
}
367+
}
368+
}
369+
370+
fn parse_deletion_path(query_path: &str) -> Result<Option<DeletionInfo>, JsonPathError> {
371+
if query_path == "$" {
372+
return Ok(Some(DeletionInfo::Root));
373+
}
374+
375+
let JpQuery { segments } = parse_json_path(query_path)?;
376+
377+
if segments.is_empty() {
378+
return Ok(None);
379+
}
380+
381+
let mut parent_path = String::new();
382+
let mut segments_iter = segments.iter().peekable();
383+
384+
while let Some(segment) = segments_iter.next() {
385+
if segments_iter.peek().is_some() {
386+
// Not the last segment, add to parent path
387+
match segment {
388+
Segment::Selector(Selector::Name(name)) => {
389+
parent_path.push_str(&format!("/{}", name.trim_matches(|c| c == '\'')));
390+
}
391+
Segment::Selector(Selector::Index(index)) => {
392+
parent_path.push_str(&format!("/{}", index));
393+
}
394+
e => {
395+
return Err(JsonPathError::InvalidJsonPath(format!(
396+
"Unsupported segment to be deleted: {:?}",
397+
e
398+
)));
399+
}
400+
}
401+
} else {
402+
match segment {
403+
Segment::Selector(Selector::Name(name)) => {
404+
let field_name = name.trim_matches(|c| c == '\'').to_string();
405+
return Ok(Some(DeletionInfo::ObjectField {
406+
parent_path,
407+
field_name,
408+
}));
409+
}
410+
Segment::Selector(Selector::Index(index)) => {
411+
return Ok(Some(DeletionInfo::ArrayIndex {
412+
parent_path,
413+
index: *index as usize,
414+
}));
415+
}
416+
e => {
417+
return Err(JsonPathError::InvalidJsonPath(format!(
418+
"Unsupported segment to be deleted: {:?}",
419+
e
420+
)));
421+
}
422+
}
423+
}
424+
}
425+
426+
Ok(None)
427+
}
428+
429+
fn execute_deletion(value: &mut Value, deletion: &DeletionInfo) -> Queried<bool> {
430+
match deletion {
431+
DeletionInfo::Root => {
432+
*value = Value::Null;
433+
Ok(true)
434+
}
435+
DeletionInfo::ObjectField {
436+
parent_path,
437+
field_name,
438+
} => {
439+
let parent = if parent_path.is_empty() {
440+
value
441+
} else {
442+
value.pointer_mut(parent_path).ok_or_else(|| {
443+
JsonPathError::InvalidJsonPath("Parent path not found".to_string())
444+
})?
445+
};
446+
447+
if let Some(obj) = parent.as_object_mut() {
448+
Ok(obj.remove(field_name).is_some())
449+
} else {
450+
Err(JsonPathError::InvalidJsonPath(
451+
"Parent is not an object".to_string(),
452+
))
453+
}
454+
}
455+
DeletionInfo::ArrayIndex { parent_path, index } => {
456+
let parent = if parent_path.is_empty() {
457+
value
458+
} else {
459+
value.pointer_mut(parent_path).ok_or_else(|| {
460+
JsonPathError::InvalidJsonPath("Parent path not found".to_string())
461+
})?
462+
};
463+
464+
if let Some(arr) = parent.as_array_mut() {
465+
if *index < arr.len() {
466+
arr.remove(*index);
467+
Ok(true)
468+
} else {
469+
Ok(false) // Index out of bounds
470+
}
471+
} else {
472+
Err(JsonPathError::InvalidJsonPath(
473+
"Parent is not an array".to_string(),
474+
))
475+
}
476+
}
477+
}
282478
}
283479

284480
fn convert_js_path(path: &str) -> Parsed<String> {
@@ -310,7 +506,7 @@ mod tests {
310506
use crate::query::queryable::{convert_js_path, Queryable};
311507
use crate::query::Queried;
312508
use crate::JsonPath;
313-
use serde_json::json;
509+
use serde_json::{json, Value};
314510

315511
#[test]
316512
fn in_smoke() -> Queried<()> {
@@ -446,4 +642,116 @@ mod tests {
446642

447643
Ok(())
448644
}
645+
#[test]
646+
fn test_delete_object_field() {
647+
let mut data = json!({
648+
"users": {
649+
"alice": {"age": 30},
650+
"bob": {"age": 25}
651+
}
652+
});
653+
654+
let deleted = data.delete_by_path("$.users.alice").unwrap();
655+
assert_eq!(deleted, 1);
656+
657+
let expected = json!({
658+
"users": {
659+
"bob": {"age": 25}
660+
}
661+
});
662+
assert_eq!(data, expected);
663+
}
664+
665+
#[test]
666+
fn test_delete_array_element() {
667+
let mut data = json!({
668+
"numbers": [1, 2, 3, 4, 5]
669+
});
670+
671+
let deleted = data.delete_by_path("$.numbers[2]").unwrap();
672+
assert_eq!(deleted, 1);
673+
674+
let expected = json!({
675+
"numbers": [1, 2, 4, 5]
676+
});
677+
assert_eq!(data, expected);
678+
}
679+
680+
#[test]
681+
fn test_delete_multiple_elements() {
682+
let mut data = json!({
683+
"users": [
684+
{"name": "Alice", "age": 30},
685+
{"name": "Bob", "age": 25},
686+
{"name": "Charlie", "age": 35},
687+
{"name": "David", "age": 22}
688+
]
689+
});
690+
691+
// Delete users older than 24
692+
let deleted = data.delete_by_path("$.users[?(@.age > 24)]").unwrap();
693+
assert_eq!(deleted, 3);
694+
695+
let expected = json!({
696+
"users": [
697+
{"name": "David", "age": 22}
698+
]
699+
});
700+
assert_eq!(data, expected);
701+
}
702+
703+
#[test]
704+
fn test_delete_nested_fields() {
705+
let mut data = json!({
706+
"company": {
707+
"departments": {
708+
"engineering": {"budget": 100000},
709+
"marketing": {"budget": 50000},
710+
"hr": {"budget": 30000}
711+
}
712+
}
713+
});
714+
715+
let deleted = data
716+
.delete_by_path("$.company.departments.marketing")
717+
.unwrap();
718+
assert_eq!(deleted, 1);
719+
720+
let expected = json!({
721+
"company": {
722+
"departments": {
723+
"engineering": {"budget": 100000},
724+
"hr": {"budget": 30000}
725+
}
726+
}
727+
});
728+
assert_eq!(data, expected);
729+
}
730+
731+
#[test]
732+
fn test_delete_nonexistent_path() {
733+
let mut data = json!({
734+
"test": "value"
735+
});
736+
737+
let deleted = data.delete_by_path("$.nonexistent").unwrap();
738+
assert_eq!(deleted, 0);
739+
740+
// Data should remain unchanged
741+
let expected = json!({
742+
"test": "value"
743+
});
744+
assert_eq!(data, expected);
745+
}
746+
747+
#[test]
748+
fn test_delete_root() {
749+
let mut data = json!({
750+
"test": "value"
751+
});
752+
753+
let deleted = data.delete_by_path("$").unwrap();
754+
assert_eq!(deleted, 1);
755+
assert_eq!(data, Value::Null);
756+
}
449757
}

0 commit comments

Comments
 (0)