diff --git a/crates/ruma-common/src/push/condition.rs b/crates/ruma-common/src/push/condition.rs index 11fde7b3..dd78d3fd 100644 --- a/crates/ruma-common/src/push/condition.rs +++ b/crates/ruma-common/src/push/condition.rs @@ -5,18 +5,21 @@ use regex::Regex; #[cfg(feature = "unstable-msc3931")] use ruma_macros::StringEnum; use serde::{Deserialize, Serialize}; -use serde_json::{to_value as to_json_value, value::Value as JsonValue}; -use tracing::{instrument, warn}; +use serde_json::value::Value as JsonValue; use wildmatch::WildMatch; -use crate::{power_levels::NotificationPowerLevels, serde::Raw, OwnedRoomId, OwnedUserId, UserId}; +use crate::{power_levels::NotificationPowerLevels, OwnedRoomId, OwnedUserId, UserId}; #[cfg(feature = "unstable-msc3931")] use crate::{PrivOwnedStr, RoomVersionId}; +mod flattened_json; mod push_condition_serde; mod room_member_count_is; -pub use room_member_count_is::{ComparisonOperator, RoomMemberCountIs}; +pub use self::{ + flattened_json::FlattenedJson, + room_member_count_is::{ComparisonOperator, RoomMemberCountIs}, +}; /// Features supported by room versions. #[cfg(feature = "unstable-msc3931")] @@ -410,61 +413,12 @@ impl StrExt for str { } } -/// The flattened representation of a JSON object. -#[derive(Clone, Debug)] -pub struct FlattenedJson { - /// The internal map containing the flattened JSON as a pair path, value. - map: BTreeMap, -} - -impl FlattenedJson { - /// Create a `FlattenedJson` from `Raw`. - pub fn from_raw(raw: &Raw) -> Self { - let mut s = Self { map: BTreeMap::new() }; - s.flatten_value(to_json_value(raw).unwrap(), "".into()); - s - } - - /// Flatten and insert the `value` at `path`. - #[instrument(skip(self, value))] - fn flatten_value(&mut self, value: JsonValue, path: String) { - match value { - JsonValue::Object(fields) => { - for (key, value) in fields { - let key = escape_key(&key); - let path = if path.is_empty() { key } else { format!("{path}.{key}") }; - self.flatten_value(value, path); - } - } - JsonValue::String(s) => { - if self.map.insert(path.clone(), s).is_some() { - warn!("Duplicate path in flattened JSON: {path}"); - } - } - JsonValue::Number(_) | JsonValue::Bool(_) | JsonValue::Array(_) | JsonValue::Null => {} - } - } - - /// Value associated with the given `path`. - pub fn get(&self, path: &str) -> Option<&str> { - self.map.get(path).map(|s| s.as_str()) - } -} - -/// Escape a key for path matching. -/// -/// This escapes the dots (`.`) and backslashes (`\`) in the key with a backslash. -fn escape_key(key: &str) -> String { - key.replace('\\', r"\\").replace('.', r"\.") -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; use js_int::{int, uint}; - use maplit::btreemap; use serde_json::{ from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, }; @@ -796,53 +750,4 @@ mod tests { assert!(room_version_condition.applies(&simple_event, &context_matching)); assert!(!room_version_condition.applies(&simple_event, &context_not_matching)); } - - #[test] - fn flattened_json_values() { - let raw = serde_json::from_str::>( - r#"{ - "string": "Hello World", - "number": 10, - "array": [1, 2], - "boolean": true, - "null": null - }"#, - ) - .unwrap(); - - let flattened = FlattenedJson::from_raw(&raw); - assert_eq!(flattened.map, btreemap! { "string".into() => "Hello World".into() }); - } - - #[test] - fn flattened_json_nested() { - let raw = serde_json::from_str::>( - r#"{ - "desc": "Level 0", - "desc.bis": "Level 0 bis", - "up": { - "desc": "Level 1", - "desc.bis": "Level 1 bis", - "up": { - "desc": "Level 2", - "desc\\bis": "Level 2 bis" - } - } - }"#, - ) - .unwrap(); - - let flattened = FlattenedJson::from_raw(&raw); - assert_eq!( - flattened.map, - btreemap! { - "desc".into() => "Level 0".into(), - r"desc\.bis".into() => "Level 0 bis".into(), - "up.desc".into() => "Level 1".into(), - r"up.desc\.bis".into() => "Level 1 bis".into(), - "up.up.desc".into() => "Level 2".into(), - r"up.up.desc\\bis".into() => "Level 2 bis".into(), - }, - ); - } } diff --git a/crates/ruma-common/src/push/condition/flattened_json.rs b/crates/ruma-common/src/push/condition/flattened_json.rs new file mode 100644 index 00000000..8b7e599d --- /dev/null +++ b/crates/ruma-common/src/push/condition/flattened_json.rs @@ -0,0 +1,111 @@ +use serde_json::{to_value as to_json_value, value::Value as JsonValue}; +use std::collections::BTreeMap; +use tracing::{instrument, warn}; + +use crate::serde::Raw; + +/// The flattened representation of a JSON object. +#[derive(Clone, Debug)] +pub struct FlattenedJson { + /// The internal map containing the flattened JSON as a pair path, value. + map: BTreeMap, +} + +impl FlattenedJson { + /// Create a `FlattenedJson` from `Raw`. + pub fn from_raw(raw: &Raw) -> Self { + let mut s = Self { map: BTreeMap::new() }; + s.flatten_value(to_json_value(raw).unwrap(), "".into()); + s + } + + /// Flatten and insert the `value` at `path`. + #[instrument(skip(self, value))] + fn flatten_value(&mut self, value: JsonValue, path: String) { + match value { + JsonValue::Object(fields) => { + for (key, value) in fields { + let key = escape_key(&key); + let path = if path.is_empty() { key } else { format!("{path}.{key}") }; + self.flatten_value(value, path); + } + } + JsonValue::String(s) => { + if self.map.insert(path.clone(), s).is_some() { + warn!("Duplicate path in flattened JSON: {path}"); + } + } + JsonValue::Number(_) | JsonValue::Bool(_) | JsonValue::Array(_) | JsonValue::Null => {} + } + } + + /// Value associated with the given `path`. + pub fn get(&self, path: &str) -> Option<&str> { + self.map.get(path).map(|s| s.as_str()) + } +} + +/// Escape a key for path matching. +/// +/// This escapes the dots (`.`) and backslashes (`\`) in the key with a backslash. +fn escape_key(key: &str) -> String { + key.replace('\\', r"\\").replace('.', r"\.") +} + +#[cfg(test)] +mod tests { + use maplit::btreemap; + use serde_json::Value as JsonValue; + + use super::FlattenedJson; + use crate::serde::Raw; + + #[test] + fn flattened_json_values() { + let raw = serde_json::from_str::>( + r#"{ + "string": "Hello World", + "number": 10, + "array": [1, 2], + "boolean": true, + "null": null + }"#, + ) + .unwrap(); + + let flattened = FlattenedJson::from_raw(&raw); + assert_eq!(flattened.map, btreemap! { "string".into() => "Hello World".into() }); + } + + #[test] + fn flattened_json_nested() { + let raw = serde_json::from_str::>( + r#"{ + "desc": "Level 0", + "desc.bis": "Level 0 bis", + "up": { + "desc": "Level 1", + "desc.bis": "Level 1 bis", + "up": { + "desc": "Level 2", + "desc\\bis": "Level 2 bis" + } + } + }"#, + ) + .unwrap(); + + let flattened = FlattenedJson::from_raw(&raw); + assert_eq!( + flattened.map, + btreemap! { + "desc".into() => "Level 0".into(), + r"desc\.bis".into() => "Level 0 bis".into(), + "up.desc".into() => "Level 1".into(), + r"up.desc\.bis".into() => "Level 1 bis".into(), + "up.up.desc".into() => "Level 2".into(), + r"up.up.desc\\bis".into() => "Level 2 bis".into(), + }, + ); + } +}