diff --git a/Cargo.toml b/Cargo.toml index 15843bd..2c942f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ nom = "8.0.0" num-traits = "0.2.19" ordered-float = { version = "5.1.0", default-features = false } rand = { version = "0.9.2", features = ["small_rng"] } -serde = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", default-features = false, features = ["std"] } zmij = "1.0" diff --git a/src/functions/path.rs b/src/functions/path.rs index 254b749..8e79f64 100644 --- a/src/functions/path.rs +++ b/src/functions/path.rs @@ -1053,12 +1053,20 @@ impl RawJsonb<'_> { /// along with their corresponding key paths. The key path represents the navigation /// path from the root to reach each scalar value. /// + /// # Arguments + /// + /// * `ignore_array` - When true, arrays are treated as leaf values and returned as + /// `Value::Array` without descending into their elements. + /// /// # Returns /// /// * `Result, Value<'_>)>>` - A vector of tuples, each containing: /// - `KeyPaths`: The path to reach the scalar value /// - `Value`: The scalar value itself /// + /// Empty objects or arrays are treated as leaf values and returned as `Value::Object` or + /// `Value::Array`. + /// /// # Examples /// /// ```rust @@ -1067,7 +1075,7 @@ impl RawJsonb<'_> { /// let json = r#"{"user": {"name": "Alice", "scores": [85, 92, 78]}}"#; /// let jsonb = json.parse::().unwrap(); /// let raw_jsonb = jsonb.as_raw(); - /// let result = raw_jsonb.extract_scalar_key_values(); + /// let result = raw_jsonb.extract_scalar_key_values(false); /// assert!(result.is_ok()); /// let result = result.unwrap(); /// assert_eq!(result.len(), 4); @@ -1077,11 +1085,34 @@ impl RawJsonb<'_> { /// // - path: "user", "scores", 1 -> value: 92 /// // - path: "user", "scores", 2 -> value: 78 /// ``` - pub fn extract_scalar_key_values(&self) -> Result, Value<'_>)>> { + /// + /// ```rust + /// use jsonb::{OwnedJsonb, Value}; + /// + /// let json = r#"{"user": {"name": "Alice", "scores": [85, 92, 78]}}"#; + /// let jsonb = json.parse::().unwrap(); + /// let raw_jsonb = jsonb.as_raw(); + /// let result = raw_jsonb.extract_scalar_key_values(true).unwrap(); + /// + /// assert_eq!(result.len(), 2); + /// assert!(result.iter().any(|(_, value)| matches!(value, Value::Array(_)))); + /// // Result contains: + /// // - path: "user", "name" -> value: "Alice" + /// // - path: "user", "scores" -> value: [85, 92, 78] + /// ``` + pub fn extract_scalar_key_values( + &self, + ignore_array: bool, + ) -> Result, Value<'_>)>> { let item = JsonbItem::from_raw_jsonb(*self)?; let mut result = Vec::with_capacity(16); let mut current_paths = Vec::with_capacity(3); - Self::extract_scalar_key_values_recursive(item, &mut current_paths, &mut result)?; + Self::extract_scalar_key_values_recursive( + item, + ignore_array, + &mut current_paths, + &mut result, + )?; Ok(result) } @@ -1089,6 +1120,8 @@ impl RawJsonb<'_> { /// /// This function implements a depth-first traversal of the JSONB document, building up the /// key path as it goes and collecting scalar values when it reaches leaf nodes. + /// Empty objects or arrays are treated as leaf values and returned as `Value::Object` or + /// `Value::Array` instead of being skipped. /// /// # Arguments /// @@ -1101,6 +1134,7 @@ impl RawJsonb<'_> { /// * `Result<()>` - Success or error during traversal fn extract_scalar_key_values_recursive<'a>( current_item: JsonbItem<'a>, + ignore_array: bool, current_paths: &mut Vec>, result: &mut Vec<(KeyPaths<'a>, Value<'a>)>, ) -> Result<()> { @@ -1108,25 +1142,48 @@ impl RawJsonb<'_> { JsonbItem::Raw(raw) => { let object_iter_opt = ObjectIterator::new(raw)?; if let Some(mut object_iter) = object_iter_opt { - for object_result in &mut object_iter { - let (key, val_item) = object_result?; - current_paths.push(KeyPath::Name(Cow::Borrowed(key))); - // Recursively handle object values - Self::extract_scalar_key_values_recursive(val_item, current_paths, result)?; - current_paths.pop(); + if object_iter.len() > 0 { + for object_result in &mut object_iter { + let (key, val_item) = object_result?; + current_paths.push(KeyPath::Name(Cow::Borrowed(key))); + // Recursively handle object values + Self::extract_scalar_key_values_recursive( + val_item, + ignore_array, + current_paths, + result, + )?; + current_paths.pop(); + } + return Ok(()); } - return Ok(()); - } - let array_iter_opt = ArrayIterator::new(raw)?; - if let Some(array_iter) = array_iter_opt { - for (index, array_result) in &mut array_iter.enumerate() { - let val_item = array_result?; - current_paths.push(KeyPath::Index(index as i32)); - // Recursively handle array values - Self::extract_scalar_key_values_recursive(val_item, current_paths, result)?; - current_paths.pop(); + } else if !ignore_array { + let array_iter_opt = ArrayIterator::new(raw)?; + if let Some(array_iter) = array_iter_opt { + if array_iter.len() > 0 { + for (index, array_result) in &mut array_iter.enumerate() { + let val_item = array_result?; + current_paths.push(KeyPath::Index(index as i32)); + // Recursively handle array values + Self::extract_scalar_key_values_recursive( + val_item, + ignore_array, + current_paths, + result, + )?; + current_paths.pop(); + } + return Ok(()); + } } } + if !current_paths.is_empty() { + let key_paths = KeyPaths { + paths: current_paths.clone(), + }; + let value = raw.to_value()?; + result.push((key_paths, value)); + } } JsonbItem::Owned(_) => unreachable!(), _ => { diff --git a/src/keypath.rs b/src/keypath.rs index 6f056a1..42d51a9 100644 --- a/src/keypath.rs +++ b/src/keypath.rs @@ -50,6 +50,23 @@ pub enum KeyPath<'a> { Name(Cow<'a, str>), } +/// Represents a set of owned key path chains. +#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +pub struct OwnedKeyPaths { + pub paths: Vec, +} + +/// Represents a valid owned key path. +#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +pub enum OwnedKeyPath { + /// represents the index of an Array, allow negative indexing. + Index(i32), + /// represents the quoted field name of an Object. + QuotedName(String), + /// represents the field name of an Object. + Name(String), +} + impl Display for KeyPaths<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{{")?; @@ -81,6 +98,73 @@ impl Display for KeyPath<'_> { } } +impl Display for OwnedKeyPaths { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{{")?; + for (i, path) in self.paths.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "{path}")?; + } + write!(f, "}}")?; + Ok(()) + } +} + +impl Display for OwnedKeyPath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + OwnedKeyPath::Index(idx) => { + write!(f, "{idx}")?; + } + OwnedKeyPath::QuotedName(name) => { + write!(f, "\"{name}\"")?; + } + OwnedKeyPath::Name(name) => { + write!(f, "{name}")?; + } + } + Ok(()) + } +} + +impl<'a> KeyPaths<'a> { + pub fn to_owned(&self) -> OwnedKeyPaths { + OwnedKeyPaths { + paths: self.paths.iter().map(KeyPath::to_owned).collect(), + } + } +} + +impl OwnedKeyPaths { + pub fn as_key_paths(&self) -> KeyPaths<'_> { + KeyPaths { + paths: self.paths.iter().map(OwnedKeyPath::as_key_path).collect(), + } + } +} + +impl<'a> KeyPath<'a> { + pub fn to_owned(&self) -> OwnedKeyPath { + match self { + KeyPath::Index(idx) => OwnedKeyPath::Index(*idx), + KeyPath::QuotedName(name) => OwnedKeyPath::QuotedName(name.to_string()), + KeyPath::Name(name) => OwnedKeyPath::Name(name.to_string()), + } + } +} + +impl OwnedKeyPath { + pub fn as_key_path(&self) -> KeyPath<'_> { + match self { + OwnedKeyPath::Index(idx) => KeyPath::Index(*idx), + OwnedKeyPath::QuotedName(name) => KeyPath::QuotedName(Cow::Borrowed(name.as_str())), + OwnedKeyPath::Name(name) => KeyPath::Name(Cow::Borrowed(name.as_str())), + } + } +} + /// Parsing the input string to key paths. pub fn parse_key_paths(input: &[u8]) -> Result, Error> { match key_paths(input) { diff --git a/src/value.rs b/src/value.rs index 0e69a0b..42dee8e 100644 --- a/src/value.rs +++ b/src/value.rs @@ -232,14 +232,17 @@ impl Display for Value<'_> { } impl<'a> Value<'a> { + /// Returns true if this value is not an array or object. pub fn is_scalar(&self) -> bool { !self.is_array() && !self.is_object() } + /// Returns true if this value is an object. pub fn is_object(&self) -> bool { matches!(self, Value::Object(_v)) } + /// Returns the object map if this value is an object. pub fn as_object(&self) -> Option<&Object<'a>> { match self { Value::Object(ref obj) => Some(obj), @@ -247,10 +250,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is an array. pub fn is_array(&self) -> bool { matches!(self, Value::Array(_v)) } + /// Returns the array if this value is an array. pub fn as_array(&self) -> Option<&Vec>> { match self { Value::Array(ref array) => Some(array), @@ -258,10 +263,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is a string. pub fn is_string(&self) -> bool { self.as_str().is_some() } + /// Returns the string if this value is a string. pub fn as_str(&self) -> Option<&Cow<'_, str>> { match self { Value::String(s) => Some(s), @@ -269,10 +276,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is a number. pub fn is_number(&self) -> bool { matches!(self, Value::Number(_)) } + /// Returns the number if this value is a number. pub fn as_number(&self) -> Option<&Number> { match self { Value::Number(n) => Some(n), @@ -280,18 +289,22 @@ impl<'a> Value<'a> { } } + /// Returns true if this value can be represented as i64. pub fn is_i64(&self) -> bool { self.as_i64().is_some() } + /// Returns true if this value can be represented as u64. pub fn is_u64(&self) -> bool { self.as_u64().is_some() } + /// Returns true if this value can be represented as f64. pub fn is_f64(&self) -> bool { self.as_f64().is_some() } + /// Returns the number as i64 if it fits. pub fn as_i64(&self) -> Option { match self { Value::Number(n) => n.as_i64(), @@ -299,6 +312,7 @@ impl<'a> Value<'a> { } } + /// Returns the number as u64 if it fits. pub fn as_u64(&self) -> Option { match self { Value::Number(n) => n.as_u64(), @@ -306,6 +320,7 @@ impl<'a> Value<'a> { } } + /// Returns the number as f64 if it is a number. pub fn as_f64(&self) -> Option { match self { Value::Number(n) => Some(n.as_f64()), @@ -313,10 +328,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is a boolean. pub fn is_boolean(&self) -> bool { matches!(self, Value::Bool(_v)) } + /// Returns the boolean if this value is a boolean. pub fn as_bool(&self) -> Option { match self { Value::Bool(v) => Some(*v), @@ -324,10 +341,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is null. pub fn is_null(&self) -> bool { matches!(self, Value::Null) } + /// Returns Some(()) if this value is null. pub fn as_null(&self) -> Option<()> { match self { Value::Null => Some(()), @@ -335,10 +354,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is a binary extension. pub fn is_binary(&self) -> bool { matches!(self, Value::Binary(_v)) } + /// Returns the binary bytes if this value is a binary extension. pub fn as_binary(&self) -> Option<&[u8]> { match self { Value::Binary(v) => Some(v), @@ -346,10 +367,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is a date extension. pub fn is_date(&self) -> bool { matches!(self, Value::Date(_v)) } + /// Returns the date if this value is a date extension. pub fn as_date(&self) -> Option<&Date> { match self { Value::Date(v) => Some(v), @@ -357,10 +380,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is a timestamp extension. pub fn is_timestamp(&self) -> bool { matches!(self, Value::Timestamp(_v)) } + /// Returns the timestamp if this value is a timestamp extension. pub fn as_timestamp(&self) -> Option<&Timestamp> { match self { Value::Timestamp(v) => Some(v), @@ -368,10 +393,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is a timestamp with time zone extension. pub fn is_timestamp_tz(&self) -> bool { matches!(self, Value::TimestampTz(_v)) } + /// Returns the timestamp with time zone if this value is that extension. pub fn as_timestamp_tz(&self) -> Option<&TimestampTz> { match self { Value::TimestampTz(v) => Some(v), @@ -379,10 +406,12 @@ impl<'a> Value<'a> { } } + /// Returns true if this value is an interval extension. pub fn is_interval(&self) -> bool { matches!(self, Value::Interval(_v)) } + /// Returns the interval if this value is an interval extension. pub fn as_interval(&self) -> Option<&Interval> { match self { Value::Interval(v) => Some(v), @@ -390,19 +419,20 @@ impl<'a> Value<'a> { } } - /// Serialize the JSONB Value into a byte stream. + /// Serializes this value into JSONB bytes, appending to `buf`. pub fn write_to_vec(&self, buf: &mut Vec) { let mut encoder = Encoder::new(buf); encoder.encode(self); } - /// Serialize the JSONB Value into a byte stream. + /// Serializes this value into JSONB bytes and returns the buffer. pub fn to_vec(&self) -> Vec { let mut buf = Vec::new(); self.write_to_vec(&mut buf); buf } + /// Returns the value for a key, case-insensitive, if this value is an object. pub fn get_by_name_ignore_case(&self, name: &str) -> Option<&Value<'a>> { match self { Value::Object(obj) => match obj.get(name) { @@ -420,6 +450,7 @@ impl<'a> Value<'a> { } } + /// Returns the array length if this value is an array. pub fn array_length(&self) -> Option { match self { Value::Array(arr) => Some(arr.len()), @@ -427,6 +458,7 @@ impl<'a> Value<'a> { } } + /// Returns the object keys as a `Value::Array` of strings if this value is an object. pub fn object_keys(&self) -> Option> { match self { Value::Object(obj) => { @@ -440,11 +472,12 @@ impl<'a> Value<'a> { } } + /// Returns true if both values have the same enum variant. pub fn eq_variant(&self, other: &Value) -> bool { discriminant(self) == discriminant(other) } - /// generate random JSONB value + /// Generates a random JSONB value for testing. pub fn rand_value() -> Value<'static> { let mut rng = rng(); let val = match rng.random_range(0..=2) { diff --git a/tests/it/functions.rs b/tests/it/functions.rs index 48c61ce..a89d3da 100644 --- a/tests/it/functions.rs +++ b/tests/it/functions.rs @@ -1953,7 +1953,7 @@ fn test_extract_scalar_key_values() { let jsonb = json.parse::().unwrap(); let raw_jsonb = jsonb.as_raw(); - let result = raw_jsonb.extract_scalar_key_values().unwrap(); + let result = raw_jsonb.extract_scalar_key_values(false).unwrap(); assert_eq!(result.len(), 3); let expected = vec![ @@ -1988,7 +1988,7 @@ fn test_extract_scalar_key_values() { let jsonb = json.parse::().unwrap(); let raw_jsonb = jsonb.as_raw(); - let result = raw_jsonb.extract_scalar_key_values().unwrap(); + let result = raw_jsonb.extract_scalar_key_values(false).unwrap(); assert_eq!(result.len(), 4); let expected = vec![ @@ -2044,7 +2044,7 @@ fn test_extract_scalar_key_values() { let jsonb = json.parse::().unwrap(); let raw_jsonb = jsonb.as_raw(); - let result = raw_jsonb.extract_scalar_key_values().unwrap(); + let result = raw_jsonb.extract_scalar_key_values(false).unwrap(); assert_eq!(result.len(), 2); let expected = vec![ @@ -2075,6 +2075,83 @@ fn test_extract_scalar_key_values() { assert_eq!(key_paths, expected_key_paths); assert_eq!(value, expected_value); } + + // Test case 4: Ignore array values + let json = r#"{"user": {"name": "Alice", "scores": [85, 92, 78]}}"#; + let jsonb = json.parse::().unwrap(); + let raw_jsonb = jsonb.as_raw(); + + let result = raw_jsonb.extract_scalar_key_values(true).unwrap(); + assert_eq!(result.len(), 2); + + let expected = vec![ + ( + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("name")), + ], + }, + Value::String(Cow::Borrowed("Alice")), + ), + ( + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("scores")), + ], + }, + Value::Array(vec![ + Value::Number(Number::UInt64(85)), + Value::Number(Number::UInt64(92)), + Value::Number(Number::UInt64(78)), + ]), + ), + ]; + for ((key_paths, value), (expected_key_paths, expected_value)) in + result.into_iter().zip(expected.into_iter()) + { + assert_eq!(key_paths, expected_key_paths); + assert_eq!(value, expected_value); + } + + // Test case 5: Empty object/array are returned as values + let json = r#"{"empty_obj": {}, "empty_arr": [], "nested": {"empty": []}}"#; + let jsonb = json.parse::().unwrap(); + let raw_jsonb = jsonb.as_raw(); + + let result = raw_jsonb.extract_scalar_key_values(false).unwrap(); + assert_eq!(result.len(), 3); + + let expected = vec![ + ( + KeyPaths { + paths: vec![KeyPath::Name(Cow::Borrowed("empty_arr"))], + }, + Value::Array(vec![]), + ), + ( + KeyPaths { + paths: vec![KeyPath::Name(Cow::Borrowed("empty_obj"))], + }, + Value::Object(BTreeMap::new()), + ), + ( + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("nested")), + KeyPath::Name(Cow::Borrowed("empty")), + ], + }, + Value::Array(vec![]), + ), + ]; + for ((key_paths, value), (expected_key_paths, expected_value)) in + result.into_iter().zip(expected.into_iter()) + { + assert_eq!(key_paths, expected_key_paths); + assert_eq!(value, expected_value); + } } #[test]