ruma_common/push/condition/
flattened_json.rs

1use std::collections::BTreeMap;
2
3use as_variant::as_variant;
4use js_int::Int;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::{to_value as to_json_value, value::Value as JsonValue};
7use thiserror::Error;
8use tracing::{instrument, warn};
9
10use crate::serde::Raw;
11
12/// The flattened representation of a JSON object.
13#[derive(Clone, Debug)]
14pub struct FlattenedJson {
15    /// The internal map containing the flattened JSON as a pair path, value.
16    map: BTreeMap<String, FlattenedJsonValue>,
17}
18
19impl FlattenedJson {
20    /// Create a `FlattenedJson` from `Raw`.
21    pub fn from_raw<T>(raw: &Raw<T>) -> Self {
22        Self::from_value(to_json_value(raw).unwrap())
23    }
24
25    pub(crate) fn from_value(v: JsonValue) -> Self {
26        let mut s = Self { map: BTreeMap::new() };
27        s.flatten_value(v, "".into());
28        s
29    }
30
31    /// Flatten and insert the `value` at `path`.
32    #[instrument(skip(self, value))]
33    fn flatten_value(&mut self, value: JsonValue, path: String) {
34        match value {
35            JsonValue::Object(fields) => {
36                if fields.is_empty() {
37                    if self.map.insert(path.clone(), FlattenedJsonValue::EmptyObject).is_some() {
38                        warn!("Duplicate path in flattened JSON: {path}");
39                    }
40                } else {
41                    for (key, value) in fields {
42                        let key = escape_key(&key);
43                        let path = if path.is_empty() { key } else { format!("{path}.{key}") };
44                        self.flatten_value(value, path);
45                    }
46                }
47            }
48            value => {
49                if let Some(v) = FlattenedJsonValue::from_json_value(value) {
50                    if self.map.insert(path.clone(), v).is_some() {
51                        warn!("Duplicate path in flattened JSON: {path}");
52                    }
53                }
54            }
55        }
56    }
57
58    /// Get the value associated with the given `path`.
59    pub fn get(&self, path: &str) -> Option<&FlattenedJsonValue> {
60        self.map.get(path)
61    }
62
63    /// Get the value associated with the given `path`, if it is a string.
64    pub fn get_str(&self, path: &str) -> Option<&str> {
65        self.map.get(path).and_then(|v| v.as_str())
66    }
67
68    /// Whether this flattened JSON contains an `m.mentions` property under the `content` property.
69    pub fn contains_mentions(&self) -> bool {
70        self.map
71            .keys()
72            .any(|s| s == r"content.m\.mentions" || s.starts_with(r"content.m\.mentions."))
73    }
74}
75
76/// Escape a key for path matching.
77///
78/// This escapes the dots (`.`) and backslashes (`\`) in the key with a backslash.
79fn escape_key(key: &str) -> String {
80    key.replace('\\', r"\\").replace('.', r"\.")
81}
82
83/// The set of possible errors when converting to a JSON subset.
84#[derive(Debug, Error)]
85#[allow(clippy::exhaustive_enums)]
86enum IntoJsonSubsetError {
87    /// The numeric value failed conversion to js_int::Int.
88    #[error("number found is not a valid `js_int::Int`")]
89    IntConvert,
90
91    /// The JSON type is not accepted in this subset.
92    #[error("JSON type is not accepted in this subset")]
93    NotInSubset,
94}
95
96/// Scalar (non-compound) JSON values.
97#[derive(Debug, Clone, Default, Eq, PartialEq)]
98#[allow(clippy::exhaustive_enums)]
99pub enum ScalarJsonValue {
100    /// Represents a `null` value.
101    #[default]
102    Null,
103
104    /// Represents a boolean.
105    Bool(bool),
106
107    /// Represents an integer.
108    Integer(Int),
109
110    /// Represents a string.
111    String(String),
112}
113
114impl ScalarJsonValue {
115    fn try_from_json_value(val: JsonValue) -> Result<Self, IntoJsonSubsetError> {
116        Ok(match val {
117            JsonValue::Bool(b) => Self::Bool(b),
118            JsonValue::Number(num) => Self::Integer(
119                Int::try_from(num.as_i64().ok_or(IntoJsonSubsetError::IntConvert)?)
120                    .map_err(|_| IntoJsonSubsetError::IntConvert)?,
121            ),
122            JsonValue::String(string) => Self::String(string),
123            JsonValue::Null => Self::Null,
124            _ => Err(IntoJsonSubsetError::NotInSubset)?,
125        })
126    }
127
128    /// If the `ScalarJsonValue` is a `Bool`, return the inner value.
129    pub fn as_bool(&self) -> Option<bool> {
130        as_variant!(self, Self::Bool).copied()
131    }
132
133    /// If the `ScalarJsonValue` is an `Integer`, return the inner value.
134    pub fn as_integer(&self) -> Option<Int> {
135        as_variant!(self, Self::Integer).copied()
136    }
137
138    /// If the `ScalarJsonValue` is a `String`, return a reference to the inner value.
139    pub fn as_str(&self) -> Option<&str> {
140        as_variant!(self, Self::String)
141    }
142}
143
144impl Serialize for ScalarJsonValue {
145    #[inline]
146    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147    where
148        S: Serializer,
149    {
150        match self {
151            Self::Null => serializer.serialize_unit(),
152            Self::Bool(b) => serializer.serialize_bool(*b),
153            Self::Integer(n) => n.serialize(serializer),
154            Self::String(s) => serializer.serialize_str(s),
155        }
156    }
157}
158
159impl<'de> Deserialize<'de> for ScalarJsonValue {
160    #[inline]
161    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162    where
163        D: Deserializer<'de>,
164    {
165        let val = JsonValue::deserialize(deserializer)?;
166        ScalarJsonValue::try_from_json_value(val).map_err(serde::de::Error::custom)
167    }
168}
169
170impl From<bool> for ScalarJsonValue {
171    fn from(value: bool) -> Self {
172        Self::Bool(value)
173    }
174}
175
176impl From<Int> for ScalarJsonValue {
177    fn from(value: Int) -> Self {
178        Self::Integer(value)
179    }
180}
181
182impl From<String> for ScalarJsonValue {
183    fn from(value: String) -> Self {
184        Self::String(value)
185    }
186}
187
188impl From<&str> for ScalarJsonValue {
189    fn from(value: &str) -> Self {
190        value.to_owned().into()
191    }
192}
193
194impl PartialEq<FlattenedJsonValue> for ScalarJsonValue {
195    fn eq(&self, other: &FlattenedJsonValue) -> bool {
196        match self {
197            Self::Null => *other == FlattenedJsonValue::Null,
198            Self::Bool(b) => other.as_bool() == Some(*b),
199            Self::Integer(i) => other.as_integer() == Some(*i),
200            Self::String(s) => other.as_str() == Some(s),
201        }
202    }
203}
204
205/// Possible JSON values after an object is flattened.
206#[derive(Debug, Clone, Default, Eq, PartialEq)]
207#[allow(clippy::exhaustive_enums)]
208pub enum FlattenedJsonValue {
209    /// Represents a `null` value.
210    #[default]
211    Null,
212
213    /// Represents a boolean.
214    Bool(bool),
215
216    /// Represents an integer.
217    Integer(Int),
218
219    /// Represents a string.
220    String(String),
221
222    /// Represents an array.
223    Array(Vec<ScalarJsonValue>),
224
225    /// Represents an empty object.
226    EmptyObject,
227}
228
229impl FlattenedJsonValue {
230    fn from_json_value(val: JsonValue) -> Option<Self> {
231        Some(match val {
232            JsonValue::Bool(b) => Self::Bool(b),
233            JsonValue::Number(num) => Self::Integer(Int::try_from(num.as_i64()?).ok()?),
234            JsonValue::String(string) => Self::String(string),
235            JsonValue::Null => Self::Null,
236            JsonValue::Array(vec) => Self::Array(
237                // Drop values we don't need instead of throwing an error.
238                vec.into_iter()
239                    .filter_map(|v| ScalarJsonValue::try_from_json_value(v).ok())
240                    .collect::<Vec<_>>(),
241            ),
242            _ => None?,
243        })
244    }
245
246    /// If the `FlattenedJsonValue` is a `Bool`, return the inner value.
247    pub fn as_bool(&self) -> Option<bool> {
248        as_variant!(self, Self::Bool).copied()
249    }
250
251    /// If the `FlattenedJsonValue` is an `Integer`, return the inner value.
252    pub fn as_integer(&self) -> Option<Int> {
253        as_variant!(self, Self::Integer).copied()
254    }
255
256    /// If the `FlattenedJsonValue` is a `String`, return a reference to the inner value.
257    pub fn as_str(&self) -> Option<&str> {
258        as_variant!(self, Self::String)
259    }
260
261    /// If the `FlattenedJsonValue` is an `Array`, return a reference to the inner value.
262    pub fn as_array(&self) -> Option<&[ScalarJsonValue]> {
263        as_variant!(self, Self::Array)
264    }
265}
266
267impl From<bool> for FlattenedJsonValue {
268    fn from(value: bool) -> Self {
269        Self::Bool(value)
270    }
271}
272
273impl From<Int> for FlattenedJsonValue {
274    fn from(value: Int) -> Self {
275        Self::Integer(value)
276    }
277}
278
279impl From<String> for FlattenedJsonValue {
280    fn from(value: String) -> Self {
281        Self::String(value)
282    }
283}
284
285impl From<&str> for FlattenedJsonValue {
286    fn from(value: &str) -> Self {
287        value.to_owned().into()
288    }
289}
290
291impl From<Vec<ScalarJsonValue>> for FlattenedJsonValue {
292    fn from(value: Vec<ScalarJsonValue>) -> Self {
293        Self::Array(value)
294    }
295}
296
297impl PartialEq<ScalarJsonValue> for FlattenedJsonValue {
298    fn eq(&self, other: &ScalarJsonValue) -> bool {
299        match self {
300            Self::Null => *other == ScalarJsonValue::Null,
301            Self::Bool(b) => other.as_bool() == Some(*b),
302            Self::Integer(i) => other.as_integer() == Some(*i),
303            Self::String(s) => other.as_str() == Some(s),
304            Self::Array(_) | Self::EmptyObject => false,
305        }
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use js_int::int;
312    use maplit::btreemap;
313    use serde_json::json;
314
315    use super::{FlattenedJson, FlattenedJsonValue};
316
317    #[test]
318    fn flattened_json_values() {
319        let flattened = FlattenedJson::from_value(json!({
320            "string": "Hello World",
321            "number": 10,
322            "array": [1, 2],
323            "boolean": true,
324            "null": null,
325            "empty_object": {},
326        }));
327        assert_eq!(
328            flattened.map,
329            btreemap! {
330                "string".into() => "Hello World".into(),
331                "number".into() => int!(10).into(),
332                "array".into() => vec![int!(1).into(), int!(2).into()].into(),
333                "boolean".into() => true.into(),
334                "null".into() => FlattenedJsonValue::Null,
335                "empty_object".into() => FlattenedJsonValue::EmptyObject,
336            }
337        );
338    }
339
340    #[test]
341    fn flattened_json_nested() {
342        let flattened = FlattenedJson::from_value(json!({
343            "desc": "Level 0",
344            "desc.bis": "Level 0 bis",
345            "up": {
346                "desc": 1,
347                "desc.bis": null,
348                "up": {
349                    "desc": ["Level 2a", "Level 2b"],
350                    "desc\\bis": true,
351                },
352            },
353        }));
354        assert_eq!(
355            flattened.map,
356            btreemap! {
357                "desc".into() => "Level 0".into(),
358                r"desc\.bis".into() => "Level 0 bis".into(),
359                "up.desc".into() => int!(1).into(),
360                r"up.desc\.bis".into() => FlattenedJsonValue::Null,
361                "up.up.desc".into() => vec!["Level 2a".into(), "Level 2b".into()].into(),
362                r"up.up.desc\\bis".into() => true.into(),
363            },
364        );
365    }
366
367    #[test]
368    fn contains_mentions() {
369        let flattened = FlattenedJson::from_value(json!({
370            "m.mentions": {},
371            "content": {
372                "body": "Text",
373            },
374        }));
375        assert!(!flattened.contains_mentions());
376
377        let flattened = FlattenedJson::from_value(json!({
378            "content": {
379                "body": "Text",
380                "m.mentions": {},
381            },
382        }));
383        assert!(flattened.contains_mentions());
384
385        let flattened = FlattenedJson::from_value(json!({
386            "content": {
387                "body": "Text",
388                "m.mentions": {
389                    "room": true,
390                },
391            },
392        }));
393        assert!(flattened.contains_mentions());
394    }
395}