diff --git a/Cargo.toml b/Cargo.toml index a1391a4..3a555f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_dotpath" -version = "0.1.1" +version = "0.1.2" authors = ["Ondřej Hruška "] edition = "2018" license = "MIT" diff --git a/README.md b/README.md index c6f8fac..766d7b0 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,41 @@ Access members of nested JSON arrays and objects using "dotted paths". -The `DotPaths` trait is implemented for `serde_json::Value`, -`serde_json::Map`, and `Vec`. +Consider this example JSON: + +```json +{ + "fruit": [ + {"name": "lemon", "color": "yellow"}, + {"name": "apple", "color": "green"}, + {"name": "cherry", "color": "red"} + ] +} +``` + +The following can be used to access its parts: +- `obj.dot_get("fruit")` ... get the fruits array +- `obj.dot_get("fruit.0.name")` ... 0th fruit name, "lemon" +- `obj.dot_get("fruit.>.color")` ... last fruit's color, "red" + +The JSON can also be manipulated: + +- `obj.dot_take("fruit.1")` ... extract the "apple" object, removing it from the JSON +- `obj.dot_set("fruit.<1", json!({"name":"plum","color":"blue"})` ... insert before the 1st element, shifting the rest +- `obj.dot_set("fruit.>1", json!({"name":"plum","color":"blue"})` ... insert after the 1st element, shifting the rest +- `obj.dot_set("fruit.>.name", "tangerine")` ... set the last fruit's name +- `obj.dot_set("fruit.>", Value::Null)` ... append a JSON null +- `obj.dot_set("fruit.<", true)` ... prepend a JSON true +- `obj.dot_set("vegetables.onion.>", "aaa")` ... add `{"vegetables":{"onion":["aaa"]}}` to the object Any serializable type or `serde_json::Value` can be stored to or retrieved from -the nested object. Any value stored in the object can also be modified in place -by getting a mutable reference. +the nested object (`Value::Object`, `Value::Array`, `Value::Null`). + +Any value stored in the object can also be modified in place, without deserialization, +by getting a mutable reference (`dot_get_mut(path)`). This crate is useful for tasks such as working with dynamic JSON API payloads, -parsing config files, or polymorphic data store. +parsing config files, or building a polymorphic data store. ## Supported Operations @@ -36,12 +62,25 @@ It becomes an array or object of the appropriate type based on the root key. ## Dotted Path Syntax -Array keys must be numeric (integer), or one of the special patterns listed below. +### Map Patterns To avoid ambiguity, it's not allowed to use numeric keys (or keys starting with a number) as map keys. Map keys must start with an ASCII letter or underscore and must not contain a dot (`.`). -### Array Index Patterns +Examples: + +- `abc` +- `_123` +- `key with spaces` + +If a numeric key or a key nonconforming in other way must be used, prefix it with `#`. +It will be taken literally as a string, excluding the prefix. + +e.g. to get 456 from `{"foo":{"123":456}}`, use `foo.#123` instead of `foo.123` + +### Array Patterns + +Array keys must be numeric (integer), or one of the special patterns listed below. - `-` ... prepend - `<` ... prepend (or get first) diff --git a/src/lib.rs b/src/lib.rs index 02be46b..177798f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ pub trait DotPaths { /// # Special keys /// Arrays can be indexed by special keys for reading: /// - `>` ... last element + /// - `#123` ... map keys may be prefixed by `#` to use numeric strings + /// or other unusual forms (excluding dot `.`, which is still illegal in keys) /// /// # Panics /// - If the path attempts to index into a scalar (e.g. `"foo.bar"` in `{"foo": 123}`) @@ -252,14 +254,23 @@ where /// 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); +#[must_use] +fn validate_map_key(key: &str) -> &str { + if key.contains('.') { + // this shouldn't happen due to the way the splitting works + panic!("Invalid map key: {}", key); + } + + // 'literal modifier', e.g. for numeric map keys + if key.starts_with('#') { + return &key[1..]; } - if !key.starts_with(|p: char| p.is_ascii_alphabetic() || p == '_') || key.contains('.') { + if !key.starts_with(|p: char| p.is_ascii_alphabetic() || p == '_') { panic!("Invalid map key: {}", key); } + + key } impl DotPaths for serde_json::Map { @@ -268,7 +279,7 @@ impl DotPaths for serde_json::Map { T: DeserializeOwned, { let (my, sub) = path_split(path); - validate_map_key(my); + let my = validate_map_key(my); if let Some(sub_path) = sub { self.get(my) @@ -285,7 +296,7 @@ impl DotPaths for serde_json::Map { fn dot_get_mut(&mut self, path: &str) -> Option<&mut Value> { let (my, sub) = path_split(path); - validate_map_key(my); + let my = validate_map_key(my); if let Some(sub_path) = sub { self.get_mut(my) @@ -301,7 +312,7 @@ impl DotPaths for serde_json::Map { T: Serialize, { let (my, sub) = path_split(path); - validate_map_key(my); + let my = validate_map_key(my); if let Some(subpath) = sub { if self.contains_key(my) { @@ -321,7 +332,7 @@ impl DotPaths for serde_json::Map { U: DeserializeOwned, { let (my, sub) = path_split(path); - validate_map_key(my); + let my = validate_map_key(my); if let Some(subpath) = sub { if self.contains_key(my) { @@ -344,7 +355,7 @@ impl DotPaths for serde_json::Map { T: DeserializeOwned, { let (my, sub) = path_split(path); - validate_map_key(my); + let my = validate_map_key(my); if let Some(subpath) = sub { if let Some(item) = self.get_mut(my) { @@ -770,6 +781,19 @@ mod tests { assert_eq!(None, vec.dot_get::("xxx")); } + #[test] + fn map_escaped_keys() { + let mut map = json!({}); + map.dot_set("#0.#1", 123); + assert_eq!(json!({"0": {"1": 123}}), map); + map.dot_set("#0.#2", 456); + + assert_eq!(Some(123), map.dot_get("#0.#1")); + assert_eq!(Some(123), map.dot_take("#0.#1")); + assert_eq!(json!({"0": {"2": 456}}), map); + assert_eq!(true, map.dot_remove("#0.#2")); + } + #[test] fn remove_from_map() { let mut vec = json!({"one": "two", "x": "y"});