initial version

master
Ondřej Hruška 4 years ago
commit 7454ccdfe2
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 4
      .gitignore
  2. 12
      Cargo.toml
  3. 776
      src/lib.rs

4
.gitignore vendored

@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
.idea/

@ -0,0 +1,12 @@
[package]
name = "json_dotpath"
version = "0.1.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1"
serde_derive = "1"
serde_json = "1"

@ -0,0 +1,776 @@
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::{Map, Value};
use std::cmp::Ordering;
use std::mem;
/// Access and mutate nested JSON elements by dotted paths
///
/// The path is composed of keys separated by dots, e.g. `foo.bar.1`.
///
/// Arrays are indexed by numeric strings or special keys (see `dot_get()` and `dot_set()`).
///
/// This trait is implemented for `serde_json::Value`, specifically the
/// `Map`, `Array`, and `Null` variants. Empty path can also be used to access a scalar.
pub trait DotPaths {
/// Get an item by path, if present.
///
/// JSON `null` becomes `None`, same as unpopulated path.
///
/// # Special keys
/// Arrays can be indexed by special keys for reading:
/// - `>` ... last element
///
/// # Panics
/// - If the path attempts to index into a scalar (e.g. `"foo.bar"` in `{"foo": 123}`)
/// - If the path uses invalid key in an array or map
fn dot_get<T>(&self, path: &str) -> Option<T>
where
T: DeserializeOwned;
/// Get an item, or a default value.
///
/// # Special keys
/// see `dot_get()`
///
/// # Panics
/// see `dot_get()`
fn dot_get_or<T>(&self, path: &str, def: T) -> T
where
T: DeserializeOwned,
{
self.dot_get(path).unwrap_or(def)
}
/// Get an item, or a default value using the Default trait
///
/// # Special keys
/// see `dot_get()`
///
/// # Panics
/// see `dot_get()`
fn dot_get_or_default<T>(&self, path: &str) -> T
where
T: DeserializeOwned + Default,
{
self.dot_get(path).unwrap_or_default()
}
/// Get a mutable reference to an item
///
/// # Special keys
/// see `dot_get()`
///
/// # Panics
/// see `dot_get()`
fn dot_get_mut(&mut self, path: &str) -> Option<&mut Value>;
/// Insert an item by path.
///
/// # Special keys
/// Arrays can be indexed by special keys:
/// - `+` or `>` ... append
/// - `-` or `<` ... prepend
/// - `>n` ... insert after an index `n`
/// - `<n` ... insert before an index `n`
///
/// # Panics
/// see `dot_get()`
fn dot_set<T>(&mut self, path: &str, value: T)
where
T: Serialize;
/// Replace a value by path with a new value.
/// The value types do not have to match.
///
/// # Panics
/// see `dot_get()`
fn dot_replace<T, U>(&mut self, path: &str, value: T) -> Option<U>
where
T: Serialize,
U: DeserializeOwned;
/// Get an item using a path, removing it from the store.
/// If no item was stored under this path, then None is returned.
///
/// # Panics
/// see `dot_get()`
fn dot_take<T>(&mut self, path: &str) -> Option<T>
where
T: DeserializeOwned;
/// Remove an item matching a key.
/// Returns true if any item was removed.
///
/// # Panics
/// see `dot_get()`
fn dot_remove(&mut self, path: &str) -> bool {
self.dot_take::<Value>(path).is_some()
}
}
/// Split the path string by dot, if present.
///
/// Returns a tuple of (before_dot, after_dot)
fn path_split(path: &str) -> (&str, Option<&str>) {
let dot = path.find('.');
match dot {
None => (path, None),
Some(pos) => (&path[0..pos], Some(&path[pos + 1..])),
}
}
impl DotPaths for serde_json::Value {
fn dot_get<T>(&self, path: &str) -> Option<T>
where
T: DeserializeOwned,
{
match self {
Value::Array(vec) => vec.dot_get(path),
Value::Object(map) => map.dot_get(path),
_ => {
if path.is_empty() {
serde_json::from_value(self.to_owned()).ok()
} else {
panic!("Node is not array or object!");
}
}
}
}
fn dot_get_mut(&mut self, path: &str) -> Option<&mut Value> {
match self {
Value::Array(vec) => vec.dot_get_mut(path),
Value::Object(map) => map.dot_get_mut(path),
_ => {
if path.is_empty() {
Some(self)
} else {
panic!("Node is not array or object!");
}
}
}
}
fn dot_set<T>(&mut self, path: &str, value: T)
where
T: Serialize,
{
match self {
Value::Array(vec) => {
vec.dot_set(path, value);
}
Value::Object(map) => {
map.dot_set(path, value);
}
Value::Null => {
mem::replace(self, new_by_path_root(path, value));
}
_ => {
if path.is_empty() {
mem::replace(self, serde_json::to_value(value).expect("Serialize error"));
} else {
panic!("Node is not an array, object, or null!");
}
}
}
}
fn dot_replace<T, U>(&mut self, path: &str, value: T) -> Option<U>
where
T: Serialize,
U: DeserializeOwned,
{
match self {
Value::Array(vec) => vec.dot_replace(path, value),
Value::Object(map) => map.dot_replace(path, value),
Value::Null => {
self.dot_set(path, value);
None
}
_ => {
if path.is_empty() {
let new = serde_json::to_value(value).expect("Serialize error");
let old = mem::replace(self, new);
Some(serde_json::from_value(old).expect("Unserialize error"))
} else {
panic!("Node is not an array, object, or null!")
}
}
}
}
fn dot_take<T>(&mut self, path: &str) -> Option<T>
where
T: DeserializeOwned,
{
match self {
Value::Array(vec) => vec.dot_take(path),
Value::Object(map) => map.dot_take(path),
Value::Null => None,
_ => {
if path.is_empty() {
let old = mem::replace(self, Value::Null);
Some(serde_json::from_value(old).expect("Unserialize error"))
} else {
panic!("Node is not an array, object, or null!")
}
}
}
}
}
/// Create a Value::Object or Value::Array based on a nested path.
///
/// Builds the parent path to a non-existent key in set-type operations.
fn new_by_path_root<T>(path: &str, value: T) -> Value
where
T: Serialize,
{
let (sub1, _) = path_split(path);
if sub1 == "0" || sub1 == "+" || sub1 == "<" || sub1 == ">" {
// new vec
let mut new_vec = vec![];
new_vec.dot_set(path, value);
Value::Array(new_vec)
} else {
// new map
let mut new_map = Map::new();
new_map.dot_set(path, value);
Value::Object(new_map)
}
}
/// Check if a key is valid to use by dot paths in Value::Object.
/// The key must start with an alpha character or underscore and must not contain period.
fn validate_map_key(key: &str) {
if key.parse::<usize>().is_ok() {
panic!("Numeric keys are not allowed in maps: {}", key);
}
if !key.starts_with(|p: char| p.is_ascii_alphabetic() || p == '_') || key.contains('.') {
panic!("Invalid map key: {}", key);
}
}
impl DotPaths for serde_json::Map<String, serde_json::Value> {
fn dot_get<T>(&self, path: &str) -> Option<T>
where
T: DeserializeOwned,
{
let (my, sub) = path_split(path);
validate_map_key(my);
if let Some(sub_path) = sub {
self.get(my)
.map(|child| child.dot_get(sub_path)) // this produces Option<Option<T>>
.unwrap_or_default()
} else {
self.get(my)
.map(ToOwned::to_owned)
.map(serde_json::from_value)
.transpose() // Option<Result> to Result<Option>
.unwrap_or_default() // get rid of the result
}
}
fn dot_get_mut(&mut self, path: &str) -> Option<&mut Value> {
let (my, sub) = path_split(path);
validate_map_key(my);
if let Some(sub_path) = sub {
self.get_mut(my)
.map(|m| m.get_mut(sub_path))
.unwrap_or_default()
} else {
self.get_mut(my)
}
}
fn dot_set<T>(&mut self, path: &str, value: T)
where
T: Serialize,
{
let (my, sub) = path_split(path);
validate_map_key(my);
if let Some(subpath) = sub {
if self.contains_key(my) {
self.get_mut(my).unwrap().dot_set(subpath, value);
} else {
self.insert(my.into(), new_by_path_root(subpath, value));
}
} else {
let packed = serde_json::to_value(value).expect("Serialize error");
self.insert(my.into(), packed);
}
}
fn dot_replace<T, U>(&mut self, path: &str, value: T) -> Option<U>
where
T: Serialize,
U: DeserializeOwned,
{
let (my, sub) = path_split(path);
validate_map_key(my);
if let Some(subpath) = sub {
if self.contains_key(my) {
self.get_mut(my).unwrap().dot_replace(subpath, value)
} else {
self.dot_set(path, value);
None
}
} else {
let packed = serde_json::to_value(value).expect("Serialize error");
self.insert(my.to_string(), packed)
.map(serde_json::from_value)
.transpose()
.expect("Unserialize error")
}
}
fn dot_take<T>(&mut self, path: &str) -> Option<T>
where
T: DeserializeOwned,
{
let (my, sub) = path_split(path);
validate_map_key(my);
if let Some(subpath) = sub {
if let Some(item) = self.get_mut(my) {
item.dot_take(subpath)
} else {
None
}
} else {
self.remove(my)
.map(serde_json::from_value)
.transpose() // Option<Result> to Result<Option>
.expect("Unserialize error") // get rid of the result
}
}
}
impl DotPaths for Vec<serde_json::Value> {
fn dot_get<T>(&self, path: &str) -> Option<T>
where
T: DeserializeOwned,
{
let (my, sub) = path_split(path);
let index: usize = if my == ">" {
self.len() - 1
} else {
my.parse().unwrap()
};
if let Some(subpath) = sub {
self.get(index)
.map(ToOwned::to_owned)
.map(|child| child.dot_get(subpath))
.unwrap_or_default()
} else {
self.get(index)
.map(ToOwned::to_owned)
.map(serde_json::from_value)
.transpose()
.unwrap_or_default()
}
}
fn dot_get_mut(&mut self, path: &str) -> Option<&mut Value> {
let (my, sub) = path_split(path);
let my: usize = my
.parse()
.unwrap_or_else(|_| panic!("Cannot index an array by \"{}\"", my));
if let Some(subpath) = sub {
self.get_mut(my)
.map(|m: &mut Value| m.get_mut(subpath))
.unwrap_or_default()
} else {
self.get_mut(my)
}
}
fn dot_set<T>(&mut self, path: &str, value: T)
where
T: Serialize,
{
let (mut my, sub) = path_split(path);
if my.is_empty() {
panic!("Cannot index array by empty key");
}
let mut insert = false;
let mut add = 0isize;
if my.starts_with('<') {
// "<n" means insert before n
// "<" means prepend
insert = true;
my = &my[1..];
} else if my.starts_with('>') {
insert = true;
my = &my[1..];
if my.is_empty() {
// ">" means append
add = self.len() as isize;
} else {
// ">n" means insert after
add = 1;
}
}
let mut my = match my {
// append
"+" => self.len(),
// prepend
"-" => {
insert = true;
0
}
// empty (this happens only if the < or > was removed above)
"" => 0,
_ => my
.parse()
.unwrap_or_else(|_| panic!("Cannot index an array by \"{}\"", my)),
};
my = (my as isize + add) as usize;
match my.cmp(&self.len()) {
Ordering::Less => {
if insert {
// insert
if let Some(subpath) = sub {
self.insert(my, new_by_path_root(subpath, value));
} else {
self.insert(my, serde_json::to_value(value).expect("Serialize error"));
}
} else {
// replace
if let Some(subpath) = sub {
self[my].dot_set(subpath, value);
} else {
self[my] = serde_json::to_value(value).expect("Serialize error");
}
}
}
Ordering::Equal => {
if let Some(subpath) = sub {
self.push(new_by_path_root(subpath, value))
} else {
self.push(serde_json::to_value(value).expect("Serialize error"))
}
}
Ordering::Greater => {
panic!("Index out of bounds.");
}
}
}
fn dot_replace<T, U>(&mut self, path: &str, value: T) -> Option<U>
where
T: Serialize,
U: DeserializeOwned,
{
let (my, sub) = path_split(path);
let index = my
.parse::<usize>()
.unwrap_or_else(|_| panic!("Cannot index an array by \"{}\"", my));
if let Some(subpath) = sub {
if index < self.len() {
let a_mut: &mut Value = self.get_mut(index).unwrap();
a_mut.dot_replace(subpath, value)
} else {
// use the append implementation & validations from dot_set
self.dot_set(path, value);
None
}
} else {
// No subpath
if index < self.len() {
let other = serde_json::to_value(value).expect("Serialize error");
let old = mem::replace(&mut self[index], other);
Some(serde_json::from_value(old).expect("Deserialize error"))
} else {
// use the append implementation & validations from dot_set
self.dot_set(my, value);
None
}
}
}
fn dot_take<T>(&mut self, path: &str) -> Option<T>
where
T: DeserializeOwned,
{
let (my, sub) = path_split(path);
let my = my
.parse()
.unwrap_or_else(|_| panic!("Cannot index an array by \"{}\"", my));
if my >= self.len() {
return None;
}
if let Some(subpath) = sub {
let value: &mut Value = &mut self[my];
value.dot_take(subpath)
} else {
// bounds are checked above
serde_json::from_value(self.remove(my)).expect("Deserialize error")
}
}
}
#[cfg(test)]
mod tests {
use crate::DotPaths;
use serde_json::json;
use serde_json::Value;
#[test]
fn get_scalar_with_empty_path() {
let value = Value::String("Hello".to_string());
assert_eq!(Some("Hello".to_string()), value.dot_get(""));
}
#[test]
#[should_panic]
fn cant_get_scalar_with_path() {
let value = Value::String("Hello".to_string());
let _ = value.dot_get::<Value>("1.2.3");
}
#[test]
fn set_vec() {
let mut vec = Value::Array(vec![]);
vec.dot_set("0", "first");
vec.dot_set("0", "second"); // this replaces it
vec.dot_set("1", "third");
vec.dot_set("+", "append");
vec.dot_set(">", "append2");
vec.dot_set("-", "prepend");
vec.dot_set("<", "prepend2");
vec.dot_set("<0", "prepend3");
vec.dot_set(">1", "insert after 1");
vec.dot_set(">0", "after0");
vec.dot_set("<2", "before2");
assert_eq!(
json!([
"prepend3",
"after0",
"before2",
"prepend2",
"insert after 1",
"prepend",
"second",
"third",
"append",
"append2"
]),
vec
);
}
#[test]
#[should_panic]
fn set_vec_panic_bad_index() {
let mut vec = Value::Array(vec![]);
vec.dot_set("1", "first");
}
#[test]
#[should_panic]
fn set_vec_panic_index_not_numeric() {
let mut vec = Value::Array(vec![]);
vec.dot_set("abc", "first");
}
#[test]
fn set_vec_spawn() {
let mut vec = Value::Array(vec![]);
vec.dot_set("0.0.0", "first");
vec.dot_set("+", "append");
vec.dot_set("<1", "in between");
assert_eq!(json!([[["first"]], "in between", "append"]), vec);
vec.dot_set("0.0.+", "second");
assert_eq!(json!([[["first", "second"]], "in between", "append"]), vec);
vec.dot_set("0.+", "mmm");
assert_eq!(
json!([[["first", "second"], "mmm"], "in between", "append"]),
vec
);
vec.dot_set("0.+.0", "xyz");
assert_eq!(
json!([
[["first", "second"], "mmm", ["xyz"]],
"in between",
"append"
]),
vec
);
// append and prepend can also spawn a new array
let mut vec = Value::Array(vec![]);
vec.dot_set(">.>.>", "first");
assert_eq!(json!([[["first"]]]), vec);
vec.dot_set(">.<.>", "second");
assert_eq!(json!([[["first"]], [["second"]]]), vec);
}
#[test]
fn get_vec() {
let vec = json!([
[["first", "second"], "mmm", ["xyz"]],
"in between",
"append"
]);
assert_eq!(Some("first".to_string()), vec.dot_get("0.0.0"));
assert_eq!(Some("second".to_string()), vec.dot_get("0.0.1"));
// this one doesn't exist
assert_eq!(None, vec.dot_get::<String>("0.0.3"));
// get last
assert_eq!(Some(json!(["xyz"])), vec.dot_get("0.>"));
// retrieve a Value
assert_eq!(
Some(json!([["first", "second"], "mmm", ["xyz"]])),
vec.dot_get("0")
);
assert_eq!(Some(json!(["first", "second"])), vec.dot_get("0.0"));
}
#[test]
#[should_panic]
fn get_vec_panic_index_scalar() {
let vec = json!([
[["first", "second"], "mmm", ["xyz"]],
"in between",
"append"
]);
let _ = vec.dot_get::<Value>("0.0.1.4");
}
#[test]
fn take_from_vec() {
let mut vec = json!(["a", "b", "c"]);
// test Take
assert_eq!(Some("b".to_string()), vec.dot_take("1"));
assert_eq!(None, vec.dot_take::<Value>("4"));
assert_eq!(json!(["a", "c"]), vec);
let mut vec = json!([[["a"], "b"], "c"]);
assert_eq!(Some("a".to_string()), vec.dot_take("0.0.0"));
assert_eq!(json!([[[], "b"], "c"]), vec); // empty vec is left in place
}
#[test]
fn remove_from_vec() {
let mut vec = json!(["a", "b", "c"]);
assert_eq!(true, vec.dot_remove("1"));
assert_eq!(false, vec.dot_remove("4"));
assert_eq!(json!(["a", "c"]), vec);
let mut vec = json!([[["a"], "b"], "c"]);
assert_eq!(true, vec.dot_remove("0.0.0"));
assert_eq!(false, vec.dot_remove("0.0.0"));
assert_eq!(json!([[[], "b"], "c"]), vec); // empty vec is left in place
}
#[test]
fn replace_in_vec() {
let mut vec = json!(["a", "b", "c"]);
assert_eq!(Some("b".to_string()), vec.dot_replace("1", "BBB"));
let mut vec = json!([[["a"], "b"], "c"]);
assert_eq!(Some("a".to_string()), vec.dot_replace("0.0.0", "AAA"));
assert_eq!(json!([[["AAA"], "b"], "c"]), vec);
}
#[test]
fn set_map() {
let mut vec = json!({});
vec.dot_set("foo", "bar");
assert_eq!(json!({"foo": "bar"}), vec);
let mut vec = json!({});
vec.dot_set("foo.bar.baz", "binx");
assert_eq!(json!({"foo": {"bar": {"baz": "binx"}}}), vec);
vec.dot_set("foo.bar.abc.0", "aaa");
assert_eq!(json!({"foo": {"bar": {"baz": "binx","abc": ["aaa"]}}}), vec);
}
#[test]
#[should_panic]
fn set_map_panic_bad_index() {
let mut vec = json!({});
vec.dot_set("0", "first");
}
#[test]
fn get_map() {
let vec = json!({"one": "two"});
assert_eq!(Some("two".to_string()), vec.dot_get("one"));
let vec = json!({"one": {"two": "three"}});
assert_eq!(Some("three".to_string()), vec.dot_get("one.two"));
let vec = json!({"one": "two"});
assert_eq!(None, vec.dot_get::<Value>("xxx"));
}
#[test]
fn remove_from_map() {
let mut vec = json!({"one": "two", "x": "y"});
assert_eq!(true, vec.dot_remove("one"));
assert_eq!(json!({"x": "y"}), vec);
}
#[test]
fn take_from_map() {
let mut vec = json!({"one": "two", "x": "y"});
assert_eq!(Some("two".to_string()), vec.dot_take("one"));
assert_eq!(json!({"x": "y"}), vec);
}
#[test]
fn replace_in_map() {
let mut vec = json!({"one": "two", "x": "y"});
assert_eq!(Some("two".to_string()), vec.dot_replace("one", "fff"));
assert_eq!(json!({"one": "fff", "x": "y"}), vec);
// replace value for string
let mut vec = json!({"one": "two", "x": {"bbb": "y"}});
assert_eq!(Some("y".to_string()), vec.dot_replace("x.bbb", "mm"));
assert_eq!(
Some(json!({"bbb": "mm"})),
vec.dot_replace("x", "betelgeuze")
);
assert_eq!(json!({"one": "two", "x": "betelgeuze"}), vec);
// nested replace
let mut vec = json!({"one": "two", "x": {"bbb": ["y"]}});
assert_eq!(
Some("y".to_string()),
vec.dot_replace("x.bbb.0", "betelgeuze")
);
assert_eq!(json!({"one": "two", "x": {"bbb": ["betelgeuze"]}}), vec);
}
}
Loading…
Cancel
Save