ruma_events/space/
child.rs

1//! Types for the [`m.space.child`] event.
2//!
3//! [`m.space.child`]: https://spec.matrix.org/latest/client-server-api/#mspacechild
4
5use std::{cmp::Ordering, ops::Deref};
6
7use ruma_common::{
8    serde::{JsonCastable, JsonObject},
9    MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedServerName, OwnedSpaceChildOrder, OwnedUserId,
10    RoomId, SpaceChildOrder,
11};
12use ruma_macros::{Event, EventContent};
13use serde::{Deserialize, Serialize};
14
15use crate::{StateEvent, SyncStateEvent};
16
17/// The content of an `m.space.child` event.
18///
19/// The admins of a space can advertise rooms and subspaces for their space by setting
20/// `m.space.child` state events.
21///
22/// The `state_key` is the ID of a child room or space, and the content must contain a `via` key
23/// which gives a list of candidate servers that can be used to join the room.
24#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
25#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
26#[ruma_event(type = "m.space.child", kind = State, state_key_type = OwnedRoomId)]
27pub struct SpaceChildEventContent {
28    /// List of candidate servers that can be used to join the room.
29    pub via: Vec<OwnedServerName>,
30
31    /// Provide a default ordering of siblings in the room list.
32    ///
33    /// Rooms are sorted based on a lexicographic ordering of the Unicode codepoints of the
34    /// characters in `order` values. Rooms with no `order` come last, in ascending numeric order
35    /// of the origin_server_ts of their m.room.create events, or ascending lexicographic order of
36    /// their room_ids in case of equal `origin_server_ts`. `order`s which are not strings, or do
37    /// not consist solely of ascii characters in the range `\x20` (space) to `\x7E` (`~`), or
38    /// consist of more than 50 characters, are forbidden and the field should be ignored if
39    /// received.
40    ///
41    /// During deserialization, this field is set to `None` if it is invalid.
42    #[serde(
43        default,
44        deserialize_with = "ruma_common::serde::default_on_error",
45        skip_serializing_if = "Option::is_none"
46    )]
47    pub order: Option<OwnedSpaceChildOrder>,
48
49    /// Space admins can mark particular children of a space as "suggested".
50    ///
51    /// This mainly serves as a hint to clients that that they can be displayed differently, for
52    /// example by showing them eagerly in the room list. A child which is missing the `suggested`
53    /// property is treated identically to a child with `"suggested": false`. A suggested child may
54    /// be a room or a subspace.
55    ///
56    /// Defaults to `false`.
57    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
58    pub suggested: bool,
59}
60
61impl SpaceChildEventContent {
62    /// Creates a new `SpaceChildEventContent` with the given routing servers.
63    pub fn new(via: Vec<OwnedServerName>) -> Self {
64        Self { via, order: None, suggested: false }
65    }
66}
67
68/// An `m.space.child` event represented as a Stripped State Event with an added `origin_server_ts`
69/// key.
70#[derive(Clone, Debug, Event)]
71#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
72pub struct HierarchySpaceChildEvent {
73    /// The content of the space child event.
74    pub content: SpaceChildEventContent,
75
76    /// The fully-qualified ID of the user who sent this event.
77    pub sender: OwnedUserId,
78
79    /// The room ID of the child.
80    pub state_key: OwnedRoomId,
81
82    /// Timestamp in milliseconds on originating homeserver when this event was sent.
83    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
84}
85
86impl PartialEq for HierarchySpaceChildEvent {
87    fn eq(&self, other: &Self) -> bool {
88        self.space_child_ord_fields().eq(&other.space_child_ord_fields())
89    }
90}
91
92impl Eq for HierarchySpaceChildEvent {}
93
94impl Ord for HierarchySpaceChildEvent {
95    fn cmp(&self, other: &Self) -> Ordering {
96        self.space_child_ord_fields().cmp(&other.space_child_ord_fields())
97    }
98}
99
100impl PartialOrd for HierarchySpaceChildEvent {
101    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
102        Some(self.cmp(other))
103    }
104}
105
106impl JsonCastable<HierarchySpaceChildEvent> for SpaceChildEvent {}
107
108impl JsonCastable<HierarchySpaceChildEvent> for OriginalSpaceChildEvent {}
109
110impl JsonCastable<HierarchySpaceChildEvent> for SyncSpaceChildEvent {}
111
112impl JsonCastable<HierarchySpaceChildEvent> for OriginalSyncSpaceChildEvent {}
113
114impl JsonCastable<JsonObject> for HierarchySpaceChildEvent {}
115
116/// Helper trait to sort `m.space.child` events using using the algorithm for [ordering children
117/// within a space].
118///
119/// This trait can be used to sort a slice using `.sort_by(SpaceChildOrd::cmp_space_child)`. It is
120/// also possible to use [`SpaceChildOrdHelper`] to sort the events in a `BTreeMap` or a `BTreeSet`.
121///
122/// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
123pub trait SpaceChildOrd {
124    #[doc(hidden)]
125    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_>;
126
127    /// Return an [`Ordering`] between `self` and `other`, using the algorithm for [ordering
128    /// children within a space].
129    ///
130    /// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
131    fn cmp_space_child(&self, other: &impl SpaceChildOrd) -> Ordering {
132        self.space_child_ord_fields().cmp(&other.space_child_ord_fields())
133    }
134}
135
136/// Fields necessary to implement `Ord` for space child events using the algorithm for [ordering
137/// children within a space].
138///
139/// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
140#[doc(hidden)]
141#[derive(PartialEq, Eq)]
142pub struct SpaceChildOrdFields<'a> {
143    order: Option<&'a SpaceChildOrder>,
144    origin_server_ts: MilliSecondsSinceUnixEpoch,
145    state_key: &'a RoomId,
146}
147
148impl<'a> SpaceChildOrdFields<'a> {
149    /// Construct a new `SpaceChildEventOrdFields` with the given values.
150    ///
151    /// Filters the order if it is invalid.
152    fn new(
153        order: Option<&'a SpaceChildOrder>,
154        origin_server_ts: MilliSecondsSinceUnixEpoch,
155        state_key: &'a RoomId,
156    ) -> Self {
157        Self { order, origin_server_ts, state_key }
158    }
159}
160
161impl<'a> Ord for SpaceChildOrdFields<'a> {
162    fn cmp(&self, other: &Self) -> Ordering {
163        match (self.order, other.order) {
164            // Events with order are ordered before events without order.
165            (Some(_), None) => Ordering::Less,
166            (None, Some(_)) => Ordering::Greater,
167            (Some(self_order), Some(other_order)) => self_order.cmp(other_order),
168            (None, None) => Ordering::Equal,
169        }
170        .then_with(|| self.origin_server_ts.cmp(&other.origin_server_ts))
171        .then_with(|| self.state_key.cmp(other.state_key))
172    }
173}
174
175impl<'a> PartialOrd for SpaceChildOrdFields<'a> {
176    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
177        Some(self.cmp(other))
178    }
179}
180
181impl<T> SpaceChildOrd for &T
182where
183    T: SpaceChildOrd,
184{
185    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
186        (*self).space_child_ord_fields()
187    }
188}
189
190impl SpaceChildOrd for OriginalSpaceChildEvent {
191    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
192        SpaceChildOrdFields::new(
193            self.content.order.as_deref(),
194            self.origin_server_ts,
195            &self.state_key,
196        )
197    }
198}
199
200impl SpaceChildOrd for RedactedSpaceChildEvent {
201    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
202        SpaceChildOrdFields::new(None, self.origin_server_ts, &self.state_key)
203    }
204}
205
206impl SpaceChildOrd for SpaceChildEvent {
207    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
208        match self {
209            StateEvent::Original(original) => original.space_child_ord_fields(),
210            StateEvent::Redacted(redacted) => redacted.space_child_ord_fields(),
211        }
212    }
213}
214
215impl SpaceChildOrd for OriginalSyncSpaceChildEvent {
216    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
217        SpaceChildOrdFields::new(
218            self.content.order.as_deref(),
219            self.origin_server_ts,
220            &self.state_key,
221        )
222    }
223}
224
225impl SpaceChildOrd for RedactedSyncSpaceChildEvent {
226    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
227        SpaceChildOrdFields::new(None, self.origin_server_ts, &self.state_key)
228    }
229}
230
231impl SpaceChildOrd for SyncSpaceChildEvent {
232    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
233        match self {
234            SyncStateEvent::Original(original) => original.space_child_ord_fields(),
235            SyncStateEvent::Redacted(redacted) => redacted.space_child_ord_fields(),
236        }
237    }
238}
239
240impl SpaceChildOrd for HierarchySpaceChildEvent {
241    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
242        SpaceChildOrdFields::new(
243            self.content.order.as_deref(),
244            self.origin_server_ts,
245            &self.state_key,
246        )
247    }
248}
249
250/// Helper type to sort `m.space.child` events using using the algorithm for [ordering children
251/// within a space].
252///
253/// This type can be use with `BTreeMap` or `BTreeSet` to order space child events.
254///
255/// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
256#[derive(Debug, Clone)]
257#[allow(clippy::exhaustive_structs)]
258pub struct SpaceChildOrdHelper<T: SpaceChildOrd>(pub T);
259
260impl<T: SpaceChildOrd> PartialEq for SpaceChildOrdHelper<T> {
261    fn eq(&self, other: &Self) -> bool {
262        self.0.space_child_ord_fields().eq(&other.0.space_child_ord_fields())
263    }
264}
265
266impl<T: SpaceChildOrd> Eq for SpaceChildOrdHelper<T> {}
267
268impl<T: SpaceChildOrd> Ord for SpaceChildOrdHelper<T> {
269    fn cmp(&self, other: &Self) -> Ordering {
270        self.0.cmp_space_child(&other.0)
271    }
272}
273
274impl<T: SpaceChildOrd> PartialOrd for SpaceChildOrdHelper<T> {
275    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
276        Some(self.cmp(other))
277    }
278}
279
280impl<T: SpaceChildOrd> Deref for SpaceChildOrdHelper<T> {
281    type Target = T;
282
283    fn deref(&self) -> &Self::Target {
284        &self.0
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use std::{collections::BTreeSet, iter::repeat_n};
291
292    use js_int::{uint, UInt};
293    use ruma_common::{
294        owned_server_name, owned_user_id, room_id, server_name, MilliSecondsSinceUnixEpoch, RoomId,
295        SpaceChildOrder,
296    };
297    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
298
299    use super::{
300        HierarchySpaceChildEvent, SpaceChildEventContent, SpaceChildOrd, SpaceChildOrdHelper,
301    };
302
303    #[test]
304    fn space_child_serialization() {
305        let content = SpaceChildEventContent {
306            via: vec![server_name!("example.com").to_owned()],
307            order: Some(SpaceChildOrder::parse("uwu").unwrap()),
308            suggested: false,
309        };
310
311        let json = json!({
312            "via": ["example.com"],
313            "order": "uwu",
314        });
315
316        assert_eq!(to_json_value(&content).unwrap(), json);
317    }
318
319    #[test]
320    fn space_child_empty_serialization() {
321        let content = SpaceChildEventContent { via: vec![], order: None, suggested: false };
322
323        let json = json!({ "via": [] });
324
325        assert_eq!(to_json_value(&content).unwrap(), json);
326    }
327
328    #[test]
329    fn space_child_content_deserialization_order() {
330        let via = server_name!("localhost");
331
332        // Valid string.
333        let json = json!({
334            "order": "aaa",
335            "via": [via],
336        });
337        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
338        assert_eq!(content.order.unwrap(), "aaa");
339        assert!(!content.suggested);
340        assert_eq!(content.via, &[via]);
341
342        // Not a string.
343        let json = json!({
344            "order": 2,
345            "via": [via],
346        });
347        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
348        assert_eq!(content.order, None);
349        assert!(!content.suggested);
350        assert_eq!(content.via, &[via]);
351
352        // Empty string.
353        let json = json!({
354            "order": "",
355            "via": [via],
356        });
357        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
358        assert_eq!(content.order.unwrap(), "");
359        assert!(!content.suggested);
360        assert_eq!(content.via, &[via]);
361
362        // String too long.
363        let order = repeat_n('a', 60).collect::<String>();
364        let json = json!({
365            "order": order,
366            "via": [via],
367        });
368        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
369        assert_eq!(content.order, None);
370        assert!(!content.suggested);
371        assert_eq!(content.via, &[via]);
372
373        // Invalid character.
374        let json = json!({
375            "order": "🔝",
376            "via": [via],
377        });
378        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
379        assert_eq!(content.order, None);
380        assert!(!content.suggested);
381        assert_eq!(content.via, &[via]);
382    }
383
384    #[test]
385    fn hierarchy_space_child_deserialization() {
386        let json = json!({
387            "content": {
388                "via": [
389                    "example.org"
390                ]
391            },
392            "origin_server_ts": 1_629_413_349,
393            "sender": "@alice:example.org",
394            "state_key": "!a:example.org",
395            "type": "m.space.child"
396        });
397
398        let ev = from_json_value::<HierarchySpaceChildEvent>(json).unwrap();
399        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1_629_413_349)));
400        assert_eq!(ev.sender, "@alice:example.org");
401        assert_eq!(ev.state_key, "!a:example.org");
402        assert_eq!(ev.content.via, ["example.org"]);
403        assert_eq!(ev.content.order, None);
404        assert!(!ev.content.suggested);
405    }
406
407    /// Construct a [`HierarchySpaceChildEvent`] with the given state key, order and timestamp.
408    fn hierarchy_space_child_event(
409        state_key: &RoomId,
410        order: Option<&str>,
411        origin_server_ts: UInt,
412    ) -> HierarchySpaceChildEvent {
413        let mut content = SpaceChildEventContent::new(vec![owned_server_name!("example.org")]);
414        content.order = order.and_then(|order| SpaceChildOrder::parse(order).ok());
415
416        HierarchySpaceChildEvent {
417            content,
418            sender: owned_user_id!("@alice:example.org"),
419            state_key: state_key.to_owned(),
420            origin_server_ts: MilliSecondsSinceUnixEpoch(origin_server_ts),
421        }
422    }
423
424    #[test]
425    fn space_child_ord_spec_example() {
426        // Reproduce the example from the spec.
427        let child_a = hierarchy_space_child_event(
428            room_id!("!a:example.org"),
429            Some("aaaa"),
430            uint!(1_640_141_000),
431        );
432        let child_b = hierarchy_space_child_event(
433            room_id!("!b:example.org"),
434            Some(" "),
435            uint!(1_640_341_000),
436        );
437        let child_c = hierarchy_space_child_event(
438            room_id!("!c:example.org"),
439            Some("first"),
440            uint!(1_640_841_000),
441        );
442        let child_d =
443            hierarchy_space_child_event(room_id!("!d:example.org"), None, uint!(1_640_741_000));
444        let child_e =
445            hierarchy_space_child_event(room_id!("!e:example.org"), None, uint!(1_640_641_000));
446
447        let events =
448            [child_a.clone(), child_b.clone(), child_c.clone(), child_d.clone(), child_e.clone()];
449
450        // Using slice::sort_by.
451        let mut sorted_events = events.clone();
452        sorted_events.sort_by(SpaceChildOrd::cmp_space_child);
453        assert_eq!(sorted_events[0].state_key, child_b.state_key);
454        assert_eq!(sorted_events[1].state_key, child_a.state_key);
455        assert_eq!(sorted_events[2].state_key, child_c.state_key);
456        assert_eq!(sorted_events[3].state_key, child_e.state_key);
457        assert_eq!(sorted_events[4].state_key, child_d.state_key);
458
459        // Using BTreeSet.
460        let sorted_events = events.clone().into_iter().collect::<BTreeSet<_>>();
461        let mut iter = sorted_events.iter();
462        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
463        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
464        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
465        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
466        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
467
468        // Using BTreeSet and helper.
469        let sorted_events = events.into_iter().map(SpaceChildOrdHelper).collect::<BTreeSet<_>>();
470        let mut iter = sorted_events.iter();
471        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
472        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
473        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
474        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
475        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
476    }
477
478    #[test]
479    fn space_child_ord_other_example() {
480        // We also check invalid order and state key comparison here.
481        let child_a = hierarchy_space_child_event(
482            room_id!("!a:example.org"),
483            Some("🔝"),
484            uint!(1_640_141_000),
485        );
486        let child_b = hierarchy_space_child_event(
487            room_id!("!b:example.org"),
488            Some(" "),
489            uint!(1_640_341_000),
490        );
491        let child_c =
492            hierarchy_space_child_event(room_id!("!c:example.org"), None, uint!(1_640_841_000));
493        let child_d =
494            hierarchy_space_child_event(room_id!("!d:example.org"), None, uint!(1_640_741_000));
495        let child_e =
496            hierarchy_space_child_event(room_id!("!e:example.org"), None, uint!(1_640_741_000));
497
498        let mut events =
499            [child_a.clone(), child_b.clone(), child_c.clone(), child_d.clone(), child_e.clone()];
500
501        events.sort_by(SpaceChildOrd::cmp_space_child);
502
503        // Using slice::sort_by.
504        let mut sorted_events = events.clone();
505        sorted_events.sort_by(SpaceChildOrd::cmp_space_child);
506        assert_eq!(sorted_events[0].state_key, child_b.state_key);
507        assert_eq!(sorted_events[1].state_key, child_a.state_key);
508        assert_eq!(sorted_events[2].state_key, child_d.state_key);
509        assert_eq!(sorted_events[3].state_key, child_e.state_key);
510        assert_eq!(sorted_events[4].state_key, child_c.state_key);
511
512        // Using BTreeSet.
513        let sorted_events = events.clone().into_iter().collect::<BTreeSet<_>>();
514        let mut iter = sorted_events.iter();
515        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
516        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
517        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
518        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
519        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
520
521        // Using BTreeSet and helper.
522        let sorted_events = events.into_iter().map(SpaceChildOrdHelper).collect::<BTreeSet<_>>();
523        let mut iter = sorted_events.iter();
524        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
525        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
526        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
527        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
528        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
529    }
530}