commit 7454ccdfe2f3c71fd8f20320486bbe96fbe67b88 Author: Ondřej Hruška Date: Sat Dec 28 16:43:18 2019 +0100 initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22676ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +.idea/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5cd9a53 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "json_dotpath" +version = "0.1.0" +authors = ["Ondřej Hruška "] +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" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d7d74c6 --- /dev/null +++ b/src/lib.rs @@ -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(&self, path: &str) -> Option + where + T: DeserializeOwned; + + /// Get an item, or a default value. + /// + /// # Special keys + /// see `dot_get()` + /// + /// # Panics + /// see `dot_get()` + fn dot_get_or(&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(&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` + /// - `(&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(&mut self, path: &str, value: T) -> Option + 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(&mut self, path: &str) -> Option + 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::(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(&self, path: &str) -> Option + 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(&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(&mut self, path: &str, value: T) -> Option + 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(&mut self, path: &str) -> Option + 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(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::().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 { + fn dot_get(&self, path: &str) -> Option + 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> + .unwrap_or_default() + } else { + self.get(my) + .map(ToOwned::to_owned) + .map(serde_json::from_value) + .transpose() // Option to Result