From 2c8cdde57059bcef7f183d60f23b49df15949795 Mon Sep 17 00:00:00 2001 From: Satyam Singh Date: Tue, 21 Mar 2023 14:03:52 +0530 Subject: [PATCH 1/4] Add custom flattening that ignores array --- Cargo.lock | 22 ---- server/Cargo.toml | 1 - server/src/alerts/mod.rs | 15 +-- server/src/event.rs | 58 +++++++--- server/src/handlers/http/ingest.rs | 7 +- server/src/utils/json.rs | 9 +- server/src/utils/json/flatten.rs | 178 +++++++++++++++++++++++++++++ 7 files changed, 234 insertions(+), 56 deletions(-) create mode 100644 server/src/utils/json/flatten.rs diff --git a/Cargo.lock b/Cargo.lock index 420c5ff76..ddd42da42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1794,16 +1794,6 @@ dependencies = [ "libc", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "backtrace", - "version_check", -] - [[package]] name = "extend" version = "0.1.2" @@ -3053,7 +3043,6 @@ dependencies = [ "relative-path", "reqwest", "rstest", - "rust-flatten-json", "rustls", "rustls-pemfile", "semver", @@ -3518,17 +3507,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rust-flatten-json" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "264b19b0d73b6ab33a706dc04b2b8148390da5aa16cfe567a8df6135f431e972" -dependencies = [ - "error-chain", - "log", - "serde_json", -] - [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/server/Cargo.toml b/server/Cargo.toml index 3fff36002..a9461f0ff 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -51,7 +51,6 @@ relative-path = { version = "1.7", features = ["serde"] } reqwest = { version = "0.11", default_features=false, features=["rustls", "json", "hyper-rustls", "tokio-rustls"]} rustls = "0.20" rustls-pemfile = "1.0" -rust-flatten-json = "0.2" semver = "1.0" serde = { version = "1.0", features = ["rc"] } serde_json = "1.0" diff --git a/server/src/alerts/mod.rs b/server/src/alerts/mod.rs index 5b0e9915f..e89d15f36 100644 --- a/server/src/alerts/mod.rs +++ b/server/src/alerts/mod.rs @@ -26,9 +26,9 @@ pub mod rule; pub mod target; use crate::metrics::ALERTS_STATES; -use crate::storage; use crate::utils::uid; use crate::CONFIG; +use crate::{storage, utils}; pub use self::rule::Rule; use self::target::Target; @@ -97,16 +97,9 @@ impl Alert { let deployment_mode = storage::StorageMetadata::global().mode.to_string(); let additional_labels = serde_json::to_value(rule).expect("rule is perfectly deserializable"); - let mut flatten_additional_labels = serde_json::json!({}); - flatten_json::flatten( - &additional_labels, - &mut flatten_additional_labels, - Some("rule".to_string()), - false, - Some("_"), - ) - .expect("can be flattened"); - + let flatten_additional_labels = + utils::json::flatten::flatten_with_parent_prefix(additional_labels, "rule", "_") + .expect("can be flattened"); Context::new( stream_name, AlertInfo::new( diff --git a/server/src/event.rs b/server/src/event.rs index 70965441a..64b0a108f 100644 --- a/server/src/event.rs +++ b/server/src/event.rs @@ -156,27 +156,55 @@ pub fn get_schema_key(body: &Value) -> String { fn fields_mismatch(schema: &Schema, body: &Value) -> bool { for (name, val) in body.as_object().expect("body is of object variant") { let Ok(field) = schema.field_with_name(name) else { return true }; - - // datatype check only some basic cases - let valid_datatype = match field.data_type() { - DataType::Boolean => val.is_boolean(), - DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => val.is_i64(), - DataType::UInt8 | DataType::UInt16 | DataType::UInt32 | DataType::UInt64 => { - val.is_u64() - } - DataType::Float16 | DataType::Float32 | DataType::Float64 => val.is_f64(), - DataType::Utf8 => val.is_string(), - _ => false, - }; - - if !valid_datatype { + if !valid_type(field.data_type(), val) { return true; } } - false } +fn valid_type(data_type: &DataType, value: &Value) -> bool { + match data_type { + DataType::Boolean => value.is_boolean(), + DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => value.is_i64(), + DataType::UInt8 | DataType::UInt16 | DataType::UInt32 | DataType::UInt64 => value.is_u64(), + DataType::Float16 | DataType::Float32 | DataType::Float64 => value.is_f64(), + DataType::Utf8 => value.is_string(), + DataType::List(field) => { + let data_type = field.data_type(); + if let Value::Array(arr) = value { + for elem in arr { + if !valid_type(data_type, elem) { + return false; + } + } + } + true + } + DataType::Struct(fields) => { + if let Value::Object(val) = value { + for (key, value) in val { + let field = (0..fields.len()) + .find(|idx| fields[*idx].name() == key) + .map(|idx| &fields[idx]); + + if let Some(field) = field { + if !valid_type(field.data_type(), value) { + return false; + } + } else { + return false; + } + } + true + } else { + false + } + } + _ => unreachable!(), + } +} + fn commit_schema( stream_name: &str, schema_key: &str, diff --git a/server/src/handlers/http/ingest.rs b/server/src/handlers/http/ingest.rs index 1a5ce607f..669abfac2 100644 --- a/server/src/handlers/http/ingest.rs +++ b/server/src/handlers/http/ingest.rs @@ -79,7 +79,7 @@ async fn push_logs( Value::Array(array) => { for mut body in array { merge(&mut body, tags_n_metadata.clone().into_iter()); - let body = flatten_json_body(&body).unwrap(); + let body = flatten_json_body(body).map_err(|_| PostError::FlattenError)?; let schema_key = event::get_schema_key(&body); let event = event::Event { @@ -93,7 +93,7 @@ async fn push_logs( } mut body @ Value::Object(_) => { merge(&mut body, tags_n_metadata.into_iter()); - let body = flatten_json_body(&body).unwrap(); + let body = flatten_json_body(body).map_err(|_| PostError::FlattenError)?; let schema_key = event::get_schema_key(&body); let event = event::Event { body, @@ -117,6 +117,8 @@ pub enum PostError { Event(#[from] EventError), #[error("Invalid Request")] Invalid, + #[error("failed to flatten the json object")] + FlattenError, #[error("Failed to create stream due to {0}")] CreateStream(Box), } @@ -128,6 +130,7 @@ impl actix_web::ResponseError for PostError { PostError::Event(_) => StatusCode::INTERNAL_SERVER_ERROR, PostError::Invalid => StatusCode::BAD_REQUEST, PostError::CreateStream(_) => StatusCode::INTERNAL_SERVER_ERROR, + PostError::FlattenError => StatusCode::BAD_REQUEST, } } diff --git a/server/src/utils/json.rs b/server/src/utils/json.rs index ef5f75ae5..8a0af86a0 100644 --- a/server/src/utils/json.rs +++ b/server/src/utils/json.rs @@ -17,13 +17,12 @@ */ use serde_json; -use serde_json::json; use serde_json::Value; -pub fn flatten_json_body(body: &serde_json::Value) -> Result { - let mut flat_value: Value = json!({}); - flatten_json::flatten(body, &mut flat_value, None, false, Some("_")).unwrap(); - Ok(flat_value) +pub mod flatten; + +pub fn flatten_json_body(body: Value) -> Result { + flatten::flatten(body, "_") } pub fn merge(value: &mut Value, fields: impl Iterator) { diff --git a/server/src/utils/json/flatten.rs b/server/src/utils/json/flatten.rs new file mode 100644 index 000000000..9fea80c42 --- /dev/null +++ b/server/src/utils/json/flatten.rs @@ -0,0 +1,178 @@ +/* + * Parseable Server (C) 2022 - 2023 Parseable, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +use serde_json::map::Map; +use serde_json::value::Value; + +pub fn flatten(nested_value: Value, separator: &str) -> Result { + let mut map = Map::new(); + if let Value::Object(nested_dict) = nested_value { + flatten_object(&mut map, None, nested_dict, separator)?; + } else { + return Err(()); + } + Ok(Value::Object(map)) +} + +pub fn flatten_with_parent_prefix( + nested_value: Value, + prefix: &str, + separator: &str, +) -> Result { + let mut map = Map::new(); + if let Value::Object(nested_dict) = nested_value { + flatten_object(&mut map, Some(prefix), nested_dict, separator)?; + } else { + return Err(()); + } + Ok(Value::Object(map)) +} + +pub fn flatten_object( + map: &mut Map, + parent_key: Option<&str>, + nested_dict: Map, + separator: &str, +) -> Result<(), ()> { + for (key, value) in nested_dict.into_iter() { + let new_key = parent_key + .map(|parent_key| format!("{}{}{}", parent_key, separator, key)) + .unwrap_or(key); + match value { + Value::Object(obj) => flatten_object(map, Some(&new_key), obj, separator)?, + Value::Array(arr) => { + let mut new_arr = Vec::with_capacity(arr.len()); + for elem in arr { + if let Value::Object(object) = elem { + let mut map = Map::new(); + flatten_object_disallow_array(&mut map, None, object, separator)?; + new_arr.push(Value::Object(map)) + } else { + new_arr.push(elem) + } + } + map.insert(new_key, Value::Array(new_arr)); + } + x => { + map.insert(new_key, x); + } + } + } + Ok(()) +} + +fn flatten_object_disallow_array( + map: &mut Map, + parent_key: Option<&str>, + object: Map, + separator: &str, +) -> Result<(), ()> { + for (key, value) in object.into_iter() { + let new_key = parent_key + .map(|parent_key| format!("{}{}{}", parent_key, separator, key)) + .unwrap_or(key); + match value { + Value::Object(obj) => { + flatten_object_disallow_array(map, Some(&new_key), obj, separator)?; + } + Value::Array(_) => return Err(()), + x => { + map.insert(new_key, x); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::flatten; + use serde_json::json; + + #[test] + fn flatten_single_key_string() { + let obj = json!({"key": "value"}); + assert_eq!(obj.clone(), flatten(obj, "_").unwrap()); + } + + #[test] + fn flatten_single_key_int() { + let obj = json!({"key": 1}); + assert_eq!(obj.clone(), flatten(obj, "_").unwrap()); + } + + #[test] + fn flatten_multiple_key_value() { + let obj = json!({"key1": 1, "key2": "value2"}); + assert_eq!(obj.clone(), flatten(obj, "_").unwrap()); + } + + #[test] + fn flatten_nested_single_key_value() { + let obj = json!({"key": "value", "nested_key": {"key":"value"}}); + assert_eq!( + json!({"key": "value", "nested_key.key": "value"}), + flatten(obj, ".").unwrap() + ); + } + + #[test] + fn nested_multiple_key_value() { + let obj = json!({"key": "value", "nested_key": {"key1":"value1", "key2": "value2"}}); + assert_eq!( + json!({"key": "value", "nested_key.key1": "value1", "nested_key.key2": "value2"}), + flatten(obj, ".").unwrap() + ); + } + + #[test] + fn nested_key_value_with_array() { + let obj = json!({"key": "value", "nested_key": {"key1":[1,2,3]}}); + assert_eq!( + json!({"key": "value", "nested_key.key1": [1,2,3]}), + flatten(obj, ".").unwrap() + ); + } + + #[test] + fn nested_array() { + let obj = json!({"key": ["value1", "value2"]}); + assert_eq!(obj.clone(), flatten(obj, ".").unwrap()); + } + + #[test] + fn nested_obj_array() { + let obj = json!({"key": ["value1", {"key": "value2"}]}); + assert_eq!(obj.clone(), flatten(obj, ".").unwrap()); + } + + #[test] + fn flatten_mixed_object() { + let obj = json!({"a": 42, "arr": ["1", {"key": "2"}, {"key": {"nested": "3"}}]}); + assert_eq!( + json!({"a": 42, "arr": ["1", {"key": "2"}, {"key.nested": "3"}]}), + flatten(obj, ".").unwrap() + ); + } + + #[test] + fn err_flatten_mixed_object_with_array_within_object() { + let obj = json!({"a": 42, "arr": ["1", {"key": [1,2,4]}]}); + assert!(flatten(obj, ".").is_err()); + } +} From d76ffc5825f7fe9a0aab71da2afd42ddb2164e18 Mon Sep 17 00:00:00 2001 From: Satyam Singh Date: Tue, 21 Mar 2023 15:33:59 +0530 Subject: [PATCH 2/4] Use map_or_else --- server/src/utils/json/flatten.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/src/utils/json/flatten.rs b/server/src/utils/json/flatten.rs index 9fea80c42..2175c2748 100644 --- a/server/src/utils/json/flatten.rs +++ b/server/src/utils/json/flatten.rs @@ -50,9 +50,10 @@ pub fn flatten_object( separator: &str, ) -> Result<(), ()> { for (key, value) in nested_dict.into_iter() { - let new_key = parent_key - .map(|parent_key| format!("{}{}{}", parent_key, separator, key)) - .unwrap_or(key); + let new_key = parent_key.map_or_else( + || key.clone(), + |parent_key| format!("{}{}{}", parent_key, separator, key), + ); match value { Value::Object(obj) => flatten_object(map, Some(&new_key), obj, separator)?, Value::Array(arr) => { @@ -83,9 +84,10 @@ fn flatten_object_disallow_array( separator: &str, ) -> Result<(), ()> { for (key, value) in object.into_iter() { - let new_key = parent_key - .map(|parent_key| format!("{}{}{}", parent_key, separator, key)) - .unwrap_or(key); + let new_key = parent_key.map_or_else( + || key.clone(), + |parent_key| format!("{}{}{}", parent_key, separator, key), + ); match value { Value::Object(obj) => { flatten_object_disallow_array(map, Some(&new_key), obj, separator)?; From 671b2d7ee539419d522e87397f43e3034a1694cc Mon Sep 17 00:00:00 2001 From: Satyam Singh Date: Fri, 24 Mar 2023 19:39:17 +0530 Subject: [PATCH 3/4] Add merge schema check --- server/src/event.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/event.rs b/server/src/event.rs index 64b0a108f..701f460e3 100644 --- a/server/src/event.rs +++ b/server/src/event.rs @@ -100,6 +100,11 @@ impl Event { let stream_name = &self.stream_name; let schema_key = &self.schema_key; + let old = metadata::STREAM_INFO.merged_schema(stream_name)?; + if Schema::try_merge(vec![old, schema.clone()]).is_err() { + return Err(EventError::SchemaMismatch); + }; + commit_schema(stream_name, schema_key, Arc::new(schema))?; self.process_event(event) } From 872fc58fecc9c83c96c39bb9a542298f0499fd9c Mon Sep 17 00:00:00 2001 From: Satyam Singh Date: Thu, 30 Mar 2023 22:39:24 +0530 Subject: [PATCH 4/4] Flatten array --- server/src/utils/json/flatten.rs | 204 +++++++++++++++++++++++++------ 1 file changed, 168 insertions(+), 36 deletions(-) diff --git a/server/src/utils/json/flatten.rs b/server/src/utils/json/flatten.rs index 2175c2748..da2a96dcc 100644 --- a/server/src/utils/json/flatten.rs +++ b/server/src/utils/json/flatten.rs @@ -16,6 +16,7 @@ * */ +use itertools::Itertools; use serde_json::map::Map; use serde_json::value::Value; @@ -57,17 +58,12 @@ pub fn flatten_object( match value { Value::Object(obj) => flatten_object(map, Some(&new_key), obj, separator)?, Value::Array(arr) => { - let mut new_arr = Vec::with_capacity(arr.len()); - for elem in arr { - if let Value::Object(object) = elem { - let mut map = Map::new(); - flatten_object_disallow_array(&mut map, None, object, separator)?; - new_arr.push(Value::Object(map)) - } else { - new_arr.push(elem) - } + // if value is object then decompose this list into lists + if arr.iter().any(|value| value.is_object()) { + flatten_array_objects(map, &new_key, arr, separator)?; + } else { + map.insert(new_key, Value::Array(arr)); } - map.insert(new_key, Value::Array(new_arr)); } x => { map.insert(new_key, x); @@ -77,34 +73,71 @@ pub fn flatten_object( Ok(()) } -fn flatten_object_disallow_array( +pub fn flatten_array_objects( map: &mut Map, - parent_key: Option<&str>, - object: Map, + parent_key: &str, + arr: Vec, separator: &str, ) -> Result<(), ()> { - for (key, value) in object.into_iter() { - let new_key = parent_key.map_or_else( - || key.clone(), - |parent_key| format!("{}{}{}", parent_key, separator, key), - ); - match value { - Value::Object(obj) => { - flatten_object_disallow_array(map, Some(&new_key), obj, separator)?; + let mut columns: Vec<(String, Vec)> = Vec::new(); + let mut len = 0; + for value in arr { + if let Value::Object(object) = value { + let mut flattened_object = Map::new(); + flatten_object(&mut flattened_object, None, object, separator)?; + let mut col_index = 0; + for (key, value) in flattened_object.into_iter().sorted_by(|a, b| a.0.cmp(&b.0)) { + loop { + if let Some((column_name, column)) = columns.get_mut(col_index) { + match (*column_name).cmp(&key) { + std::cmp::Ordering::Less => { + column.push(Value::Null); + col_index += 1; + continue; + } + std::cmp::Ordering::Equal => column.push(value), + std::cmp::Ordering::Greater => { + let mut list = vec![Value::Null; len]; + list.push(value); + columns.insert(col_index, (key, list)); + } + } + } else { + let mut list = vec![Value::Null; len]; + list.push(value); + columns.push((key, list)); + } + col_index += 1; + break; + } } - Value::Array(_) => return Err(()), - x => { - map.insert(new_key, x); + for (_, column) in &mut columns[col_index..] { + column.push(Value::Null) } + } else if value.is_null() { + for (_, column) in &mut columns { + column.push(Value::Null) + } + } else { + return Err(()); } + len += 1; + } + + for (key, arr) in columns { + let new_key = format!("{}{}{}", parent_key, separator, key); + map.insert(new_key, Value::Array(arr)); } + Ok(()) } #[cfg(test)] mod tests { + use crate::utils::json::flatten::flatten_array_objects; + use super::flatten; - use serde_json::json; + use serde_json::{json, Map, Value}; #[test] fn flatten_single_key_string() { @@ -152,29 +185,128 @@ mod tests { } #[test] - fn nested_array() { - let obj = json!({"key": ["value1", "value2"]}); - assert_eq!(obj.clone(), flatten(obj, ".").unwrap()); + fn nested_obj_array() { + let obj = json!({"key": [{"a": "value0"}, {"a": "value1"}]}); + assert_eq!( + json!({"key.a": ["value0", "value1"]}), + flatten(obj, ".").unwrap() + ); } #[test] - fn nested_obj_array() { - let obj = json!({"key": ["value1", {"key": "value2"}]}); - assert_eq!(obj.clone(), flatten(obj, ".").unwrap()); + fn nested_obj_array_nulls() { + let obj = json!({"key": [{"a": "value0"}, {"a": "value1", "b": "value1"}]}); + assert_eq!( + json!({"key.a": ["value0", "value1"], "key.b": [null, "value1"]}), + flatten(obj, ".").unwrap() + ); } #[test] - fn flatten_mixed_object() { - let obj = json!({"a": 42, "arr": ["1", {"key": "2"}, {"key": {"nested": "3"}}]}); + fn nested_obj_array_nulls_reversed() { + let obj = json!({"key": [{"a": "value0", "b": "value0"}, {"a": "value1"}]}); + assert_eq!( + json!({"key.a": ["value0", "value1"], "key.b": ["value0", null]}), + flatten(obj, ".").unwrap() + ); + } + + #[test] + fn nested_obj_array_nested_obj() { + let obj = json!({"key": [{"a": {"p": 0}, "b": "value0"}, {"b": "value1"}]}); assert_eq!( - json!({"a": 42, "arr": ["1", {"key": "2"}, {"key.nested": "3"}]}), + json!({"key.a.p": [0, null], "key.b": ["value0", "value1"]}), flatten(obj, ".").unwrap() ); } #[test] - fn err_flatten_mixed_object_with_array_within_object() { - let obj = json!({"a": 42, "arr": ["1", {"key": [1,2,4]}]}); + fn nested_obj_array_nested_obj_array() { + let obj = json!({"key": [{"a": [{"p": "value0", "q": "value0"}, {"p": "value1", "q": null}], "b": "value0"}, {"b": "value1"}]}); + assert_eq!( + json!({"key.a.p": [["value0", "value1"], null], "key.a.q": [["value0", null], null], "key.b": ["value0", "value1"]}), + flatten(obj, ".").unwrap() + ); + } + + #[test] + fn flatten_mixed_object() { + let obj = json!({"a": 42, "arr": ["1", {"key": "2"}, {"key": {"nested": "3"}}]}); assert!(flatten(obj, ".").is_err()); } + + #[test] + fn flatten_array_nulls_at_start() { + let Value::Array(arr) = json!([ + null, + {"p": 2, "q": 2}, + {"q": 3}, + ]) else { unreachable!() }; + + let mut map = Map::new(); + flatten_array_objects(&mut map, "key", arr, ".").unwrap(); + + assert_eq!(map.len(), 2); + assert_eq!(map.get("key.p").unwrap(), &json!([null, 2, null])); + assert_eq!(map.get("key.q").unwrap(), &json!([null, 2, 3])); + } + + #[test] + fn flatten_array_objects_nulls_at_end() { + let Value::Array(arr) = json!([{"a": 1, "b": 1}, {"a": 2}, null]) else { unreachable!() }; + + let mut map = Map::new(); + flatten_array_objects(&mut map, "key", arr, ".").unwrap(); + + assert_eq!(map.len(), 2); + assert_eq!(map.get("key.a").unwrap(), &json!([1, 2, null])); + assert_eq!(map.get("key.b").unwrap(), &json!([1, null, null])); + } + + #[test] + fn flatten_array_objects_nulls_in_middle() { + let Value::Array(arr) = json!([{"a": 1, "b": 1}, null, {"a": 3, "c": 3}]) else { unreachable!() }; + + let mut map = Map::new(); + flatten_array_objects(&mut map, "key", arr, ".").unwrap(); + + assert_eq!(map.len(), 3); + assert_eq!(map.get("key.a").unwrap(), &json!([1, null, 3])); + assert_eq!(map.get("key.b").unwrap(), &json!([1, null, null])); + assert_eq!(map.get("key.c").unwrap(), &json!([null, null, 3])); + } + + #[test] + fn flatten_array_test() { + let Value::Array(arr) = json!([ + {"p": 1, "q": 1}, + {"r": 2, "q": 2}, + {"p": 3, "r": 3} + ]) else { unreachable!() }; + + let mut map = Map::new(); + flatten_array_objects(&mut map, "key", arr, ".").unwrap(); + + assert_eq!(map.len(), 3); + assert_eq!(map.get("key.p").unwrap(), &json!([1, null, 3])); + assert_eq!(map.get("key.q").unwrap(), &json!([1, 2, null])); + assert_eq!(map.get("key.r").unwrap(), &json!([null, 2, 3])); + } + + #[test] + fn flatten_array_nested_test() { + let Value::Array(arr) = json!([ + {"p": 1, "q": [{"x": 1}, {"x": 2}]}, + {"r": 2, "q": [{"x": 1}]}, + {"p": 3, "r": 3} + ]) else { unreachable!() }; + + let mut map = Map::new(); + flatten_array_objects(&mut map, "key", arr, ".").unwrap(); + + assert_eq!(map.len(), 3); + assert_eq!(map.get("key.p").unwrap(), &json!([1, null, 3])); + assert_eq!(map.get("key.q.x").unwrap(), &json!([[1, 2], [1], null])); + assert_eq!(map.get("key.r").unwrap(), &json!([null, 2, 3])); + } }