Skip to content

Commit

Permalink
add ref_mut (#80)
Browse files Browse the repository at this point in the history
* add code

* fix clippy

* opt imp

* correct docs
  • Loading branch information
besok authored Dec 26, 2024
1 parent 0977b0c commit 0a69c56
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 55 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@
- **`0.7.2`**
- add JsonLike trait
- **`0.7.3`**
- make some methods public
- make some methods public
- **`0.7.5`**
- add reference and reference_mut methods
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <zhguchev@gmail.com>"]
edition = "2021"
license = "MIT"
Expand All @@ -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]]
Expand Down
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonPath> = 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
Expand Down
75 changes: 47 additions & 28 deletions src/jsonpath.rs
Original file line number Diff line number Diff line change
@@ -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<T> JsonPath<T>
where
Expand Down Expand Up @@ -39,7 +38,7 @@ where
let has_v: Vec<JsonPathValue<'_, T>> = res.into_iter().filter(|v| v.has_value()).collect();

if has_v.is_empty() {
vec![NoValue]
vec![JsonPathValue::NoValue]
} else {
has_v
}
Expand Down Expand Up @@ -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<JsonPathStr> = 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<JsonPathStr> {
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;

Expand Down Expand Up @@ -901,17 +898,39 @@ mod tests {
);
}

// #[test]
// fn no_value_len_field_test() {
// let json: Box<Value> =
// Box::new(json!([{"verb": "TEST","a":[1,2,3]},{"verb": "TEST","a":[1,2,3]},{"verb": "TEST"}, {"verb": "RUN"}]));
// let path: Box<JsonPath> = 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<JsonPath> = 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(())
}
}
15 changes: 8 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -293,7 +294,7 @@ impl<'a, Data: Clone + Debug + Default> JsonPathValue<'a, Data> {
}

/// Transforms given value into path
pub fn to_path(self) -> Option<JsPathStr> {
pub fn to_path(self) -> Option<JsonPathStr> {
match self {
Slice(_, path) => Some(path),
_ => None,
Expand All @@ -313,15 +314,15 @@ 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<JsonPathValue<'a, Data>> {
pub fn map_vec(data: Vec<(&'a Data, JsonPathStr)>) -> Vec<JsonPathValue<'a, Data>> {
data.into_iter()
.map(|(data, pref)| Slice(data, pref))
.collect()
}

pub fn map_slice<F>(self, mapper: F) -> Vec<JsonPathValue<'a, Data>>
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)
Expand All @@ -336,7 +337,7 @@ impl<'a, Data> JsonPathValue<'a, Data> {

pub fn flat_map_slice<F>(self, mapper: F) -> Vec<JsonPathValue<'a, Data>>
where
F: FnOnce(&'a Data, JsPathStr) -> Vec<JsonPathValue<'a, Data>>,
F: FnOnce(&'a Data, JsonPathStr) -> Vec<JsonPathValue<'a, Data>>,
{
match self {
Slice(r, pref) => mapper(r, pref),
Expand All @@ -357,7 +358,7 @@ impl<'a, Data> JsonPathValue<'a, Data> {
})
.collect()
}
pub fn vec_as_pair(input: Vec<JsonPathValue<'a, Data>>) -> Vec<(&'a Data, JsPathStr)> {
pub fn vec_as_pair(input: Vec<JsonPathValue<'a, Data>>) -> Vec<(&'a Data, JsonPathStr)> {
input
.into_iter()
.filter_map(|v| match v {
Expand Down
2 changes: 2 additions & 0 deletions src/parser/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Loading

0 comments on commit 0a69c56

Please sign in to comment.