diff --git a/CHANGELOG.md b/CHANGELOG.md index 2665f40..6026cd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,4 +57,6 @@ - **`0.7.2`** - add JsonLike trait - **`0.7.3`** - - make some methods public \ No newline at end of file + - make some methods public +- **`0.7.5`** + - add reference and reference_mut methods \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 5497a31..b4f0590 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jsonpath-rust" description = "The library provides the basic functionality to find the set of the data according to the filtering query." -version = "0.7.3" +version = "0.7.5" authors = ["BorisZhguchev "] edition = "2021" license = "MIT" @@ -16,10 +16,9 @@ serde_json = "1.0" regex = "1" pest = "2.0" pest_derive = "2.0" -thiserror = "1.0.50" +thiserror = "2.0.9" [dev-dependencies] -lazy_static = "1.0" criterion = "0.5.1" [[bench]] diff --git a/README.md b/README.md index 3bc8070..f1d41ed 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,61 @@ https://docs.rs/jsonpath-rust/latest/jsonpath_rust/parser/model/enum.JsonPathInd The library provides a trait `JsonLike` that can be implemented for any type. This allows you to use the `JsonPath` methods on your own types. +### Update the JsonLike structure by path + +The library does not provide the functionality to update the json structure in the query itself. +Instead, the library provides the ability to update the json structure by the path. +Thus, the user needs to find a path for the `JsonLike` structure and update it manually. + +There are two methods in the `JsonLike` trait: + +- `reference_mut` - returns a mutable reference to the element by the path +- `reference` - returns a reference to the element by the path + They accept a `JsonPath` instance and return a `Option<&mut Value>` or `Option<&Value>` respectively. + The path is supported with the limited elements namely only the elements with the direct access: +- root +- field +- index + The path can be obtained manually or `find_as_path` method can be used. + +```rust +#[test] +fn update_by_path_test() -> Result<(), JsonPathParserError> { + let mut json = json!([ + {"verb": "RUN","distance":[1]}, + {"verb": "TEST"}, + {"verb": "DO NOT RUN"} + ]); + + let path: Box = Box::from(JsonPath::try_from("$.[?(@.verb == 'RUN')]")?); + let elem = path + .find_as_path(&json) + .get(0) + .cloned() + .ok_or(JsonPathParserError::InvalidJsonPath("".to_string()))?; + + if let Some(v) = json + .reference_mut(elem)? + .and_then(|v| v.as_object_mut()) + .and_then(|v| v.get_mut("distance")) + .and_then(|v| v.as_array_mut()) + { + v.push(json!(2)) + } + + assert_eq!( + json, + json!([ + {"verb": "RUN","distance":[1,2]}, + {"verb": "TEST"}, + {"verb": "DO NOT RUN"} + ]) + ); + + Ok(()) +} +``` + ## How to contribute TBD diff --git a/src/jsonpath.rs b/src/jsonpath.rs index 18ba64f..39be3c4 100644 --- a/src/jsonpath.rs +++ b/src/jsonpath.rs @@ -1,9 +1,8 @@ use crate::path::json_path_instance; use crate::path::JsonLike; -use crate::JsonPath; use crate::JsonPathValue; -use crate::JsonPathValue::NoValue; use crate::JsonPtr; +use crate::{JsonPath, JsonPathStr}; impl JsonPath where @@ -39,7 +38,7 @@ where let has_v: Vec> = res.into_iter().filter(|v| v.has_value()).collect(); if has_v.is_empty() { - vec![NoValue] + vec![JsonPathValue::NoValue] } else { has_v } @@ -105,33 +104,31 @@ where /// /// ## Example /// ```rust - /// use jsonpath_rust::{JsonPath, JsonPathValue}; + /// use jsonpath_rust::{JsonPathStr, JsonPath, JsonPathValue}; /// use serde_json::{Value, json}; /// # use std::str::FromStr; /// /// let data = json!({"first":{"second":[{"active":1},{"passive":1}]}}); /// let path = JsonPath::try_from("$.first.second[?(@.active)]").unwrap(); - /// let slice_of_data: Value = path.find_as_path(&data); + /// let slice_of_data: Vec = path.find_as_path(&data); /// /// let expected_path = "$.['first'].['second'][0]".to_string(); - /// assert_eq!(slice_of_data, Value::Array(vec![Value::String(expected_path)])); + /// assert_eq!(slice_of_data, vec![expected_path]); /// ``` - pub fn find_as_path(&self, json: &T) -> T { - T::array( - self.find_slice(json) - .into_iter() - .flat_map(|v| v.to_path()) - .map(|v| v.into()) - .collect(), - ) + pub fn find_as_path(&self, json: &T) -> Vec { + self.find_slice(json) + .into_iter() + .flat_map(|v| v.to_path()) + .collect() } } #[cfg(test)] mod tests { + use crate::path::JsonLike; use crate::JsonPathQuery; use crate::JsonPathValue::{NoValue, Slice}; - use crate::{jp_v, JsonPath, JsonPathValue}; + use crate::{jp_v, JsonPath, JsonPathParserError, JsonPathValue}; use serde_json::{json, Value}; use std::ops::Deref; @@ -901,17 +898,39 @@ mod tests { ); } - // #[test] - // fn no_value_len_field_test() { - // let json: Box = - // Box::new(json!([{"verb": "TEST","a":[1,2,3]},{"verb": "TEST","a":[1,2,3]},{"verb": "TEST"}, {"verb": "RUN"}])); - // let path: Box = Box::from( - // JsonPath::try_from("$.[?(@.verb == 'TEST')].a.length()") - // .expect("the path is correct"), - // ); - // let finder = JsonPathFinder::new(json, path); - // - // let v = finder.find_slice(); - // assert_eq!(v, vec![NewValue(json!(3))]); - // } + #[test] + fn update_by_path_test() -> Result<(), JsonPathParserError> { + let mut json = json!([ + {"verb": "RUN","distance":[1]}, + {"verb": "TEST"}, + {"verb": "DO NOT RUN"} + ]); + + let path: Box = Box::from(JsonPath::try_from("$.[?(@.verb == 'RUN')]")?); + let elem = path + .find_as_path(&json) + .first() + .cloned() + .ok_or(JsonPathParserError::InvalidJsonPath("".to_string()))?; + + if let Some(v) = json + .reference_mut(elem)? + .and_then(|v| v.as_object_mut()) + .and_then(|v| v.get_mut("distance")) + .and_then(|v| v.as_array_mut()) + { + v.push(json!(2)) + } + + assert_eq!( + json, + json!([ + {"verb": "RUN","distance":[1,2]}, + {"verb": "TEST"}, + {"verb": "DO NOT RUN"} + ]) + ); + + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index c5098bf..e34d721 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,6 +166,7 @@ pub trait JsonPathQuery { /// Json paths may return either pointers to the original json or new data. This custom pointer type allows us to handle both cases. /// Unlike JsonPathValue, this type does not represent NoValue to allow the implementation of Deref. +#[derive(Debug, PartialEq, Clone)] pub enum JsonPtr<'a, Data> { /// The slice of the initial json data Slice(&'a Data), @@ -261,7 +262,7 @@ macro_rules! jp_v { } /// Represents the path of the found json data -type JsPathStr = String; +pub type JsonPathStr = String; pub fn jsp_idx(prefix: &str, idx: usize) -> String { format!("{}[{}]", prefix, idx) @@ -275,7 +276,7 @@ pub fn jsp_obj(prefix: &str, key: &str) -> String { #[derive(Debug, PartialEq, Clone)] pub enum JsonPathValue<'a, Data> { /// The slice of the initial json data - Slice(&'a Data, JsPathStr), + Slice(&'a Data, JsonPathStr), /// The new data that was generated from the input data (like length operator) NewValue(Data), /// The absent value that indicates the input data is not matched to the given json path (like the absent fields) @@ -293,7 +294,7 @@ impl<'a, Data: Clone + Debug + Default> JsonPathValue<'a, Data> { } /// Transforms given value into path - pub fn to_path(self) -> Option { + pub fn to_path(self) -> Option { match self { Slice(_, path) => Some(path), _ => None, @@ -313,7 +314,7 @@ impl<'a, Data> JsonPathValue<'a, Data> { !input.is_empty() && input.iter().filter(|v| v.has_value()).count() == 0 } - pub fn map_vec(data: Vec<(&'a Data, JsPathStr)>) -> Vec> { + pub fn map_vec(data: Vec<(&'a Data, JsonPathStr)>) -> Vec> { data.into_iter() .map(|(data, pref)| Slice(data, pref)) .collect() @@ -321,7 +322,7 @@ impl<'a, Data> JsonPathValue<'a, Data> { pub fn map_slice(self, mapper: F) -> Vec> where - F: FnOnce(&'a Data, JsPathStr) -> Vec<(&'a Data, JsPathStr)>, + F: FnOnce(&'a Data, JsonPathStr) -> Vec<(&'a Data, JsonPathStr)>, { match self { Slice(r, pref) => mapper(r, pref) @@ -336,7 +337,7 @@ impl<'a, Data> JsonPathValue<'a, Data> { pub fn flat_map_slice(self, mapper: F) -> Vec> where - F: FnOnce(&'a Data, JsPathStr) -> Vec>, + F: FnOnce(&'a Data, JsonPathStr) -> Vec>, { match self { Slice(r, pref) => mapper(r, pref), @@ -357,7 +358,7 @@ impl<'a, Data> JsonPathValue<'a, Data> { }) .collect() } - pub fn vec_as_pair(input: Vec>) -> Vec<(&'a Data, JsPathStr)> { + pub fn vec_as_pair(input: Vec>) -> Vec<(&'a Data, JsonPathStr)> { input .into_iter() .filter_map(|v| match v { diff --git a/src/parser/errors.rs b/src/parser/errors.rs index e6a004d..7e89ec9 100644 --- a/src/parser/errors.rs +++ b/src/parser/errors.rs @@ -24,4 +24,6 @@ pub enum JsonPathParserError { InvalidTopLevelRule(Rule), #[error("Failed to get inner pairs for {0}")] EmptyInner(String), + #[error("Invalid json path: {0}")] + InvalidJsonPath(String), } diff --git a/src/path/mod.rs b/src/path/mod.rs index b15b6ab..9b67555 100644 --- a/src/path/mod.rs +++ b/src/path/mod.rs @@ -1,10 +1,11 @@ use std::fmt::Debug; -use crate::{jsp_idx, jsp_obj, JsonPathValue}; +use crate::{jsp_idx, jsp_obj, JsonPathParserError, JsonPathStr, JsonPathValue}; use regex::Regex; use serde_json::{json, Value}; use crate::parser::model::{Function, JsonPath, JsonPathIndex, Operand}; +use crate::parser::parse_json_path; pub use crate::path::index::{ArrayIndex, ArraySlice, Current, FilterPath, UnionIndex}; pub use crate::path::top::ObjectField; use crate::path::top::*; @@ -94,24 +95,68 @@ pub trait JsonLike: /// Creates an array from a vector of elements. fn array(data: Vec) -> Self; + + /// Retrieves a reference to the element at the specified path. + /// The path is specified as a string and can be obtained from the query. + /// + /// # Arguments + /// * `path` - A json path to the element specified as a string (root, field, index only). + fn reference(&self, path: T) -> Result, JsonPathParserError> + where + T: Into; + + /// Retrieves a mutable reference to the element at the specified path. + /// + /// # Arguments + /// * `path` - A json path to the element specified as a string (root, field, index only). + /// + /// # Examples + /// + /// ``` + /// use serde_json::json; + /// use jsonpath_rust::{JsonPath, JsonPathParserError}; + /// use jsonpath_rust::path::JsonLike; + /// + /// let mut json = json!([ + /// {"verb": "RUN","distance":[1]}, + /// {"verb": "TEST"}, + /// {"verb": "DO NOT RUN"} + /// ]); + /// + /// let path: Box = Box::from(JsonPath::try_from("$.[?(@.verb == 'RUN')]").unwrap()); + /// let elem = path + /// .find_as_path(&json) + /// .get(0) + /// .cloned() + /// .ok_or(JsonPathParserError::InvalidJsonPath("".to_string())).unwrap(); + /// + /// if let Some(v) = json + /// .reference_mut(elem).unwrap() + /// .and_then(|v| v.as_object_mut()) + /// .and_then(|v| v.get_mut("distance")) + /// .and_then(|v| v.as_array_mut()) + /// { + /// v.push(json!(2)) + /// } + /// + /// assert_eq!( + /// json, + /// json!([ + /// {"verb": "RUN","distance":[1,2]}, + /// {"verb": "TEST"}, + /// {"verb": "DO NOT RUN"} + /// ]) + /// ); + /// ``` + fn reference_mut(&mut self, path: T) -> Result, JsonPathParserError> + where + T: Into; } impl JsonLike for Value { - fn is_array(&self) -> bool { - self.is_array() - } - fn array(data: Vec) -> Self { - Value::Array(data) - } - - fn null() -> Self { - Value::Null - } - fn get(&self, key: &str) -> Option<&Self> { self.get(key) } - fn itre(&self, pref: String) -> Vec> { let res = match self { Value::Array(elems) => { @@ -147,6 +192,7 @@ impl JsonLike for Value { fn init_with_usize(cnt: usize) -> Self { json!(cnt) } + fn deep_flatten(&self, pref: String) -> Vec<(&Self, String)> { let mut acc = vec![]; match self { @@ -168,6 +214,7 @@ impl JsonLike for Value { } acc } + fn deep_path_by_key<'a>( &'a self, key: ObjectField<'a, Self>, @@ -200,7 +247,9 @@ impl JsonLike for Value { fn as_u64(&self) -> Option { self.as_u64() } - + fn is_array(&self) -> bool { + self.is_array() + } fn as_array(&self) -> Option<&Vec> { self.as_array() } @@ -362,6 +411,46 @@ impl JsonLike for Value { left.iter().zip(right).map(|(a, b)| a.eq(&b)).all(|a| a) } } + + fn null() -> Self { + Value::Null + } + + fn array(data: Vec) -> Self { + Value::Array(data) + } + + fn reference(&self, path: T) -> Result, JsonPathParserError> + where + T: Into, + { + Ok(self.pointer(&path_to_json_path(path.into())?)) + } + + fn reference_mut(&mut self, path: T) -> Result, JsonPathParserError> + where + T: Into, + { + Ok(self.pointer_mut(&path_to_json_path(path.into())?)) + } +} + +fn path_to_json_path(path: JsonPathStr) -> Result { + convert_part(&parse_json_path::(path.as_str())?) +} + +fn convert_part(path: &JsonPath) -> Result { + match path { + JsonPath::Chain(elems) => elems + .iter() + .map(convert_part) + .collect::>(), + + JsonPath::Index(JsonPathIndex::Single(v)) => Ok(format!("/{}", v)), + JsonPath::Field(e) => Ok(format!("/{}", e)), + JsonPath::Root => Ok("".to_string()), + e => Err(JsonPathParserError::InvalidJsonPath(e.to_string())), + } } /// The trait defining the behaviour of processing every separated element. @@ -519,8 +608,9 @@ where #[cfg(test)] mod tests { - - use crate::path::JsonLike; + use crate::path::JsonPathIndex; + use crate::path::{convert_part, JsonLike}; + use crate::{idx, path, JsonPath, JsonPathParserError}; use serde_json::{json, Value}; #[test] @@ -646,4 +736,77 @@ mod tests { assert!(!JsonLike::size(vec![&left3], vec![&right])); assert!(JsonLike::size(vec![&left3], vec![&right1])); } + + #[test] + fn convert_paths() -> Result<(), JsonPathParserError> { + let r = convert_part(&JsonPath::Chain(vec![ + path!($), + path!("abc"), + path!(idx!(1)), + ]))?; + assert_eq!(r, "/abc/1"); + + assert!(convert_part(&JsonPath::Chain(vec![path!($), path!(.."abc")])).is_err()); + + Ok(()) + } + + #[test] + fn test_references() -> Result<(), JsonPathParserError> { + let mut json = json!({ + "a": { + "b": { + "c": 42 + } + } + }); + + let path_str = convert_part(&JsonPath::Chain(vec![path!("a"), path!("b"), path!("c")]))?; + + if let Some(v) = json.pointer_mut(&path_str) { + *v = json!(43); + } + + assert_eq!( + json, + json!({ + "a": { + "b": { + "c": 43 + } + } + }) + ); + + Ok(()) + } + #[test] + fn test_js_reference() -> Result<(), JsonPathParserError> { + let mut json = json!({ + "a": { + "b": { + "c": 42 + } + } + }); + + let path = "$.a.b.c"; + + if let Some(v) = json.reference_mut(path)? { + *v = json!(43); + } + + assert_eq!( + json, + json!({ + "a": { + "b": { + "c": 43 + } + } + }) + ); + + Ok(()) + } }