Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d929a83c8
|
||
|
|
e564922c0a | ||
|
|
c8a1166789
|
||
|
|
82fa955359
|
+2
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "json_dotpath"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
@@ -16,5 +16,4 @@ categories = [
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
failure = "0.1.7"
|
||||
failure_derive = "0.1.7"
|
||||
thiserror = "1"
|
||||
|
||||
@@ -4,6 +4,14 @@ Access members of nested JSON arrays and objects using "dotted paths".
|
||||
|
||||
## Changes
|
||||
|
||||
### 1.1.0
|
||||
|
||||
Added `dot_has()` and `dot_has_checked()`
|
||||
|
||||
### 1.0.3
|
||||
|
||||
Replaced `failure` with `thiserror`, implement `std::error::Error` for the error type.
|
||||
|
||||
### 1.0.0
|
||||
|
||||
The API changed to return `Result<Option<T>>` instead of panicking internally on error.
|
||||
@@ -49,7 +57,7 @@ Five principal methods are added by the `DotPaths` trait to `serde_json::Value`,
|
||||
- `dot_get_mut(path)` - get a mutable reference to an element of the JSON object
|
||||
- `dot_set(path, value)` - set a new value, dropping the original (if any)
|
||||
- `dot_replace(path, value)` - set a new value, returning the original (if any)
|
||||
- `dot_take(path, value)` - remove a value by path, returning it (if any)
|
||||
- `dot_take(path)` - remove a value by path, returning it (if any)
|
||||
|
||||
`dot_set()` supports array manipulation syntax not found in the other methods, namely the
|
||||
`>n` and `<n` pattern to insert an element before or after an index, shifting the rest of the `Vec`.
|
||||
@@ -59,6 +67,8 @@ Five principal methods are added by the `DotPaths` trait to `serde_json::Value`,
|
||||
- `dot_remove(path)` - remove a value by path
|
||||
- `dot_get_or(path, def)` - get value, or a custom default
|
||||
- `dot_get_or_default(path)` - get value, or `Default::default()`
|
||||
- `dot_has_checked(path)` - checks if a path is valid and a value exists there
|
||||
- `dot_has(path)` - the same as above, but errors silently become `false`
|
||||
|
||||
All methods are generic and take care of serializing and deserializing the stored / retrieved
|
||||
data. `dot_get_mut()` is an exception and returns `&mut Value`.
|
||||
@@ -114,7 +124,7 @@ See unit tests for more examples.
|
||||
|
||||
### Special handling of Null
|
||||
|
||||
JSON null in an object can transparently become an array or object by setting it's members (even nested),
|
||||
JSON null can transparently become an array or object by setting it's members (even nested),
|
||||
as if it was an empty array or object. Whether it should become an array or object depends on the key used to index into it.
|
||||
|
||||
- numeric key turns null into an array (only `0` and the special array operators are allowed,
|
||||
@@ -122,6 +132,8 @@ as if it was an empty array or object. Whether it should become an array or obje
|
||||
- any other key turns it into a map
|
||||
- any key starting with an escape creates a map as well (e.g. `\0.aaa` turns `null` into `{"0": {"aaa": …} }` )
|
||||
|
||||
JSON null is considered an empty value and is transformed into `Ok(None)` when retrieved, as it can not be deserialized.
|
||||
JSON null is considered an empty value and is transformed into `Ok(None)` when retrieved,
|
||||
as it can not be deserialized.
|
||||
|
||||
Setting a value to `Value::Null` works as expected and places a JSON null in the object.
|
||||
Setting a value to `Value::Null` works as expected and places a JSON null in the object, the same
|
||||
applies when getting a mutable reference.
|
||||
|
||||
+217
-35
@@ -2,41 +2,34 @@ use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Map, Value};
|
||||
use std::mem;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
|
||||
/// Errors from dot_path methods
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Path hit a value in the JSON object that is not array or map
|
||||
/// and could not continue the traversal.
|
||||
///
|
||||
/// (e.g. `foo.bar` in `{"foo": 123}`)
|
||||
#[fail(display = "Unexpected value reached while traversing path")]
|
||||
#[error("Unexpected value reached while traversing path")]
|
||||
BadPathElement,
|
||||
|
||||
/// Array index out of range
|
||||
#[fail(display = "Invalid array index: {}", _0)]
|
||||
#[error("Invalid array index: {0}")]
|
||||
BadIndex(usize),
|
||||
|
||||
/// Invalid (usually empty) key used in Map or Array.
|
||||
/// If the key is valid but out of bounds, `BadIndex` will be used.
|
||||
#[fail(display = "Invalid key: {}", _0)]
|
||||
#[error("Invalid key: {0}")]
|
||||
InvalidKey(String),
|
||||
|
||||
/// Error serializing or deserializing a value
|
||||
#[fail(display = "Invalid array or map key")]
|
||||
SerdeError(#[fail(cause)] serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Error::SerdeError(e)
|
||||
}
|
||||
#[error("Invalid array or map key")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
use crate::Error::{BadIndex, BadPathElement, InvalidKey};
|
||||
@@ -119,6 +112,27 @@ pub trait DotPaths {
|
||||
self.dot_get_or(path, T::default())
|
||||
}
|
||||
|
||||
/// Check if a value exists under a dotted path.
|
||||
/// Returns error if the path is invalid, a string key was used to index into an array.
|
||||
///
|
||||
/// # Special symbols
|
||||
/// - `>` ... last element of an array
|
||||
/// - `<` ... first element of an array (same as `0`)
|
||||
fn dot_has_checked(&self, path: &str) -> Result<bool>;
|
||||
|
||||
/// Check if a value exists under a dotted path.
|
||||
/// Returns false also when the path is invalid.
|
||||
///
|
||||
/// Use `dot_has_checked` if you want to distinguish non-existent values from path errors.
|
||||
///
|
||||
/// # Special symbols
|
||||
/// - `>` ... last element of an array
|
||||
/// - `<` ... first element of an array (same as `0`)
|
||||
fn dot_has(&self, path: &str) -> bool {
|
||||
self.dot_has_checked(path)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get a mutable reference to an item
|
||||
///
|
||||
/// If the path does not exist but a value on the path can be created (i.e. because the path
|
||||
@@ -145,14 +159,7 @@ pub trait DotPaths {
|
||||
/// - `<` ... first element of an array (same as `0`)
|
||||
fn dot_set<T>(&mut self, path: &str, value: T) -> Result<()>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
// This is a default implementation.
|
||||
// Vec uses a custom implementation to support the special syntax.
|
||||
|
||||
let _ = self.dot_replace::<T, Value>(path, value)?; // Original value is dropped
|
||||
Ok(())
|
||||
}
|
||||
T: Serialize;
|
||||
|
||||
/// Replace a value by path with a new value.
|
||||
/// The value types do not have to match.
|
||||
@@ -243,6 +250,22 @@ impl DotPaths for serde_json::Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn dot_has_checked(&self, path: &str) -> Result<bool> {
|
||||
match self {
|
||||
Value::Array(vec) => vec.dot_has_checked(path),
|
||||
Value::Object(map) => map.dot_has_checked(path),
|
||||
Value::Null => Ok(false),
|
||||
_ => {
|
||||
if path.is_empty() {
|
||||
Ok(true)
|
||||
} else {
|
||||
// Path continues, but we can't traverse into a scalar
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dot_get_mut(&mut self, path: &str) -> Result<&mut Value> {
|
||||
match self {
|
||||
Value::Array(vec) => vec.dot_get_mut(path),
|
||||
@@ -275,7 +298,7 @@ impl DotPaths for serde_json::Value {
|
||||
Value::Object(map) => map.dot_replace(path, value),
|
||||
Value::Null => {
|
||||
// spawn new
|
||||
mem::replace(self, new_by_path_root(path, value)?);
|
||||
*self = new_by_path_root(path, value)?;
|
||||
Ok(None)
|
||||
}
|
||||
_ => {
|
||||
@@ -321,6 +344,7 @@ impl DotPaths for serde_json::Value {
|
||||
match self {
|
||||
// Special case for Vec, which implements additional path symbols
|
||||
Value::Array(a) => a.dot_set(path, value),
|
||||
Value::Object(m) => m.dot_set(path, value),
|
||||
_ => {
|
||||
let _ = self.dot_replace::<T, Value>(path, value)?; // Original value is dropped
|
||||
Ok(())
|
||||
@@ -379,6 +403,26 @@ impl DotPaths for serde_json::Map<String, serde_json::Value> {
|
||||
}
|
||||
}
|
||||
|
||||
fn dot_has_checked(&self, path: &str) -> Result<bool> {
|
||||
let (my, sub) = path_split(path);
|
||||
|
||||
if my.is_empty() {
|
||||
return Err(InvalidKey(my));
|
||||
}
|
||||
|
||||
if let Some(sub_path) = sub {
|
||||
match self.get(&my).null_to_none() {
|
||||
None => Ok(false),
|
||||
Some(child) => child.dot_has_checked(sub_path),
|
||||
}
|
||||
} else {
|
||||
match self.get(&my).null_to_none() {
|
||||
None => Ok(false),
|
||||
Some(_) => Ok(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::collapsible_if)]
|
||||
fn dot_get_mut(&mut self, path: &str) -> Result<&mut Value> {
|
||||
let (my, sub) = path_split(path);
|
||||
@@ -412,6 +456,29 @@ impl DotPaths for serde_json::Map<String, serde_json::Value> {
|
||||
}
|
||||
}
|
||||
|
||||
fn dot_set<T>(&mut self, path: &str, value: T) -> Result<()> where
|
||||
T: Serialize {
|
||||
let (my, sub) = path_split(path);
|
||||
|
||||
if my.is_empty() {
|
||||
return Err(InvalidKey(my));
|
||||
}
|
||||
|
||||
if let Some(subpath) = sub {
|
||||
if self.contains_key(&my) {
|
||||
self.get_mut(&my).unwrap().dot_set(subpath, value)
|
||||
} else {
|
||||
// Build new subpath
|
||||
let _ = self.insert(my, new_by_path_root(subpath, value)?); // always returns None here
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
let packed = serde_json::to_value(value)?;
|
||||
self.insert(my, packed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn dot_replace<NEW, OLD>(&mut self, path: &str, value: NEW) -> Result<Option<OLD>>
|
||||
where
|
||||
NEW: Serialize,
|
||||
@@ -425,10 +492,7 @@ impl DotPaths for serde_json::Map<String, serde_json::Value> {
|
||||
|
||||
if let Some(subpath) = sub {
|
||||
if self.contains_key(&my) {
|
||||
match self.get_mut(&my) {
|
||||
None => Ok(None),
|
||||
Some(m) => m.dot_replace(subpath, value),
|
||||
}
|
||||
self.get_mut(&my).unwrap().dot_replace(subpath, value)
|
||||
} else {
|
||||
// Build new subpath
|
||||
let _ = self.insert(my, new_by_path_root(subpath, value)?); // always returns None here
|
||||
@@ -487,7 +551,9 @@ impl DotPaths for Vec<serde_json::Value> {
|
||||
let index: usize = match my.as_str() {
|
||||
">" => self.len() - 1, // non-empty checked above
|
||||
"<" => 0,
|
||||
_ => my.parse().map_err(|_| InvalidKey(my))?,
|
||||
_ => my.parse().map_err(|_| {
|
||||
InvalidKey(my)
|
||||
})?,
|
||||
};
|
||||
|
||||
if index >= self.len() {
|
||||
@@ -507,6 +573,43 @@ impl DotPaths for Vec<serde_json::Value> {
|
||||
}
|
||||
}
|
||||
|
||||
fn dot_has_checked(&self, path: &str) -> Result<bool> {
|
||||
let (my, sub) = path_split(path);
|
||||
|
||||
if my.is_empty() {
|
||||
return Err(InvalidKey(my));
|
||||
}
|
||||
|
||||
if self.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let index: usize = match my.as_str() {
|
||||
">" => self.len() - 1, // non-empty checked above
|
||||
"<" => 0,
|
||||
_ => my.parse().map_err(|_| {
|
||||
InvalidKey(my)
|
||||
})?,
|
||||
};
|
||||
|
||||
if index >= self.len() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(subpath) = sub {
|
||||
match self.get(index).null_to_none() {
|
||||
None => Ok(false),
|
||||
Some(child) => child.dot_has_checked(subpath),
|
||||
}
|
||||
} else {
|
||||
match self.get(index).null_to_none() {
|
||||
// null is reported as unset
|
||||
None => Ok(false),
|
||||
Some(_) => Ok(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::collapsible_if)]
|
||||
fn dot_get_mut(&mut self, path: &str) -> Result<&mut Value> {
|
||||
let (my, sub) = path_split(path);
|
||||
@@ -524,7 +627,9 @@ impl DotPaths for Vec<serde_json::Value> {
|
||||
}
|
||||
}
|
||||
"<" => 0,
|
||||
_ => my.parse().map_err(|_| InvalidKey(my))?,
|
||||
_ => my.parse().map_err(|_| {
|
||||
InvalidKey(my)
|
||||
})?,
|
||||
};
|
||||
|
||||
if index > self.len() {
|
||||
@@ -593,14 +698,23 @@ impl DotPaths for Vec<serde_json::Value> {
|
||||
_ if my.starts_with('>') => {
|
||||
// insert after
|
||||
insert = true;
|
||||
(&my[1..]).parse::<usize>().map_err(|_| InvalidKey(my_s))? + 1
|
||||
(&my[1..]).parse::<usize>()
|
||||
.map_err(|_| {
|
||||
InvalidKey(my_s)
|
||||
})? + 1
|
||||
}
|
||||
_ if my.starts_with('<') => {
|
||||
// insert before
|
||||
insert = true;
|
||||
(&my[1..]).parse::<usize>().map_err(|_| InvalidKey(my_s))?
|
||||
(&my[1..]).parse::<usize>()
|
||||
.map_err(|_| {
|
||||
InvalidKey(my_s)
|
||||
})?
|
||||
}
|
||||
_ => my.parse::<usize>().map_err(|_| InvalidKey(my_s))?,
|
||||
_ => my.parse::<usize>()
|
||||
.map_err(|_| {
|
||||
InvalidKey(my_s)
|
||||
})?,
|
||||
};
|
||||
|
||||
if index > self.len() {
|
||||
@@ -657,7 +771,9 @@ impl DotPaths for Vec<serde_json::Value> {
|
||||
}
|
||||
}
|
||||
"<" => 0,
|
||||
_ => my.parse().map_err(|_| InvalidKey(my))?,
|
||||
_ => my.parse().map_err(|_| {
|
||||
InvalidKey(my)
|
||||
})?,
|
||||
};
|
||||
|
||||
if index >= self.len() {
|
||||
@@ -700,7 +816,9 @@ impl DotPaths for Vec<serde_json::Value> {
|
||||
}
|
||||
}
|
||||
"<" => 0,
|
||||
_ => my.parse().map_err(|_| InvalidKey(my))?,
|
||||
_ => my.parse().map_err(|_| {
|
||||
InvalidKey(my)
|
||||
})?,
|
||||
};
|
||||
|
||||
if index >= self.len() {
|
||||
@@ -838,6 +956,46 @@ mod tests {
|
||||
assert_eq!(json!([[["first"]], [["second"]]]), vec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_append1() {
|
||||
let mut test_array = Value::Array(vec![]);
|
||||
test_array.dot_set(">>", Value::String(String::from("Go to class"))).unwrap();
|
||||
test_array.dot_set(">>", Value::String(String::from("Fish"))).unwrap();
|
||||
assert_eq!(json!(["Go to class","Fish"]), test_array);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_append2() {
|
||||
let mut test_array = Value::Array(vec![]);
|
||||
test_array.dot_set("+", Value::String(String::from("Go to class"))).unwrap();
|
||||
test_array.dot_set("+", Value::String(String::from("Fish"))).unwrap();
|
||||
assert_eq!(json!(["Go to class","Fish"]), test_array);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_append_in_object1() {
|
||||
let mut test_inner_array = Value::Null;
|
||||
|
||||
test_inner_array.dot_set("todos.+", Value::String(String::from("Go to class"))).unwrap();
|
||||
assert_eq!(json!({"todos" : ["Go to class"] }), test_inner_array);
|
||||
|
||||
test_inner_array.dot_set("todos.+", Value::String(String::from("Fish"))).unwrap();
|
||||
assert_eq!(json!({"todos" : ["Go to class","Fish"] }), test_inner_array);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_append_in_object2() {
|
||||
let mut test_inner_array = Value::Null;
|
||||
test_inner_array.dot_set("name", Value::String(String::from("Google"))).unwrap();
|
||||
assert_eq!(json!({"name" : "Google"}), test_inner_array);
|
||||
|
||||
test_inner_array.dot_set("todos.+", Value::String(String::from("Go to class"))).unwrap();
|
||||
assert_eq!(json!({"name" : "Google", "todos" : ["Go to class"] }), test_inner_array);
|
||||
|
||||
test_inner_array.dot_set("todos.+", Value::String(String::from("Fish"))).unwrap();
|
||||
assert_eq!(json!({"name" : "Google", "todos" : ["Go to class","Fish"] }), test_inner_array);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_vec() {
|
||||
let vec = json!([
|
||||
@@ -1037,7 +1195,7 @@ mod tests {
|
||||
// Borrow Null as mutable
|
||||
let mut obj = Value::Null;
|
||||
let m = obj.dot_get_mut("").unwrap();
|
||||
std::mem::replace(m, Value::from(123));
|
||||
*m = Value::from(123);
|
||||
assert_eq!(Value::from(123), obj);
|
||||
|
||||
// Create a parents path
|
||||
@@ -1079,6 +1237,30 @@ mod tests {
|
||||
assert_eq!(json!([{"foo": {"bar": {"dog": "cat"}}}]), Value::Array(obj));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has() {
|
||||
let value = json!({
|
||||
"one": "two",
|
||||
"x": [1, 2, {"foo": 123}]
|
||||
});
|
||||
assert!(value.dot_has("one"));
|
||||
assert!(!value.dot_has("two"));
|
||||
assert!(value.dot_has("x"));
|
||||
assert!(value.dot_has("x.0"));
|
||||
assert!(value.dot_has("x.<"));
|
||||
assert!(value.dot_has("x.>"));
|
||||
assert!(value.dot_has("x.>.foo"));
|
||||
assert!(!value.dot_has("x.banana"));
|
||||
assert!(!value.dot_has("x.>.foo.bar"));
|
||||
assert!(value.dot_has_checked("x.banana").is_err());
|
||||
|
||||
if let Ok(false) = value.dot_has_checked("x.9999") {
|
||||
//
|
||||
} else {
|
||||
panic!("dot_has_checked failed");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stamps() {
|
||||
let mut stamps = Value::Null;
|
||||
|
||||
Reference in New Issue
Block a user