4 Commits
3 changed files with 235 additions and 42 deletions
+2 -3
View File
@@ -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"
+16 -4
View File
@@ -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
View File
@@ -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;