From 34073ab707012fa995f148cc61a0f63943add057 Mon Sep 17 00:00:00 2001 From: qrayven Date: Tue, 22 Nov 2022 16:42:26 +0100 Subject: [PATCH] feat: insert with parents for `Document` (#189) * feat: insert with parents for Document * setter and getter for data * adress the comments * fix test in drive --- dpp/src/document/mod.rs | 17 +- dpp/src/util/json_path.rs | 2 + dpp/src/util/json_value/insert_with_path.rs | 267 ++++++++++++++++++ .../util/{json_value.rs => json_value/mod.rs} | 65 ++++- drive/src/drive/document/update.rs | 2 +- 5 files changed, 341 insertions(+), 12 deletions(-) create mode 100644 dpp/src/util/json_value/insert_with_path.rs rename dpp/src/util/{json_value.rs => json_value/mod.rs} (91%) diff --git a/dpp/src/document/mod.rs b/dpp/src/document/mod.rs index 5a677e7d..fc06448c 100644 --- a/dpp/src/document/mod.rs +++ b/dpp/src/document/mod.rs @@ -181,8 +181,11 @@ impl Document { Ok(hash(self.to_buffer()?)) } - pub fn set_value(&mut self, property: &str, value: JsonValue) -> Result<(), ProtocolError> { - Ok(self.data.insert(property.to_string(), value)?) + /// Set the value under given path. + /// The path supports syntax from `lodash` JS lib. Example: "root.people[0].name". + /// If parents are not present they will be automatically created + pub fn set(&mut self, path: &str, value: JsonValue) -> Result<(), ProtocolError> { + Ok(self.data.insert_with_path(path, value)?) } /// Retrieves field specified by path @@ -192,6 +195,16 @@ impl Document { Err(_) => None, } } + + /// Get the Document's data + pub fn get_data(&self) -> &JsonValue { + &self.data + } + + /// Set the Document's data + pub fn set_data(&mut self, data: JsonValue) { + self.data = data; + } } #[cfg(test)] diff --git a/dpp/src/util/json_path.rs b/dpp/src/util/json_path.rs index 2404636a..6d3d5170 100644 --- a/dpp/src/util/json_path.rs +++ b/dpp/src/util/json_path.rs @@ -94,6 +94,8 @@ fn try_parse_indexed_field(step: &str) -> Result<(String, usize), anyhow::Error> #[cfg(test)] mod test { + use std::convert::TryInto; + use super::*; #[test] diff --git a/dpp/src/util/json_value/insert_with_path.rs b/dpp/src/util/json_value/insert_with_path.rs new file mode 100644 index 00000000..b3684adb --- /dev/null +++ b/dpp/src/util/json_value/insert_with_path.rs @@ -0,0 +1,267 @@ +use super::JsonValueExt; +use crate::util::json_path::JsonPathStep; +use anyhow::{bail, Context}; +use serde_json::Value; + +/// Inserts the value specified by the json path. If intermediate object doesn't exist, crates a one. +/// If `Value::Null` is encountered while traversing the path, they are replaced with the required structure. +pub(super) fn insert_with_path( + data: &mut Value, + json_path: &[JsonPathStep], + value: Value, +) -> Result<(), anyhow::Error> { + let mut current_level = data; + let last_index = json_path.len() - 1; + + for (i, key) in json_path.iter().enumerate() { + match key { + JsonPathStep::Index(json_index) => { + if i == last_index { + if current_level.is_null() { + *current_level = Value::Array(Default::default()); + } + fill_empty_indexes(current_level, *json_index); + insert_into_array(current_level, *json_index, value).with_context(|| { + format!("failed inserting on position {json_index} into {current_level:#?}") + })?; + break; + } + + if current_level.get(json_index).is_none() { + if current_level.is_null() { + *current_level = Value::Array(Default::default()); + } + fill_empty_indexes(current_level, *json_index); + current_level.push(new_value_based_on_next_step(&json_path[i + 1]))?; + } + + let new_level = current_level.get_mut(json_index).unwrap(); + current_level = new_level; + } + + JsonPathStep::Key(key) => { + if i == last_index { + if current_level.is_null() { + *current_level = Value::Object(Default::default()); + } + current_level.insert(key.to_string(), value)?; + break; + } + + if current_level.get(key).is_none() { + if current_level.is_null() { + *current_level = Value::Object(Default::default()); + } + current_level.insert( + key.to_string(), + new_value_based_on_next_step(&json_path[i + 1]), + )?; + } + let new_level = current_level.get_mut(key).unwrap(); + current_level = new_level; + } + } + } + + Ok(()) +} + +fn insert_into_array( + maybe_array: &mut Value, + position: usize, + value: Value, +) -> Result<(), anyhow::Error> { + match maybe_array.as_array_mut() { + Some(ref mut array) => { + if position >= array.len() { + array.push(value) + } else { + array[position] = value; + } + Ok(()) + } + None => bail!("expected array"), + } +} + +fn fill_empty_indexes(current_level: &mut Value, i: usize) { + let index = i as i64; + if let Some(array) = current_level.as_array_mut() { + let positions_to_fill = index - (array.len() as i64 - 1) - 1; + array.extend((0..positions_to_fill).map(|_| Value::Null)); + } +} + +fn new_value_based_on_next_step(next_step: &JsonPathStep) -> Value { + match next_step { + JsonPathStep::Index(_) => Value::Array(Default::default()), + JsonPathStep::Key(_) => Value::Object(Default::default()), + } +} + +#[cfg(test)] +mod test_set { + use serde_json::json; + + use super::*; + + #[test] + fn set_onto_nil_when_object_given() { + let mut data = Value::Null; + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("b".to_string()), + JsonPathStep::Key("c".to_string()), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data["a"]["b"]["c"], json!("alpha")) + } + + #[test] + fn set_onto_nil_when_array_given() { + let mut data = Value::Null; + let keys = [JsonPathStep::Index(0)]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data[0], json!("alpha")) + } + + #[test] + fn set_new_value_only_maps() { + let mut data = Value::Object(Default::default()); + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("b".to_string()), + JsonPathStep::Key("c".to_string()), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + assert_eq!(data["a"]["b"]["c"], json!("alpha")) + } + + #[test] + fn set_value_with_array() { + let mut data = Value::Object(Default::default()); + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("b".to_string()), + JsonPathStep::Index(0), + JsonPathStep::Key("c".to_string()), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data["a"]["b"][0]["c"], json!("alpha")) + } + + #[test] + fn set_value_with_array_padding() { + let mut data = Value::Object(Default::default()); + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("b".to_string()), + JsonPathStep::Index(3), + JsonPathStep::Key("c".to_string()), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data["a"]["b"][0]["c"], Value::Null); + assert_eq!(data["a"]["b"][1]["c"], Value::Null); + assert_eq!(data["a"]["b"][2]["c"], Value::Null); + assert_eq!(data["a"]["b"][3]["c"], json!("alpha")); + } + + #[test] + fn test_set_the_existing_path() { + let mut data = json!({ + "a": { + "b" : vec![ Value::Null, Value::Null] + } + }); + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("b".to_string()), + JsonPathStep::Index(1), + JsonPathStep::Key("c".to_string()), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data["a"]["b"][1]["c"], json!("alpha")); + } + + #[test] + fn set_the_existing_root_path() { + let mut data = json!({ + "a": {} + }); + let keys = [JsonPathStep::Key("a".to_string())]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data["a"], json!("alpha")); + } + + #[test] + fn errors_if_existing_path_has_different_types() { + let mut data = json!({ + "a": { + "b" : "some_string" + } + }); + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("b".to_string()), + JsonPathStep::Key("c".to_string()), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect_err("error should be returned"); + } + + #[test] + fn replace_if_existing_object_has_different_type() { + let mut data = json!({ + "a": { + "b" : { "c": "bravo"} + } + }); + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("b".to_string()), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data["a"]["b"], json!("alpha")); + } + + #[test] + fn replace_if_existing_object_has_different_type_in_array() { + let mut data = json!({ "a": [json!("already_taken")] }); + let keys = [JsonPathStep::Key("a".to_string()), JsonPathStep::Index(0)]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect("no errors"); + + assert_eq!(data["a"][0], json!("alpha")); + } + + #[test] + fn error_if_try_set_the_array_index_when_object_is_not_array() { + let mut data = json!({ "a": { + "not_array" : { + "c" :{}, + } + } }); + let keys = [ + JsonPathStep::Key("a".to_string()), + JsonPathStep::Key("not_array".to_string()), + JsonPathStep::Index(0), + ]; + + insert_with_path(&mut data, &keys, json!("alpha")).expect_err("inserting error"); + } +} diff --git a/dpp/src/util/json_value.rs b/dpp/src/util/json_value/mod.rs similarity index 91% rename from dpp/src/util/json_value.rs rename to dpp/src/util/json_value/mod.rs index 5cb109b4..ba1d9c0b 100644 --- a/dpp/src/util/json_value.rs +++ b/dpp/src/util/json_value/mod.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, convert::TryInto}; use anyhow::{anyhow, bail}; use log::trace; use serde::de::DeserializeOwned; -use serde_json::{Number, Value as JsonValue}; +use serde_json::{json, Number, Value as JsonValue}; use crate::util::deserializer; use crate::{ @@ -16,6 +16,9 @@ use super::{ string_encoding::Encoding, }; +mod insert_with_path; +use insert_with_path::*; + const PROPERTY_CONTENT_MEDIA_TYPE: &str = "contentMediaType"; const PROPERTY_PROTOCOL_VERSION: &str = "protocolVersion"; @@ -76,6 +79,10 @@ pub trait JsonValueExt { property_name: &str, protocol_bytes: &[u8], ) -> Result<(), ProtocolError>; + + /// Insert value under the path. Path is dot-separated string. i.e `properties[0].id`. If parents don't + /// exists they will be created + fn insert_with_path(&mut self, path: &str, value: JsonValue) -> Result<(), anyhow::Error>; } impl JsonValueExt for JsonValue { @@ -271,14 +278,16 @@ impl JsonValueExt for JsonValue { for raw_path in paths { let mut to_replace = get_value_mut(raw_path, self); match to_replace { - Some(ref mut v) => replace_identifier(v, with).map_err(|err| { - anyhow!( - "unable replace the {:?} with {:?}: '{}'", - raw_path, - with, - err - ) - })?, + Some(ref mut v) => { + replace_identifier(v, with).map_err(|err| { + anyhow!( + "unable replace the {:?} with {:?}: '{}'", + raw_path, + with, + err + ) + })?; + } None => { trace!("path '{}' is not found, replacing to {:?} ", raw_path, with) } @@ -347,6 +356,17 @@ impl JsonValueExt for JsonValue { _ => bail!("the Json Value isn't a map: {:?}", self), } } + + /// Insert value under the path. Path is dot-separated string. i.e `properties[0].id` + fn insert_with_path( + &mut self, + string_path: &str, + value: JsonValue, + ) -> Result<(), anyhow::Error> { + let path_literal: JsonPathLiteral = string_path.into(); + let path: JsonPath = path_literal.try_into().unwrap(); + insert_with_path(self, &path, value) + } } /// replaces the Identifiers specified in binary_properties with Bytes or Base58 @@ -563,4 +583,31 @@ mod test { let result = identifiers_to(&binary_properties, &mut document, ReplaceWith::Bytes); assert_error_contains!(result, "Identifier must be 32 bytes long"); } + + #[test] + fn insert_with_parents() { + let mut document = json!({ + "root" : { + "from" : { + "id": "123", + "message": "text_message", + }, + } + }); + + document + .insert_with_path("root.to.new_field", json!("new_value")) + .expect("no errors"); + document + .insert_with_path("root.array[0].new_field", json!("new_value")) + .expect("no errors"); + + assert_eq!(document["root"]["from"]["id"], json!("123")); + assert_eq!(document["root"]["from"]["message"], json!("text_message")); + assert_eq!(document["root"]["to"]["new_field"], json!("new_value")); + assert_eq!( + document["root"]["array"][0]["new_field"], + json!("new_value") + ); + } } diff --git a/drive/src/drive/document/update.rs b/drive/src/drive/document/update.rs index 230badc5..24b3d728 100644 --- a/drive/src/drive/document/update.rs +++ b/drive/src/drive/document/update.rs @@ -2299,7 +2299,7 @@ mod tests { // Update the document in a second document - .set_value("name", Value::String("Ivaaaaaaaaaan!".to_string())) + .set("name", Value::String("Ivaaaaaaaaaan!".to_string())) .expect("should change name"); let document_cbor = document.to_cbor().expect("should encode to cbor");