add dot_has

master
Ondřej Hruška 2 years ago
parent e564922c0a
commit 1d929a83c8
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      Cargo.toml
  2. 14
      README.md
  3. 122
      src/lib.rs

@ -1,6 +1,6 @@
[package]
name = "json_dotpath"
version = "1.0.3"
version = "1.1.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
license = "MIT"

@ -4,6 +4,10 @@ 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.
@ -63,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`.
@ -118,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,
@ -126,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.

@ -112,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
@ -229,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),
@ -261,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)
}
_ => {
@ -366,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);
@ -516,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);
@ -1101,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
@ -1143,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;

Loading…
Cancel
Save