ruma_common/push/condition/
flattened_json.rs1use 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 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 #[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 pub fn get(&self, path: &str) -> Option<&FlattenedJsonValue> {
60 self.map.get(path)
61 }
62
63 pub fn get_str(&self, path: &str) -> Option<&str> {
65 self.map.get(path).and_then(|v| v.as_str())
66 }
67
68 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
76fn escape_key(key: &str) -> String {
80 key.replace('\\', r"\\").replace('.', r"\.")
81}
82
83#[derive(Debug, Error)]
85#[allow(clippy::exhaustive_enums)]
86enum IntoJsonSubsetError {
87 #[error("number found is not a valid `js_int::Int`")]
89 IntConvert,
90
91 #[error("JSON type is not accepted in this subset")]
93 NotInSubset,
94}
95
96#[derive(Debug, Clone, Default, Eq, PartialEq)]
98#[allow(clippy::exhaustive_enums)]
99pub enum ScalarJsonValue {
100 #[default]
102 Null,
103
104 Bool(bool),
106
107 Integer(Int),
109
110 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 pub fn as_bool(&self) -> Option<bool> {
130 as_variant!(self, Self::Bool).copied()
131 }
132
133 pub fn as_integer(&self) -> Option<Int> {
135 as_variant!(self, Self::Integer).copied()
136 }
137
138 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#[derive(Debug, Clone, Default, Eq, PartialEq)]
207#[allow(clippy::exhaustive_enums)]
208pub enum FlattenedJsonValue {
209 #[default]
211 Null,
212
213 Bool(bool),
215
216 Integer(Int),
218
219 String(String),
221
222 Array(Vec<ScalarJsonValue>),
224
225 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 vec.into_iter()
239 .filter_map(|v| ScalarJsonValue::try_from_json_value(v).ok())
240 .collect::<Vec<_>>(),
241 ),
242 _ => None?,
243 })
244 }
245
246 pub fn as_bool(&self) -> Option<bool> {
248 as_variant!(self, Self::Bool).copied()
249 }
250
251 pub fn as_integer(&self) -> Option<Int> {
253 as_variant!(self, Self::Integer).copied()
254 }
255
256 pub fn as_str(&self) -> Option<&str> {
258 as_variant!(self, Self::String)
259 }
260
261 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}