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