Skip to content

Commit 2c9f18d

Browse files
committed
feat: add JSON element deletion by JSONPath
1 parent c6b0088 commit 2c9f18d

File tree

1 file changed

+333
-2
lines changed

1 file changed

+333
-2
lines changed

src/query/queryable.rs

Lines changed: 333 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::parser::errors::JsonPathError;
22
use crate::parser::model::{JpQuery, Segment, Selector};
33
use crate::parser::{parse_json_path, Parsed};
44
use crate::query::QueryPath;
5+
use crate::JsonPath;
56
use serde_json::Value;
67
use std::borrow::Cow;
78
use std::fmt::Debug;
@@ -304,13 +305,233 @@ fn convert_js_path(path: &str) -> Parsed<String> {
304305
Ok(path)
305306
}
306307

308+
pub trait QueryableDeletable: Queryable {
309+
/// Deletes all elements matching the given JSONPath
310+
///
311+
/// # Arguments
312+
/// * `path` - JSONPath string specifying elements to delete
313+
///
314+
/// # Returns
315+
/// * `Ok(usize)` - Number of elements deleted
316+
/// * `Err(JsonPathError)` - If the path is invalid or deletion fails
317+
///
318+
/// # Examples
319+
/// ```
320+
/// use serde_json::json;
321+
/// use crate::jsonpath_rust::query::queryable::QueryableDeletable;
322+
/// use jsonpath_rust::JsonPath;
323+
///
324+
/// let mut data = json!({
325+
/// "users": [
326+
/// {"name": "Alice", "age": 30},
327+
/// {"name": "Bob", "age": 25},
328+
/// {"name": "Charlie", "age": 35}
329+
/// ]
330+
/// });
331+
///
332+
/// // Delete users older than 30
333+
/// let deleted = data.delete_by_path("$.users[?(@.age > 30)]").unwrap();
334+
/// assert_eq!(deleted, 1);
335+
/// ```
336+
fn delete_by_path(&mut self, path: &str) -> Result<usize, JsonPathError>;
337+
338+
/// Deletes a single element at the given path
339+
/// Returns true if an element was deleted, false otherwise
340+
fn delete_single(&mut self, path: &str) -> Result<bool, JsonPathError>;
341+
}
342+
343+
impl QueryableDeletable for Value {
344+
fn delete_by_path(&mut self, path: &str) -> Result<usize, JsonPathError> {
345+
346+
let matching_paths = self.query_only_path(path)
347+
.map_err(|_| JsonPathError::InvalidJsonPath("Failed to query path".to_string()))?;
348+
349+
if matching_paths.is_empty() {
350+
return Ok(0);
351+
}
352+
353+
let mut deletions = Vec::new();
354+
for query_path in &matching_paths {
355+
if let Some(deletion_info) = parse_deletion_path(query_path)? {
356+
deletions.push(deletion_info);
357+
}
358+
}
359+
360+
// Sort deletions to handle array indices correctly (delete from end to start)
361+
deletions.sort_by(|a, b| {
362+
// First sort by path depth (deeper paths first)
363+
let depth_cmp = b.path_depth().cmp(&a.path_depth());
364+
if depth_cmp != std::cmp::Ordering::Equal {
365+
return depth_cmp;
366+
}
367+
368+
// Then by array index (higher indices first)
369+
match (a, b) {
370+
(DeletionInfo::ArrayIndex { index: idx_a, .. },
371+
DeletionInfo::ArrayIndex { index: idx_b, .. }) => {
372+
idx_b.cmp(idx_a)
373+
}
374+
_ => std::cmp::Ordering::Equal
375+
}
376+
});
377+
378+
// Perform deletions
379+
let mut deleted_count = 0;
380+
for deletion in deletions {
381+
if execute_deletion(self, &deletion)? {
382+
deleted_count += 1;
383+
}
384+
}
385+
386+
Ok(deleted_count)
387+
}
388+
389+
fn delete_single(&mut self, path: &str) -> Result<bool, JsonPathError> {
390+
if let Some(deletion_info) = parse_deletion_path(path)? {
391+
execute_deletion(self, &deletion_info)
392+
} else {
393+
Ok(false)
394+
}
395+
}
396+
}
397+
398+
#[derive(Debug, Clone)]
399+
enum DeletionInfo {
400+
ObjectField {
401+
parent_path: String,
402+
field_name: String,
403+
},
404+
ArrayIndex {
405+
parent_path: String,
406+
index: usize,
407+
},
408+
Root,
409+
}
410+
411+
impl DeletionInfo {
412+
fn path_depth(&self) -> usize {
413+
match self {
414+
DeletionInfo::Root => 0,
415+
DeletionInfo::ObjectField { parent_path, .. } |
416+
DeletionInfo::ArrayIndex { parent_path, .. } => {
417+
parent_path.matches('/').count()
418+
}
419+
}
420+
}
421+
}
422+
423+
fn parse_deletion_path(query_path: &str) -> Result<Option<DeletionInfo>, JsonPathError> {
424+
if query_path == "$" {
425+
return Ok(Some(DeletionInfo::Root));
426+
}
427+
428+
let JpQuery { segments } = parse_json_path(query_path)?;
429+
430+
if segments.is_empty() {
431+
return Ok(None);
432+
}
433+
434+
let mut parent_path = String::new();
435+
let mut segments_iter = segments.iter().peekable();
436+
437+
while let Some(segment) = segments_iter.next() {
438+
if segments_iter.peek().is_some() {
439+
// Not the last segment, add to parent path
440+
match segment {
441+
Segment::Selector(Selector::Name(name)) => {
442+
parent_path.push_str(&format!("/{}", name.trim_matches(|c| c == '\'')));
443+
}
444+
Segment::Selector(Selector::Index(index)) => {
445+
parent_path.push_str(&format!("/{}", index));
446+
}
447+
_ => {
448+
return Err(JsonPathError::InvalidJsonPath(
449+
"Unsupported segment type for deletion".to_string()
450+
));
451+
}
452+
}
453+
} else {
454+
match segment {
455+
Segment::Selector(Selector::Name(name)) => {
456+
let field_name = name.trim_matches(|c| c == '\'').to_string();
457+
return Ok(Some(DeletionInfo::ObjectField {
458+
parent_path,
459+
field_name,
460+
}));
461+
}
462+
Segment::Selector(Selector::Index(index)) => {
463+
return Ok(Some(DeletionInfo::ArrayIndex {
464+
parent_path,
465+
index: *index as usize,
466+
}));
467+
}
468+
_ => {
469+
return Err(JsonPathError::InvalidJsonPath(
470+
"Unsupported final segment for deletion".to_string()
471+
));
472+
}
473+
}
474+
}
475+
}
476+
477+
Ok(None)
478+
}
479+
480+
fn execute_deletion(value: &mut Value, deletion: &DeletionInfo) -> Result<bool, JsonPathError> {
481+
match deletion {
482+
DeletionInfo::Root => {
483+
*value = Value::Null;
484+
Ok(true)
485+
}
486+
DeletionInfo::ObjectField { parent_path, field_name } => {
487+
let parent = if parent_path.is_empty() {
488+
value
489+
} else {
490+
value.pointer_mut(parent_path).ok_or_else(|| {
491+
JsonPathError::InvalidJsonPath("Parent path not found".to_string())
492+
})?
493+
};
494+
495+
if let Some(obj) = parent.as_object_mut() {
496+
Ok(obj.remove(field_name).is_some())
497+
} else {
498+
Err(JsonPathError::InvalidJsonPath(
499+
"Parent is not an object".to_string()
500+
))
501+
}
502+
}
503+
DeletionInfo::ArrayIndex { parent_path, index } => {
504+
let parent = if parent_path.is_empty() {
505+
value
506+
} else {
507+
value.pointer_mut(parent_path).ok_or_else(|| {
508+
JsonPathError::InvalidJsonPath("Parent path not found".to_string())
509+
})?
510+
};
511+
512+
if let Some(arr) = parent.as_array_mut() {
513+
if *index < arr.len() {
514+
arr.remove(*index);
515+
Ok(true)
516+
} else {
517+
Ok(false) // Index out of bounds
518+
}
519+
} else {
520+
Err(JsonPathError::InvalidJsonPath(
521+
"Parent is not an array".to_string()
522+
))
523+
}
524+
}
525+
}
526+
}
527+
307528
#[cfg(test)]
308529
mod tests {
309530
use crate::parser::Parsed;
310-
use crate::query::queryable::{convert_js_path, Queryable};
531+
use crate::query::queryable::{convert_js_path, Queryable, QueryableDeletable};
311532
use crate::query::Queried;
312533
use crate::JsonPath;
313-
use serde_json::json;
534+
use serde_json::{json, Value};
314535

315536
#[test]
316537
fn in_smoke() -> Queried<()> {
@@ -446,4 +667,114 @@ mod tests {
446667

447668
Ok(())
448669
}
670+
#[test]
671+
fn test_delete_object_field() {
672+
let mut data = json!({
673+
"users": {
674+
"alice": {"age": 30},
675+
"bob": {"age": 25}
676+
}
677+
});
678+
679+
let deleted = data.delete_by_path("$.users.alice").unwrap();
680+
assert_eq!(deleted, 1);
681+
682+
let expected = json!({
683+
"users": {
684+
"bob": {"age": 25}
685+
}
686+
});
687+
assert_eq!(data, expected);
688+
}
689+
690+
#[test]
691+
fn test_delete_array_element() {
692+
let mut data = json!({
693+
"numbers": [1, 2, 3, 4, 5]
694+
});
695+
696+
let deleted = data.delete_by_path("$.numbers[2]").unwrap();
697+
assert_eq!(deleted, 1);
698+
699+
let expected = json!({
700+
"numbers": [1, 2, 4, 5]
701+
});
702+
assert_eq!(data, expected);
703+
}
704+
705+
#[test]
706+
fn test_delete_multiple_elements() {
707+
let mut data = json!({
708+
"users": [
709+
{"name": "Alice", "age": 30},
710+
{"name": "Bob", "age": 25},
711+
{"name": "Charlie", "age": 35},
712+
{"name": "David", "age": 22}
713+
]
714+
});
715+
716+
// Delete users older than 24
717+
let deleted = data.delete_by_path("$.users[?(@.age > 24)]").unwrap();
718+
assert_eq!(deleted, 3);
719+
720+
let expected = json!({
721+
"users": [
722+
{"name": "David", "age": 22}
723+
]
724+
});
725+
assert_eq!(data, expected);
726+
}
727+
728+
#[test]
729+
fn test_delete_nested_fields() {
730+
let mut data = json!({
731+
"company": {
732+
"departments": {
733+
"engineering": {"budget": 100000},
734+
"marketing": {"budget": 50000},
735+
"hr": {"budget": 30000}
736+
}
737+
}
738+
});
739+
740+
let deleted = data.delete_by_path("$.company.departments.marketing").unwrap();
741+
assert_eq!(deleted, 1);
742+
743+
let expected = json!({
744+
"company": {
745+
"departments": {
746+
"engineering": {"budget": 100000},
747+
"hr": {"budget": 30000}
748+
}
749+
}
750+
});
751+
assert_eq!(data, expected);
752+
}
753+
754+
#[test]
755+
fn test_delete_nonexistent_path() {
756+
let mut data = json!({
757+
"test": "value"
758+
});
759+
760+
let deleted = data.delete_by_path("$.nonexistent").unwrap();
761+
assert_eq!(deleted, 0);
762+
763+
// Data should remain unchanged
764+
let expected = json!({
765+
"test": "value"
766+
});
767+
assert_eq!(data, expected);
768+
}
769+
770+
#[test]
771+
fn test_delete_root() {
772+
let mut data = json!({
773+
"test": "value"
774+
});
775+
776+
let deleted = data.delete_single("$").unwrap();
777+
assert_eq!(deleted, true);
778+
assert_eq!(data, Value::Null);
779+
}
449780
}

0 commit comments

Comments
 (0)