Skip to main content

matrix_sdk_ui/spaces/
mod.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for that specific language governing permissions and
13// limitations under the License.
14
15//! High level interfaces for working with Spaces
16//!
17//! The `SpaceService` is an UI oriented, high-level interface for working with
18//! [Matrix Spaces](https://spec.matrix.org/latest/client-server-api/#spaces).
19//! It provides methods to retrieve joined spaces, subscribe
20//! to updates, and navigate space hierarchies.
21//!
22//! It consists of 3 main components:
23//! - `SpaceService`: The main service for managing spaces. It
24//! - `SpaceGraph`: An utility that maps the `m.space.parent` and
25//!   `m.space.child` fields into a graph structure, removing cycles and
26//!   providing access to top level parents.
27//! - `SpaceRoomList`: A component for retrieving a space's children rooms and
28//!   their details.
29
30use std::{
31    cmp::Ordering,
32    collections::{HashMap, HashSet, VecDeque},
33    sync::Arc,
34};
35
36use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
37use futures_util::{future::join_all, pin_mut};
38use imbl::Vector;
39use itertools::Itertools;
40use matrix_sdk::{
41    Client, Error as SDKError, Room, deserialized_responses::SyncOrStrippedState,
42    task_monitor::BackgroundTaskHandle,
43};
44use ruma::{
45    OwnedRoomId, RoomId, SpaceChildOrder,
46    events::{
47        self, StateEventType, SyncStateEvent,
48        space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
49    },
50};
51use thiserror::Error;
52use tokio::sync::Mutex as AsyncMutex;
53use tracing::{error, trace, warn};
54
55use crate::spaces::{graph::SpaceGraph, leave::LeaveSpaceHandle, room::SpaceRoomChildState};
56pub use crate::spaces::{room::SpaceRoom, room_list::SpaceRoomList};
57
58pub mod graph;
59pub mod leave;
60pub mod room;
61pub mod room_list;
62
63/// Possible [`SpaceService`] errors.
64#[derive(Debug, Error)]
65pub enum Error {
66    /// The user ID was not available from the client.
67    #[error("User ID not available from client")]
68    UserIdNotFound,
69
70    /// The requested room was not found.
71    #[error("Room `{0}` not found")]
72    RoomNotFound(OwnedRoomId),
73
74    /// The space parent/child state was missing.
75    #[error("Missing `{0}` for `{1}`")]
76    MissingState(StateEventType, OwnedRoomId),
77
78    /// Failed to set either of the m.space.parent or m.space.child state
79    /// events.
80    #[error("Failed to set either of the m.space.parent or m.space.child state events")]
81    UpdateRelationship(SDKError),
82
83    /// Failed to set the expected m.space.parent state event (but any
84    /// m.space.child changes were successful).
85    #[error(
86        "Failed to set the expected m.space.parent state event (but any m.space.child changes were successful)"
87    )]
88    UpdateInverseRelationship(SDKError),
89
90    /// Failed to leave a space.
91    #[error("Failed to leave space")]
92    LeaveSpace(SDKError),
93
94    /// Failed to load members.
95    #[error("Failed to load members")]
96    LoadRoomMembers(SDKError),
97}
98
99struct SpaceState {
100    graph: SpaceGraph,
101    top_level_joined_spaces: ObservableVector<SpaceRoom>,
102    space_filters: ObservableVector<SpaceFilter>,
103}
104
105/// The main entry point into the Spaces facilities.
106///
107/// The spaces service is responsible for retrieving one's joined rooms,
108/// building a graph out of their `m.space.parent` and `m.space.child` state
109/// events, and providing access to the top-level spaces and their children.
110///
111/// # Examples
112///
113/// ```no_run
114/// use futures_util::StreamExt;
115/// use matrix_sdk::Client;
116/// use matrix_sdk_ui::spaces::SpaceService;
117/// use ruma::owned_room_id;
118///
119/// # async {
120/// # let client: Client = todo!();
121/// let space_service = SpaceService::new(client.clone()).await;
122///
123/// // Get a list of all the joined spaces
124/// let joined_spaces = space_service.top_level_joined_spaces().await;
125///
126/// // And subscribe to changes on them
127/// // `initial_values` is equal to `top_level_joined_spaces` if nothing changed meanwhile
128/// let (initial_values, stream) =
129///     space_service.subscribe_to_top_level_joined_spaces().await;
130///
131/// while let Some(diffs) = stream.next().await {
132///     println!("Received joined spaces updates: {diffs:?}");
133/// }
134///
135/// // Get a list of all the rooms in a particular space
136/// let room_list = space_service
137///     .space_room_list(owned_room_id!("!some_space:example.org"))
138///     .await;
139///
140/// // Which can be used to retrieve information about the children rooms
141/// let children = room_list.rooms().await;
142/// # anyhow::Ok(()) };
143/// ```
144pub struct SpaceService {
145    client: Client,
146
147    space_state: Arc<AsyncMutex<SpaceState>>,
148
149    _room_update_handle: AsyncMutex<BackgroundTaskHandle>,
150}
151
152impl SpaceService {
153    /// Creates a new `SpaceService` instance.
154    pub async fn new(client: Client) -> Self {
155        let space_state = Arc::new(AsyncMutex::new(SpaceState {
156            graph: SpaceGraph::new(),
157            top_level_joined_spaces: ObservableVector::new(),
158            space_filters: ObservableVector::new(),
159        }));
160
161        let room_update_handle = client
162            .task_monitor()
163            .spawn_infinite_task("space_service", {
164                let client = client.clone();
165                let space_state = Arc::clone(&space_state);
166                let all_room_updates_receiver = client.subscribe_to_all_room_updates();
167
168                async move {
169                    pin_mut!(all_room_updates_receiver);
170
171                    loop {
172                        match all_room_updates_receiver.recv().await {
173                            Ok(updates) => {
174                                if updates.is_empty() {
175                                    continue;
176                                }
177
178                                let (spaces, filters, graph) =
179                                    Self::build_space_state(&client).await;
180                                Self::update_space_state_if_needed(
181                                    Vector::from(spaces),
182                                    Vector::from(filters),
183                                    graph,
184                                    &space_state,
185                                )
186                                .await;
187                            }
188                            Err(err) => {
189                                error!("error when listening to room updates: {err}");
190                            }
191                        }
192                    }
193                }
194            })
195            .abort_on_drop();
196
197        // Make sure to also update the currently joined spaces for the initial values.
198        let (spaces, filters, graph) = Self::build_space_state(&client).await;
199        Self::update_space_state_if_needed(
200            Vector::from(spaces),
201            Vector::from(filters),
202            graph,
203            &space_state,
204        )
205        .await;
206
207        Self { client, space_state, _room_update_handle: AsyncMutex::new(room_update_handle) }
208    }
209
210    /// Subscribes to updates on the joined spaces list. If space rooms are
211    /// joined or left, the stream will yield diffs that reflect the changes.
212    pub async fn subscribe_to_top_level_joined_spaces(
213        &self,
214    ) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
215        self.space_state
216            .lock()
217            .await
218            .top_level_joined_spaces
219            .subscribe()
220            .into_values_and_batched_stream()
221    }
222
223    /// Returns a list of all the top-level joined spaces. It will eagerly
224    /// compute the latest version and also notify subscribers if there were
225    /// any changes.
226    pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
227        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
228
229        Self::update_space_state_if_needed(
230            Vector::from(top_level_joined_spaces.clone()),
231            Vector::from(filters),
232            graph,
233            &self.space_state,
234        )
235        .await;
236
237        top_level_joined_spaces
238    }
239
240    /// Space filters provide access to a custom subset of the space graph that
241    /// can be used in tandem with the [`crate::RoomListService`] to narrow
242    /// down the presented rooms. A [`crate::room_list_service::RoomList`]'s
243    /// [`crate::room_list_service::RoomListDynamicEntriesController`] can take
244    /// a filter, which in this case can be a
245    /// [`crate::room_list_service::filters::new_filter_identifiers`]
246    /// pointing to the space descendants retrieved from the filters.
247    ///
248    /// They are limited to the first 2 levels of the graph, with the first
249    /// level only containing direct descendants while the second holds the rest
250    /// of them recursively.
251    ///
252    /// # Examples
253    ///
254    /// ```no_run
255    /// use futures_util::StreamExt;
256    /// use matrix_sdk::Client;
257    /// use matrix_sdk_ui::{
258    ///     room_list_service::{RoomListService, filters},
259    ///     spaces::SpaceService,
260    /// };
261    /// use ruma::owned_room_id;
262    ///
263    /// # async {
264    /// # let client: Client = todo!();
265    /// let space_service = SpaceService::new(client.clone()).await;
266    /// let room_list_service = RoomListService::new(client.clone()).await?;
267    ///
268    /// // Get the list of filters derived from the space hierarchy.
269    /// let space_filters = space_service.space_filters().await;
270    /// // Pick a filter/space
271    /// let space_filter = space_filters.first().unwrap();
272    ///
273    /// // Create a room list stream and a controller that accepts filters.
274    /// let all_rooms = room_list_service.all_rooms().await?;
275    /// let (_, controller) = all_rooms.entries_with_dynamic_adapters(25);
276    ///
277    /// // Apply an identifiers filter built from the space filter descendants.
278    /// controller.set_filter(Box::new(filters::new_filter_identifiers(
279    ///     space_filter.descendants.clone(),
280    /// )));
281    ///
282    /// # anyhow::Ok(()) };
283    /// ```
284    pub async fn space_filters(&self) -> Vec<SpaceFilter> {
285        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
286
287        Self::update_space_state_if_needed(
288            Vector::from(top_level_joined_spaces),
289            Vector::from(filters.clone()),
290            graph,
291            &self.space_state,
292        )
293        .await;
294
295        filters
296    }
297
298    /// Subscribe to changes or updates to the space filters.
299    pub async fn subscribe_to_space_filters(
300        &self,
301    ) -> (Vector<SpaceFilter>, VectorSubscriberBatchedStream<SpaceFilter>) {
302        self.space_state.lock().await.space_filters.subscribe().into_values_and_batched_stream()
303    }
304
305    /// Returns a flattened list containing all the spaces where the user has
306    /// permission to send `m.space.child` state events.
307    ///
308    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
309    /// recompute graph, nor does it notify subscribers about changes.
310    pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
311        let Some(user_id) = self.client.user_id() else {
312            return vec![];
313        };
314
315        let graph = &self.space_state.lock().await.graph;
316        let rooms = self.client.joined_space_rooms();
317
318        let mut editable_spaces = Vec::new();
319        for room in &rooms {
320            if let Ok(power_levels) = room.power_levels().await
321                && power_levels.user_can_send_state(user_id, StateEventType::SpaceChild)
322            {
323                let room_id = room.room_id();
324                editable_spaces.push(
325                    SpaceRoom::new_from_known(room, graph.children_of(room_id).len() as u64).await,
326                );
327            }
328        }
329
330        editable_spaces
331    }
332
333    /// Returns a `SpaceRoomList` for the given space ID.
334    pub async fn space_room_list(&self, space_id: OwnedRoomId) -> SpaceRoomList {
335        SpaceRoomList::new(self.client.clone(), space_id).await
336    }
337
338    /// Returns all known direct-parents of a given space room ID.
339    pub async fn joined_parents_of_child(&self, child_id: &RoomId) -> Vec<SpaceRoom> {
340        let graph = &self.space_state.lock().await.graph;
341
342        let rooms = graph
343            .parents_of(child_id)
344            .into_iter()
345            .filter_map(|parent_id| self.client.get_room(parent_id));
346
347        join_all(rooms.map(|room| async move {
348            SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64).await
349        }))
350        .await
351    }
352
353    /// Returns the room IDs of all known direct parents of the given child
354    /// space or room.
355    ///
356    /// This is a much cheaper version of [`Self::joined_parents_of_child()`]
357    /// that doesn't build any [`SpaceRoom`] instances, it only reads the
358    /// existing space graph.
359    ///
360    /// The returned IDs are always joined spaces, as that's all the space graph
361    /// includes. Note that an empty result either means that the child is a
362    /// top-level space (which has no direct parents) or the child isn't
363    /// part of the space graph at all.
364    /// See [`Self::top_level_ancestors_of()`] if you need that particular level
365    /// of detail.
366    ///
367    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
368    /// recompute the space graph nor notify subscribers about changes.
369    pub async fn joined_parent_ids_of_child(&self, child_id: &RoomId) -> Vec<OwnedRoomId> {
370        self.space_state
371            .lock()
372            .await
373            .graph
374            .parents_of(child_id)
375            .into_iter()
376            .map(ToOwned::to_owned)
377            .collect()
378    }
379
380    /// Returns the room IDs of the top-level joined space(s) that the given
381    /// child room/space descends from, by walking the space graph upwards.
382    ///
383    /// A room/space can be the child of multiple spaces, so this might return
384    /// multiple top-level spaces (in no order).
385    ///
386    /// A top-level space is its own only ancestor, so a returned set holding
387    /// just `child_id` is a cheap top-level space check.
388    ///
389    /// Returns an empty set if the room isn't part of the graph, which is
390    /// notably the case for a room that was joined too recently for the graph
391    /// to have been rebuilt.
392    ///
393    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
394    /// recompute the space graph nor notify subscribers about changes.
395    pub async fn top_level_ancestors_of(&self, child_id: &RoomId) -> HashSet<OwnedRoomId> {
396        let space_state = self.space_state.lock().await;
397        let graph = &space_state.graph;
398
399        if !graph.has_node(child_id) {
400            return HashSet::new();
401        }
402
403        let mut queue = VecDeque::from([child_id.to_owned()]);
404        let mut visited = HashSet::from([child_id.to_owned()]);
405        let mut roots = HashSet::new();
406        while let Some(current) = queue.pop_front() {
407            let parents = graph.parents_of(&current);
408            if parents.is_empty() {
409                // A node without parents must be a joined space.
410                roots.insert(current);
411                continue;
412            }
413            for parent in parents {
414                if visited.insert(parent.to_owned()) {
415                    queue.push_back(parent.to_owned());
416                }
417            }
418        }
419        roots
420    }
421
422    /// Returns the corresponding `SpaceRoom` for the given room ID, or `None`
423    /// if it isn't known.
424    pub async fn get_space_room(&self, room_id: &RoomId) -> Option<SpaceRoom> {
425        let graph = &self.space_state.lock().await.graph;
426
427        if graph.has_node(room_id)
428            && let Some(room) = self.client.get_room(room_id)
429        {
430            Some(
431                SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64)
432                    .await,
433            )
434        } else {
435            None
436        }
437    }
438
439    pub async fn add_child_to_space(
440        &self,
441        child_id: OwnedRoomId,
442        space_id: OwnedRoomId,
443    ) -> Result<(), Error> {
444        let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
445        let space_room =
446            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
447        let child_room =
448            self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
449        let child_power_levels = child_room
450            .power_levels()
451            .await
452            .map_err(|error| Error::UpdateRelationship(matrix_sdk::Error::from(error)))?;
453
454        // Add the child to the space.
455        let child_route = child_room.route().await.map_err(Error::UpdateRelationship)?;
456        space_room
457            .send_state_event_for_key(&child_id, SpaceChildEventContent::new(child_route))
458            .await
459            .map_err(Error::UpdateRelationship)?;
460
461        // Add the space as parent of the child if allowed.
462        if child_power_levels.user_can_send_state(user_id, StateEventType::SpaceParent) {
463            let parent_route =
464                space_room.route().await.map_err(Error::UpdateInverseRelationship)?;
465            child_room
466                .send_state_event_for_key(&space_id, SpaceParentEventContent::new(parent_route))
467                .await
468                .map_err(Error::UpdateInverseRelationship)?;
469        } else {
470            warn!("The current user doesn't have permission to set the child's parent.");
471        }
472
473        Ok(())
474    }
475
476    pub async fn remove_child_from_space(
477        &self,
478        child_id: OwnedRoomId,
479        space_id: OwnedRoomId,
480    ) -> Result<(), Error> {
481        let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
482        let space_room =
483            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
484
485        if let Ok(Some(_)) =
486            space_room.get_state_event_static_for_key::<SpaceChildEventContent, _>(&child_id).await
487        {
488            // Redacting state is a "weird" thing to do, so send {} instead.
489            // https://github.com/matrix-org/matrix-spec/issues/2252
490            //
491            // Specifically, "The redaction of the state doesn't participate in state
492            // resolution so behaves quite differently from e.g. sending an empty form of
493            // that state events".
494            space_room
495                .send_state_event_raw("m.space.child", child_id.as_str(), serde_json::json!({}))
496                .await
497                .map_err(Error::UpdateRelationship)?;
498        } else {
499            warn!("A space child event wasn't found on the parent, ignoring.");
500        }
501
502        if let Some(child_room) = self.client.get_room(&child_id) {
503            let power_levels = child_room.power_levels().await.map_err(|error| {
504                Error::UpdateInverseRelationship(matrix_sdk::Error::from(error))
505            })?;
506
507            if power_levels.user_can_send_state(user_id, StateEventType::SpaceParent)
508                && let Ok(Some(_)) = child_room
509                    .get_state_event_static_for_key::<SpaceParentEventContent, _>(&space_id)
510                    .await
511            {
512                // Same as the comment above.
513                child_room
514                    .send_state_event_raw(
515                        "m.space.parent",
516                        space_id.as_str(),
517                        serde_json::json!({}),
518                    )
519                    .await
520                    .map_err(Error::UpdateInverseRelationship)?;
521            } else {
522                warn!("A space parent event wasn't found on the child, ignoring.");
523            }
524        } else {
525            warn!("The child room is unknown, skipping m.space.parent removal.");
526        }
527
528        Ok(())
529    }
530
531    /// Start a space leave process returning a [`LeaveSpaceHandle`] from which
532    /// rooms can be retrieved in reversed BFS order starting from the requested
533    /// `space_id` graph node. If the room is unknown then an error will be
534    /// returned.
535    ///
536    /// Once the rooms to be left are chosen the handle can be used to leave
537    /// them.
538    pub async fn leave_space(&self, space_id: &RoomId) -> Result<LeaveSpaceHandle, Error> {
539        let space_state = self.space_state.lock().await;
540
541        if !space_state.graph.has_node(space_id) {
542            return Err(Error::RoomNotFound(space_id.to_owned()));
543        }
544
545        let room_ids = space_state.graph.flattened_bottom_up_subtree(space_id);
546
547        let handle = LeaveSpaceHandle::new(self.client.clone(), room_ids).await;
548
549        Ok(handle)
550    }
551
552    async fn update_space_state_if_needed(
553        new_spaces: Vector<SpaceRoom>,
554        new_filters: Vector<SpaceFilter>,
555        new_graph: SpaceGraph,
556        space_state: &Arc<AsyncMutex<SpaceState>>,
557    ) {
558        let mut space_state = space_state.lock().await;
559
560        if new_spaces != space_state.top_level_joined_spaces.clone() {
561            space_state.top_level_joined_spaces.clear();
562            space_state.top_level_joined_spaces.append(new_spaces);
563        }
564
565        if new_filters != space_state.space_filters.clone() {
566            space_state.space_filters.clear();
567            space_state.space_filters.append(new_filters);
568        }
569
570        space_state.graph = new_graph;
571    }
572
573    async fn build_space_state(client: &Client) -> (Vec<SpaceRoom>, Vec<SpaceFilter>, SpaceGraph) {
574        let joined_spaces = client.joined_space_rooms();
575        let joined_space_ids =
576            joined_spaces.iter().map(|space| space.room_id()).collect::<HashSet<_>>();
577
578        // Build a graph to hold the parent-child relations
579        let mut graph = SpaceGraph::new();
580
581        // And also store `m.space.child` ordering info for later use
582        let mut space_child_states = HashMap::<OwnedRoomId, SpaceRoomChildState>::new();
583
584        // Iterate over all joined spaces and populate the graph with edges based
585        // on `m.space.parent` and `m.space.child` state events.
586        for space in joined_spaces.iter() {
587            graph.add_node(space.room_id().to_owned());
588
589            if let Ok(parents) = space.get_state_events_static::<SpaceParentEventContent>().await {
590                parents.into_iter()
591                .flat_map(|parent_event| match parent_event.deserialize() {
592                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
593                        Some(e.state_key)
594                    }
595                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
596                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
597                    Err(e) => {
598                        trace!(room_id = ?space.room_id(), "Could not deserialize m.space.parent: {e}");
599                        None
600                    }
601                })
602                // Note: this filter, together with the fact that the loop only
603                // ever adds edges out of a joined space, is what guarantees
604                // that the parent end of every edge is a joined space. Both
605                // `joined_parent_ids_of_child` and `top_level_ancestors_of`
606                // rely on it to return joined spaces without re-checking.
607                .filter(|parent| joined_space_ids.contains(&**parent))
608                .for_each(|parent| graph.add_edge(parent, space.room_id().to_owned()));
609            } else {
610                error!(room_id = ?space.room_id(), "Could not get m.space.parent events");
611            }
612
613            if let Ok(children) = space.get_state_events_static::<SpaceChildEventContent>().await {
614                children.into_iter()
615                .filter_map(|child_event| match child_event.deserialize() {
616                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
617                        space_child_states.insert(
618                            e.state_key.to_owned(),
619                            SpaceRoomChildState {
620                                order: e.content.order.clone(),
621                                origin_server_ts: e.origin_server_ts,
622                            },
623                        );
624
625                        Some(e.state_key)
626                    }
627                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
628                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
629                    Err(e) => {
630                        trace!(room_id = ?space.room_id(), "Could not deserialize m.space.child: {e}");
631                        None
632                    }
633                }).for_each(|child| graph.add_edge(space.room_id().to_owned(), child));
634            } else {
635                error!(room_id = ?space.room_id(), "Could not get m.space.child events");
636            }
637        }
638
639        // Remove cycles from the graph. This is important because they are not
640        // enforced backend side.
641        graph.remove_cycles();
642
643        let root_nodes = graph.root_nodes();
644
645        // Proceed with filtering to the top level spaces, sorting them by their
646        // (optional) order field (as defined in MSC3230) and then mapping them
647        // to `SpaceRoom`s.
648        let top_level_space_rooms = joined_spaces
649            .iter()
650            .filter(|room| root_nodes.contains(&room.room_id()))
651            .collect::<Vec<_>>();
652
653        let mut top_level_space_order = HashMap::new();
654        for space in &top_level_space_rooms {
655            if let Ok(Some(raw_event)) =
656                space.account_data_static::<events::space_order::SpaceOrderEventContent>().await
657                && let Ok(event) = raw_event.deserialize()
658            {
659                top_level_space_order.insert(space.room_id().to_owned(), event.content.order);
660            }
661        }
662
663        let top_level_space_rooms = top_level_space_rooms
664            .into_iter()
665            .sorted_by(|a, b| {
666                let a = (a.room_id(), top_level_space_order.get(a.room_id()).map(AsRef::as_ref));
667                let b = (b.room_id(), top_level_space_order.get(b.room_id()).map(AsRef::as_ref));
668
669                compare_top_level_space_rooms(a, b)
670            })
671            .collect::<Vec<_>>();
672
673        let mut top_level_spaces = Vec::new();
674
675        for room in &top_level_space_rooms {
676            top_level_spaces.push(
677                SpaceRoom::new_from_known(room, graph.children_of(room.room_id()).len() as u64)
678                    .await,
679            );
680        }
681
682        let space_filters =
683            Self::build_space_filters(client, &graph, top_level_space_rooms, space_child_states)
684                .await;
685
686        (top_level_spaces, space_filters, graph)
687    }
688
689    /// Build the 2 levels required for space filters.
690    /// As per product requirements, the first level space filters only include
691    /// direct descendants while second level ones contain *all* descendants.
692    ///
693    /// The sorting mechanism is different between first level spaces/filters
694    /// and second level ones so while the former are already sorted at this
695    /// point the latter need to be manually taken care of here though the use
696    /// of the collected `m.space.child` state event details.
697    async fn build_space_filters(
698        client: &Client,
699        graph: &SpaceGraph,
700        top_level_space_rooms: Vec<&Room>,
701        space_child_states: HashMap<OwnedRoomId, SpaceRoomChildState>,
702    ) -> Vec<SpaceFilter> {
703        let mut filters = Vec::new();
704        for top_level_space in top_level_space_rooms {
705            let children = graph
706                .children_of(top_level_space.room_id())
707                .into_iter()
708                .map(|id| id.to_owned())
709                .collect::<Vec<_>>();
710
711            filters.push(SpaceFilter {
712                space_room: SpaceRoom::new_from_known(top_level_space, children.len() as u64).await,
713                level: 0,
714                descendants: children.clone(),
715            });
716
717            let children_rooms = join_all(
718                children
719                    .iter()
720                    .filter_map(|child| client.get_room(child))
721                    .filter(|room| room.is_space())
722                    .map(|room| async move {
723                        SpaceRoom::new_from_known(
724                            &room,
725                            graph.children_of(room.room_id()).len() as u64,
726                        )
727                        .await
728                    }),
729            )
730            .await;
731            filters.append(
732                &mut children_rooms
733                    .into_iter()
734                    .sorted_by(|a, b| {
735                        let a_state = space_child_states.get(&a.room_id).cloned();
736                        let b_state = space_child_states.get(&b.room_id).cloned();
737
738                        SpaceRoom::compare_rooms(
739                            (&a.room_id, a_state.as_ref()),
740                            (&b.room_id, b_state.as_ref()),
741                        )
742                    })
743                    .map(|space_room| {
744                        let descendants = graph.flattened_bottom_up_subtree(&space_room.room_id);
745
746                        SpaceFilter { space_room, level: 1, descendants }
747                    })
748                    .collect::<Vec<_>>(),
749            );
750        }
751
752        filters
753    }
754}
755
756// MSC3230: lexicographically by `order` and then by room ID
757fn compare_top_level_space_rooms(
758    a: (&RoomId, Option<&SpaceChildOrder>),
759    b: (&RoomId, Option<&SpaceChildOrder>),
760) -> Ordering {
761    let (a_room_id, a_order) = a;
762    let (b_room_id, b_order) = b;
763
764    match (a_order, b_order) {
765        (Some(a_order), Some(b_order)) => a_order.cmp(b_order).then(a_room_id.cmp(b_room_id)),
766        (Some(_), None) => Ordering::Less,
767        (None, Some(_)) => Ordering::Greater,
768        (None, None) => a_room_id.cmp(b_room_id),
769    }
770}
771
772#[derive(Debug, Clone, PartialEq)]
773pub struct SpaceFilter {
774    /// The underlying [`SpaceRoom`]
775    pub space_room: SpaceRoom,
776
777    /// The level of the space filter in the tree/hierarchy.
778    /// At this point in time the filters are limited to the first 2 levels.
779    pub level: u8,
780
781    /// The room identifiers of the descendants of this space.
782    /// For top level spaces (level 0) these will be direct descendants while
783    /// for first level spaces they will be all other descendants, recursively.
784    pub descendants: Vec<OwnedRoomId>,
785}
786
787#[cfg(test)]
788mod tests {
789    use std::collections::BTreeMap;
790
791    use assert_matches2::assert_let;
792    use eyeball_im::VectorDiff;
793    use futures_util::{StreamExt, pin_mut};
794    use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
795    use matrix_sdk_test::{
796        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
797    };
798    use proptest::prelude::*;
799    use ruma::{
800        MilliSecondsSinceUnixEpoch, OwnedSpaceChildOrder, RoomVersionId, UserId, event_id,
801        owned_room_id, room_id, serde::Raw,
802    };
803    use serde_json::json;
804    use stream_assert::{assert_next_eq, assert_pending};
805
806    use super::*;
807
808    #[async_test]
809    async fn test_spaces_hierarchy() {
810        let server = MatrixMockServer::new().await;
811        let client = server.client_builder().build().await;
812        let user_id = client.user_id().unwrap();
813        let space_service = SpaceService::new(client.clone()).await;
814        let factory = EventFactory::new();
815
816        server.mock_room_state_encryption().plain().mount().await;
817
818        // Given one parent space with 2 children spaces
819
820        let parent_space_id = room_id!("!parent_space:example.org");
821        let child_space_id_1 = room_id!("!child_space_1:example.org");
822        let child_space_id_2 = room_id!("!child_space_2:example.org");
823
824        add_space_rooms(
825            vec![
826                MockSpaceRoomParameters {
827                    room_id: child_space_id_1,
828                    order: None,
829                    parents: vec![parent_space_id],
830                    children: vec![],
831                    power_level: None,
832                },
833                MockSpaceRoomParameters {
834                    room_id: child_space_id_2,
835                    order: None,
836                    parents: vec![parent_space_id],
837                    children: vec![],
838                    power_level: None,
839                },
840                MockSpaceRoomParameters {
841                    room_id: parent_space_id,
842                    order: None,
843                    parents: vec![],
844                    children: vec![child_space_id_1, child_space_id_2],
845                    power_level: None,
846                },
847            ],
848            &client,
849            &server,
850            &factory,
851            user_id,
852        )
853        .await;
854
855        // Only the parent space is returned
856        assert_eq!(
857            space_service
858                .top_level_joined_spaces()
859                .await
860                .iter()
861                .map(|s| s.room_id.to_owned())
862                .collect::<Vec<_>>(),
863            vec![parent_space_id]
864        );
865
866        // and it has 2 children
867        assert_eq!(
868            space_service
869                .top_level_joined_spaces()
870                .await
871                .iter()
872                .map(|s| s.children_count)
873                .collect::<Vec<_>>(),
874            vec![2]
875        );
876
877        let parent_space = client.get_room(parent_space_id).unwrap();
878        assert!(parent_space.is_space());
879
880        // And the parent space and the two child spaces are linked
881
882        let spaces: Vec<ParentSpace> = client
883            .get_room(child_space_id_1)
884            .unwrap()
885            .parent_spaces()
886            .await
887            .unwrap()
888            .map(Result::unwrap)
889            .collect()
890            .await;
891
892        assert_let!(ParentSpace::Reciprocal(parent) = spaces.first().unwrap());
893        assert_eq!(parent.room_id(), parent_space.room_id());
894
895        let spaces: Vec<ParentSpace> = client
896            .get_room(child_space_id_2)
897            .unwrap()
898            .parent_spaces()
899            .await
900            .unwrap()
901            .map(Result::unwrap)
902            .collect()
903            .await;
904
905        assert_let!(ParentSpace::Reciprocal(parent) = spaces.last().unwrap());
906        assert_eq!(parent.room_id(), parent_space.room_id());
907    }
908
909    #[async_test]
910    async fn test_joined_spaces_updates() {
911        let server = MatrixMockServer::new().await;
912        let client = server.client_builder().build().await;
913        let user_id = client.user_id().unwrap();
914        let factory = EventFactory::new();
915
916        server.mock_room_state_encryption().plain().mount().await;
917
918        let first_space_id = room_id!("!first_space:example.org");
919        let second_space_id = room_id!("!second_space:example.org");
920
921        // Join the first space
922        server
923            .sync_room(
924                &client,
925                JoinedRoomBuilder::new(first_space_id)
926                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type()),
927            )
928            .await;
929
930        // Build the `SpaceService` and expect the room to show up with no updates
931        // pending
932
933        let space_service = SpaceService::new(client.clone()).await;
934
935        let (initial_values, joined_spaces_subscriber) =
936            space_service.subscribe_to_top_level_joined_spaces().await;
937        pin_mut!(joined_spaces_subscriber);
938        assert_pending!(joined_spaces_subscriber);
939
940        assert_eq!(
941            initial_values,
942            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await]
943                .into()
944        );
945
946        assert_eq!(
947            space_service.top_level_joined_spaces().await,
948            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await]
949        );
950
951        // And the stream is still pending as the initial values were
952        // already set.
953        assert_pending!(joined_spaces_subscriber);
954
955        // Join the second space
956
957        server
958            .sync_room(
959                &client,
960                JoinedRoomBuilder::new(second_space_id)
961                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
962                    .add_state_event(
963                        factory
964                            .space_child(
965                                second_space_id.to_owned(),
966                                owned_room_id!("!child:example.org"),
967                            )
968                            .sender(user_id),
969                    ),
970            )
971            .await;
972
973        // And expect the list to update
974        assert_eq!(
975            space_service.top_level_joined_spaces().await,
976            vec![
977                SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await,
978                SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1).await
979            ]
980        );
981
982        assert_next_eq!(
983            joined_spaces_subscriber,
984            vec![
985                VectorDiff::Clear,
986                VectorDiff::Append {
987                    values: vec![
988                        SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)
989                            .await,
990                        SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
991                            .await
992                    ]
993                    .into()
994                },
995            ]
996        );
997
998        server.sync_room(&client, LeftRoomBuilder::new(second_space_id)).await;
999
1000        // and when one is left
1001        assert_next_eq!(
1002            joined_spaces_subscriber,
1003            vec![
1004                VectorDiff::Clear,
1005                VectorDiff::Append {
1006                    values: vec![
1007                        SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)
1008                            .await
1009                    ]
1010                    .into()
1011                },
1012            ]
1013        );
1014
1015        // but it doesn't when a non-space room gets joined
1016        server
1017            .sync_room(
1018                &client,
1019                JoinedRoomBuilder::new(room_id!("!room:example.org"))
1020                    .add_state_event(factory.create(user_id, RoomVersionId::V1)),
1021            )
1022            .await;
1023
1024        // and the subscriber doesn't yield any updates
1025        assert_pending!(joined_spaces_subscriber);
1026        assert_eq!(
1027            space_service.top_level_joined_spaces().await,
1028            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0).await]
1029        );
1030    }
1031
1032    #[async_test]
1033    async fn test_joined_child_space_becomes_top_level_after_leaving_parent() {
1034        let server = MatrixMockServer::new().await;
1035        let client = server.client_builder().build().await;
1036
1037        server.mock_room_state_encryption().plain().mount().await;
1038
1039        let parent_space_id = room_id!("!parent_space:example.org");
1040        let child_space_id = room_id!("!child_space:example.org");
1041        let child_room_id = room_id!("!child_room:example.org");
1042
1043        add_space_rooms(
1044            vec![
1045                MockSpaceRoomParameters {
1046                    room_id: child_space_id,
1047                    order: None,
1048                    parents: vec![parent_space_id],
1049                    children: vec![child_room_id],
1050                    power_level: None,
1051                },
1052                MockSpaceRoomParameters {
1053                    room_id: parent_space_id,
1054                    order: None,
1055                    parents: vec![],
1056                    children: vec![child_space_id],
1057                    power_level: None,
1058                },
1059            ],
1060            &client,
1061            &server,
1062            &EventFactory::new(),
1063            client.user_id().unwrap(),
1064        )
1065        .await;
1066
1067        let space_service = SpaceService::new(client.clone()).await;
1068
1069        let top_level_spaces = space_service.top_level_joined_spaces().await;
1070        assert_eq!(top_level_spaces.len(), 1);
1071        assert_eq!(top_level_spaces[0].room_id, parent_space_id);
1072
1073        server.sync_room(&client, LeftRoomBuilder::new(parent_space_id)).await;
1074
1075        let top_level_spaces = space_service.top_level_joined_spaces().await;
1076        assert_eq!(top_level_spaces.len(), 1);
1077        assert_eq!(top_level_spaces[0].room_id, child_space_id);
1078        assert_eq!(top_level_spaces[0].children_count, 1);
1079
1080        let filters = space_service.space_filters().await;
1081        assert_eq!(filters.len(), 1);
1082        assert_eq!(filters[0].space_room.room_id, child_space_id);
1083        assert_eq!(filters[0].descendants, vec![child_room_id]);
1084    }
1085
1086    #[async_test]
1087    async fn test_space_filters() {
1088        let server = MatrixMockServer::new().await;
1089        let client = server.client_builder().build().await;
1090
1091        server.mock_room_state_encryption().plain().mount().await;
1092
1093        add_space_rooms(
1094            vec![
1095                MockSpaceRoomParameters {
1096                    room_id: room_id!("!1:a.b"),
1097                    order: None,
1098                    parents: vec![],
1099                    children: vec![],
1100                    power_level: None,
1101                },
1102                MockSpaceRoomParameters {
1103                    room_id: room_id!("!1.2:a.b"),
1104                    order: None,
1105                    parents: vec![room_id!("!1:a.b")],
1106                    children: vec![],
1107                    power_level: None,
1108                },
1109                MockSpaceRoomParameters {
1110                    room_id: room_id!("!1.2.3:a.b"),
1111                    order: None,
1112                    parents: vec![room_id!("!1.2:a.b")],
1113                    children: vec![],
1114                    power_level: None,
1115                },
1116                MockSpaceRoomParameters {
1117                    room_id: room_id!("!1.2.3.4:a.b"),
1118                    order: None,
1119                    parents: vec![room_id!("!1.2.3:a.b")],
1120                    children: vec![],
1121                    power_level: None,
1122                },
1123            ],
1124            &client,
1125            &server,
1126            &EventFactory::new(),
1127            client.user_id().unwrap(),
1128        )
1129        .await;
1130
1131        let space_service = SpaceService::new(client.clone()).await;
1132
1133        let filters = space_service.space_filters().await;
1134        assert_eq!(filters.len(), 2);
1135        assert_eq!(filters[0].space_room.room_id, "!1:a.b");
1136        assert_eq!(filters[0].level, 0);
1137        assert_eq!(filters[0].descendants.len(), 1); //
1138        assert_eq!(filters[1].space_room.room_id, "!1.2:a.b");
1139        assert_eq!(filters[1].level, 1);
1140        assert_eq!(filters[1].descendants.len(), 3);
1141
1142        let (initial_values, space_filters_subscriber) =
1143            space_service.subscribe_to_space_filters().await;
1144        pin_mut!(space_filters_subscriber);
1145        assert_pending!(space_filters_subscriber);
1146
1147        assert_eq!(initial_values, filters.into());
1148
1149        add_space_rooms(
1150            vec![MockSpaceRoomParameters {
1151                room_id: room_id!("!1.2.3.4.5:a.b"),
1152                order: None,
1153                parents: vec![room_id!("!1.2.3.4:a.b")],
1154                children: vec![],
1155                power_level: None,
1156            }],
1157            &client,
1158            &server,
1159            &EventFactory::new(),
1160            client.user_id().unwrap(),
1161        )
1162        .await;
1163
1164        space_filters_subscriber.next().await;
1165
1166        let filters = space_service.space_filters().await;
1167        assert_eq!(filters[0].descendants.len(), 1);
1168        assert_eq!(filters[1].descendants.len(), 4);
1169    }
1170
1171    #[async_test]
1172    async fn test_top_level_space_order() {
1173        let server = MatrixMockServer::new().await;
1174        let client = server.client_builder().build().await;
1175
1176        server.mock_room_state_encryption().plain().mount().await;
1177
1178        add_space_rooms(
1179            vec![
1180                MockSpaceRoomParameters {
1181                    room_id: room_id!("!2:a.b"),
1182                    order: Some("2"),
1183                    parents: vec![],
1184                    children: vec![],
1185                    power_level: None,
1186                },
1187                MockSpaceRoomParameters {
1188                    room_id: room_id!("!4:a.b"),
1189                    order: None,
1190                    parents: vec![],
1191                    children: vec![],
1192                    power_level: None,
1193                },
1194                MockSpaceRoomParameters {
1195                    room_id: room_id!("!3:a.b"),
1196                    order: None,
1197                    parents: vec![],
1198                    children: vec![],
1199                    power_level: None,
1200                },
1201                MockSpaceRoomParameters {
1202                    room_id: room_id!("!1:a.b"),
1203                    order: Some("1"),
1204                    parents: vec![],
1205                    children: vec![],
1206                    power_level: None,
1207                },
1208            ],
1209            &client,
1210            &server,
1211            &EventFactory::new(),
1212            client.user_id().unwrap(),
1213        )
1214        .await;
1215
1216        let space_service = SpaceService::new(client.clone()).await;
1217
1218        // Space with an `order` field set should come first in lexicographic
1219        // order and rest sorted by room ID.
1220        assert_eq!(
1221            space_service.top_level_joined_spaces().await,
1222            vec![
1223                SpaceRoom::new_from_known(&client.get_room(room_id!("!1:a.b")).unwrap(), 0).await,
1224                SpaceRoom::new_from_known(&client.get_room(room_id!("!2:a.b")).unwrap(), 0).await,
1225                SpaceRoom::new_from_known(&client.get_room(room_id!("!3:a.b")).unwrap(), 0).await,
1226                SpaceRoom::new_from_known(&client.get_room(room_id!("!4:a.b")).unwrap(), 0).await,
1227            ]
1228        );
1229    }
1230
1231    #[async_test]
1232    async fn test_editable_spaces() {
1233        // Given a space hierarchy where the user is admin of some spaces and subspaces.
1234        let server = MatrixMockServer::new().await;
1235        let client = server.client_builder().build().await;
1236        let user_id = client.user_id().unwrap();
1237        let factory = EventFactory::new();
1238
1239        server.mock_room_state_encryption().plain().mount().await;
1240
1241        let admin_space_id = room_id!("!admin_space:example.org");
1242        let admin_subspace_id = room_id!("!admin_subspace:example.org");
1243        let regular_space_id = room_id!("!regular_space:example.org");
1244        let regular_subspace_id = room_id!("!regular_subspace:example.org");
1245
1246        add_space_rooms(
1247            vec![
1248                MockSpaceRoomParameters {
1249                    room_id: admin_space_id,
1250                    order: None,
1251                    parents: vec![],
1252                    children: vec![regular_subspace_id],
1253                    power_level: Some(100),
1254                },
1255                MockSpaceRoomParameters {
1256                    room_id: admin_subspace_id,
1257                    order: None,
1258                    parents: vec![regular_space_id],
1259                    children: vec![],
1260                    power_level: Some(100),
1261                },
1262                MockSpaceRoomParameters {
1263                    room_id: regular_space_id,
1264                    order: None,
1265                    parents: vec![],
1266                    children: vec![admin_subspace_id],
1267                    power_level: Some(0),
1268                },
1269                MockSpaceRoomParameters {
1270                    room_id: regular_subspace_id,
1271                    order: None,
1272                    parents: vec![admin_space_id],
1273                    children: vec![],
1274                    power_level: Some(0),
1275                },
1276            ],
1277            &client,
1278            &server,
1279            &factory,
1280            user_id,
1281        )
1282        .await;
1283
1284        let space_service = SpaceService::new(client.clone()).await;
1285
1286        // When retrieving all editable joined spaces.
1287        let editable_spaces = space_service.editable_spaces().await;
1288
1289        // Then only the spaces where the user is admin are returned.
1290        assert_eq!(
1291            editable_spaces.iter().map(|room| room.room_id.to_owned()).collect::<Vec<_>>(),
1292            vec![admin_space_id.to_owned(), admin_subspace_id.to_owned()]
1293        );
1294    }
1295
1296    #[async_test]
1297    async fn test_joined_parents_of_child() {
1298        // Given a space with three parent spaces, two of which are joined.
1299        let server = MatrixMockServer::new().await;
1300        let client = server.client_builder().build().await;
1301        let user_id = client.user_id().unwrap();
1302        let factory = EventFactory::new();
1303
1304        server.mock_room_state_encryption().plain().mount().await;
1305
1306        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
1307        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
1308        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
1309        let child_space_id = room_id!("!child_space:example.org");
1310
1311        add_space_rooms(
1312            vec![
1313                MockSpaceRoomParameters {
1314                    room_id: child_space_id,
1315                    order: None,
1316                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
1317                    children: vec![],
1318                    power_level: None,
1319                },
1320                MockSpaceRoomParameters {
1321                    room_id: parent_space_id_1,
1322                    order: None,
1323                    parents: vec![],
1324                    children: vec![child_space_id],
1325                    power_level: None,
1326                },
1327                MockSpaceRoomParameters {
1328                    room_id: parent_space_id_2,
1329                    order: None,
1330                    parents: vec![],
1331                    children: vec![child_space_id],
1332                    power_level: None,
1333                },
1334            ],
1335            &client,
1336            &server,
1337            &factory,
1338            user_id,
1339        )
1340        .await;
1341
1342        let space_service = SpaceService::new(client.clone()).await;
1343
1344        // When retrieving the joined parents of the child space
1345        let parents = space_service.joined_parents_of_child(child_space_id).await;
1346
1347        // Then both parent spaces are returned
1348        assert_eq!(
1349            parents.iter().map(|space| space.room_id.to_owned()).collect::<Vec<_>>(),
1350            vec![parent_space_id_1, parent_space_id_2]
1351        );
1352    }
1353
1354    #[async_test]
1355    async fn test_joined_parent_ids_of_child() {
1356        // Given a space with three parent spaces, two of which are joined,
1357        // and a plain room.
1358        let server = MatrixMockServer::new().await;
1359        let client = server.client_builder().build().await;
1360        let user_id = client.user_id().unwrap();
1361        let factory = EventFactory::new();
1362
1363        server.mock_room_state_encryption().plain().mount().await;
1364
1365        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
1366        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
1367        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
1368        let child_space_id = room_id!("!child_space:example.org");
1369        let child_room_id = room_id!("!child_room:example.org");
1370
1371        add_space_rooms(
1372            vec![
1373                MockSpaceRoomParameters {
1374                    room_id: child_space_id,
1375                    order: None,
1376                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
1377                    children: vec![child_room_id],
1378                    power_level: None,
1379                },
1380                MockSpaceRoomParameters {
1381                    room_id: parent_space_id_1,
1382                    order: None,
1383                    parents: vec![],
1384                    children: vec![child_space_id],
1385                    power_level: None,
1386                },
1387                MockSpaceRoomParameters {
1388                    room_id: parent_space_id_2,
1389                    order: None,
1390                    parents: vec![],
1391                    children: vec![child_space_id],
1392                    power_level: None,
1393                },
1394            ],
1395            &client,
1396            &server,
1397            &factory,
1398            user_id,
1399        )
1400        .await;
1401
1402        let space_service = SpaceService::new(client.clone()).await;
1403
1404        // When retrieving the parent IDs of the child space.
1405        let parent_ids = space_service.joined_parent_ids_of_child(child_space_id).await;
1406
1407        // Then only the two joined parent spaces are returned, ordered by room ID.
1408        // The unjoined one never made it into the graph in the first place, since
1409        // `m.space.parent` events pointing at a room that isn't a joined space are
1410        // dropped while building it.
1411        assert_eq!(parent_ids, vec![parent_space_id_1.to_owned(), parent_space_id_2.to_owned()]);
1412
1413        // And the result matches the one of the more expensive
1414        // `joined_parents_of_child`.
1415        assert_eq!(
1416            space_service
1417                .joined_parents_of_child(child_space_id)
1418                .await
1419                .into_iter()
1420                .map(|space| space.room_id)
1421                .collect::<Vec<_>>(),
1422            parent_ids
1423        );
1424
1425        // And a plain room, which is only known as the child of a joined space,
1426        // still reports its parent.
1427        assert_eq!(
1428            space_service.joined_parent_ids_of_child(child_room_id).await,
1429            vec![child_space_id.to_owned()]
1430        );
1431
1432        // And a top-level space has no parents at all.
1433        assert!(space_service.joined_parent_ids_of_child(parent_space_id_1).await.is_empty());
1434
1435        // And neither does a room the graph doesn't know about.
1436        assert!(
1437            space_service
1438                .joined_parent_ids_of_child(room_id!("!unknown_room:example.org"))
1439                .await
1440                .is_empty()
1441        );
1442    }
1443
1444    #[async_test]
1445    async fn test_top_level_ancestors_of() {
1446        // Given two top-level spaces sharing a subspace, which in turn contains a
1447        // plain room.
1448        let server = MatrixMockServer::new().await;
1449        let client = server.client_builder().build().await;
1450        let user_id = client.user_id().unwrap();
1451        let factory = EventFactory::new();
1452
1453        server.mock_room_state_encryption().plain().mount().await;
1454
1455        let top_level_space_id_1 = room_id!("!top_level_space_1:example.org");
1456        let top_level_space_id_2 = room_id!("!top_level_space_2:example.org");
1457        let middle_space_id = room_id!("!middle_space:example.org");
1458        let leaf_room_id = room_id!("!leaf_room:example.org");
1459
1460        add_space_rooms(
1461            vec![
1462                MockSpaceRoomParameters {
1463                    room_id: top_level_space_id_1,
1464                    order: None,
1465                    parents: vec![],
1466                    children: vec![middle_space_id],
1467                    power_level: None,
1468                },
1469                MockSpaceRoomParameters {
1470                    room_id: top_level_space_id_2,
1471                    order: None,
1472                    parents: vec![],
1473                    children: vec![middle_space_id],
1474                    power_level: None,
1475                },
1476                MockSpaceRoomParameters {
1477                    room_id: middle_space_id,
1478                    order: None,
1479                    parents: vec![top_level_space_id_1, top_level_space_id_2],
1480                    children: vec![leaf_room_id],
1481                    power_level: None,
1482                },
1483            ],
1484            &client,
1485            &server,
1486            &factory,
1487            user_id,
1488        )
1489        .await;
1490
1491        let space_service = SpaceService::new(client.clone()).await;
1492
1493        // Then a room several levels down resolves to both top-level spaces.
1494        assert_eq!(
1495            space_service.top_level_ancestors_of(leaf_room_id).await,
1496            HashSet::from([top_level_space_id_1.to_owned(), top_level_space_id_2.to_owned()])
1497        );
1498
1499        // And so does the subspace they share.
1500        assert_eq!(
1501            space_service.top_level_ancestors_of(middle_space_id).await,
1502            HashSet::from([top_level_space_id_1.to_owned(), top_level_space_id_2.to_owned()])
1503        );
1504
1505        // And a top-level space is its own only ancestor.
1506        assert_eq!(
1507            space_service.top_level_ancestors_of(top_level_space_id_1).await,
1508            HashSet::from([top_level_space_id_1.to_owned()])
1509        );
1510
1511        // And a room the graph doesn't know about has no ancestors, which is how it
1512        // can be told apart from a top-level space.
1513        assert!(
1514            space_service
1515                .top_level_ancestors_of(room_id!("!unknown_room:example.org"))
1516                .await
1517                .is_empty()
1518        );
1519    }
1520
1521    #[async_test]
1522    async fn test_top_level_ancestors_of_cyclic_spaces() {
1523        // Given two spaces that are each other's parent, which the homeserver
1524        // doesn't prevent.
1525        let server = MatrixMockServer::new().await;
1526        let client = server.client_builder().build().await;
1527        let user_id = client.user_id().unwrap();
1528        let factory = EventFactory::new();
1529
1530        server.mock_room_state_encryption().plain().mount().await;
1531
1532        let space_id_1 = room_id!("!cycle_space_1:example.org");
1533        let space_id_2 = room_id!("!cycle_space_2:example.org");
1534
1535        add_space_rooms(
1536            vec![
1537                MockSpaceRoomParameters {
1538                    room_id: space_id_1,
1539                    order: None,
1540                    parents: vec![],
1541                    children: vec![space_id_2],
1542                    power_level: None,
1543                },
1544                MockSpaceRoomParameters {
1545                    room_id: space_id_2,
1546                    order: None,
1547                    parents: vec![],
1548                    children: vec![space_id_1],
1549                    power_level: None,
1550                },
1551            ],
1552            &client,
1553            &server,
1554            &factory,
1555            user_id,
1556        )
1557        .await;
1558
1559        let space_service = SpaceService::new(client.clone()).await;
1560
1561        // Then the walk terminates, on the cycle-free graph the service builds:
1562        // one of the two back edges has been removed, leaving a single root that
1563        // both spaces resolve to. Which of the two it is depends on the order the
1564        // de-cycling happens to visit them in, which isn't part of the contract,
1565        // so it isn't asserted here.
1566        let ancestors_of_1 = space_service.top_level_ancestors_of(space_id_1).await;
1567        let ancestors_of_2 = space_service.top_level_ancestors_of(space_id_2).await;
1568
1569        assert_eq!(ancestors_of_1.len(), 1);
1570        assert_eq!(ancestors_of_1, ancestors_of_2);
1571        let root = ancestors_of_1.iter().next().unwrap();
1572        assert!([space_id_1, space_id_2].contains(&&**root));
1573    }
1574
1575    #[async_test]
1576    async fn test_get_space_room_for_id() {
1577        let server = MatrixMockServer::new().await;
1578        let client = server.client_builder().build().await;
1579        let user_id = client.user_id().unwrap();
1580        let factory = EventFactory::new();
1581
1582        server.mock_room_state_encryption().plain().mount().await;
1583
1584        let space_id = room_id!("!single_space:example.org");
1585
1586        add_space_rooms(
1587            vec![MockSpaceRoomParameters {
1588                room_id: space_id,
1589                order: None,
1590                parents: vec![],
1591                children: vec![],
1592                power_level: None,
1593            }],
1594            &client,
1595            &server,
1596            &factory,
1597            user_id,
1598        )
1599        .await;
1600
1601        let space_service = SpaceService::new(client.clone()).await;
1602
1603        let found = space_service.get_space_room(space_id).await;
1604        assert!(found.is_some());
1605
1606        let expected = SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await;
1607        assert_eq!(found.unwrap(), expected);
1608    }
1609
1610    #[async_test]
1611    async fn test_add_child_to_space() {
1612        // Given a space and child room where the user is admin of both.
1613        let server = MatrixMockServer::new().await;
1614        let client = server.client_builder().build().await;
1615        let user_id = client.user_id().unwrap();
1616        let factory = EventFactory::new();
1617
1618        server.mock_room_state_encryption().plain().mount().await;
1619
1620        let space_child_event_id = event_id!("$1");
1621        let space_parent_event_id = event_id!("$2");
1622        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1623        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1624
1625        let space_id = room_id!("!my_space:example.org");
1626        let child_id = room_id!("!my_child:example.org");
1627
1628        add_space_rooms(
1629            vec![
1630                MockSpaceRoomParameters {
1631                    room_id: space_id,
1632                    order: None,
1633                    parents: vec![],
1634                    children: vec![],
1635                    power_level: Some(100),
1636                },
1637                MockSpaceRoomParameters {
1638                    room_id: child_id,
1639                    order: None,
1640                    parents: vec![],
1641                    children: vec![],
1642                    power_level: Some(100),
1643                },
1644            ],
1645            &client,
1646            &server,
1647            &factory,
1648            user_id,
1649        )
1650        .await;
1651
1652        let space_service = SpaceService::new(client.clone()).await;
1653
1654        // When adding the child to the space.
1655        let result =
1656            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1657
1658        // Then both space child and parent events are set successfully.
1659        assert!(result.is_ok());
1660    }
1661
1662    #[async_test]
1663    async fn test_add_child_to_space_without_space_admin() {
1664        // Given a space and child room where the user is a regular member of both.
1665        let server = MatrixMockServer::new().await;
1666        let client = server.client_builder().build().await;
1667        let user_id = client.user_id().unwrap();
1668        let factory = EventFactory::new();
1669
1670        server.mock_room_state_encryption().plain().mount().await;
1671
1672        server.mock_set_space_child().unauthorized().expect(1).mount().await;
1673        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1674
1675        let space_id = room_id!("!my_space:example.org");
1676        let child_id = room_id!("!my_child:example.org");
1677
1678        add_space_rooms(
1679            vec![
1680                MockSpaceRoomParameters {
1681                    room_id: space_id,
1682                    order: None,
1683                    parents: vec![],
1684                    children: vec![],
1685                    power_level: Some(0),
1686                },
1687                MockSpaceRoomParameters {
1688                    room_id: child_id,
1689                    order: None,
1690                    parents: vec![],
1691                    children: vec![],
1692                    power_level: Some(0),
1693                },
1694            ],
1695            &client,
1696            &server,
1697            &factory,
1698            user_id,
1699        )
1700        .await;
1701
1702        let space_service = SpaceService::new(client.clone()).await;
1703
1704        // When adding the child to the space.
1705        let result =
1706            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1707
1708        // Then the operation fails when trying to set the space child event and the
1709        // parent event is not attempted.
1710        assert!(result.is_err());
1711    }
1712
1713    #[async_test]
1714    async fn test_add_child_to_space_without_child_admin() {
1715        // Given a space and child room where the user is admin of the space but not of
1716        // the child.
1717        let server = MatrixMockServer::new().await;
1718        let client = server.client_builder().build().await;
1719        let user_id = client.user_id().unwrap();
1720        let factory = EventFactory::new();
1721
1722        server.mock_room_state_encryption().plain().mount().await;
1723
1724        let space_child_event_id = event_id!("$1");
1725        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1726        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1727
1728        let space_id = room_id!("!my_space:example.org");
1729        let child_id = room_id!("!my_child:example.org");
1730
1731        add_space_rooms(
1732            vec![
1733                MockSpaceRoomParameters {
1734                    room_id: space_id,
1735                    order: None,
1736                    parents: vec![],
1737                    children: vec![],
1738                    power_level: Some(100),
1739                },
1740                MockSpaceRoomParameters {
1741                    room_id: child_id,
1742                    order: None,
1743                    parents: vec![],
1744                    children: vec![],
1745                    power_level: Some(0),
1746                },
1747            ],
1748            &client,
1749            &server,
1750            &factory,
1751            user_id,
1752        )
1753        .await;
1754
1755        let space_service = SpaceService::new(client.clone()).await;
1756
1757        // When adding the child to the space.
1758        let result =
1759            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1760
1761        error!("result: {:?}", result);
1762        // Then the operation succeeds in setting the space child event and the parent
1763        // event is not attempted.
1764        assert!(result.is_ok());
1765    }
1766
1767    #[async_test]
1768    async fn test_remove_child_from_space() {
1769        // Given a space and child room where the user is admin of both.
1770        let server = MatrixMockServer::new().await;
1771        let client = server.client_builder().build().await;
1772        let user_id = client.user_id().unwrap();
1773        let factory = EventFactory::new();
1774
1775        server.mock_room_state_encryption().plain().mount().await;
1776
1777        let space_child_event_id = event_id!("$1");
1778        let space_parent_event_id = event_id!("$2");
1779        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1780        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1781
1782        let parent_id = room_id!("!parent_space:example.org");
1783        let child_id = room_id!("!child_space:example.org");
1784
1785        add_space_rooms(
1786            vec![
1787                MockSpaceRoomParameters {
1788                    room_id: parent_id,
1789                    order: None,
1790                    parents: vec![],
1791                    children: vec![child_id],
1792                    power_level: None,
1793                },
1794                MockSpaceRoomParameters {
1795                    room_id: child_id,
1796                    order: None,
1797                    parents: vec![parent_id],
1798                    children: vec![],
1799                    power_level: None,
1800                },
1801            ],
1802            &client,
1803            &server,
1804            &factory,
1805            user_id,
1806        )
1807        .await;
1808
1809        let space_service = SpaceService::new(client.clone()).await;
1810
1811        // When removing the child from the space.
1812        let result =
1813            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1814
1815        // Then both space child and parent events are removed successfully.
1816        assert!(result.is_ok());
1817    }
1818
1819    #[async_test]
1820    async fn test_remove_child_from_space_without_parent_event() {
1821        // Given a space with a child where the m.space.parent event wasn't set.
1822        let server = MatrixMockServer::new().await;
1823        let client = server.client_builder().build().await;
1824        let user_id = client.user_id().unwrap();
1825        let factory = EventFactory::new();
1826
1827        server.mock_room_state_encryption().plain().mount().await;
1828
1829        let space_child_event_id = event_id!("$1");
1830        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1831        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1832
1833        let parent_id = room_id!("!parent_space:example.org");
1834        let child_id = room_id!("!child_space:example.org");
1835
1836        add_space_rooms(
1837            vec![
1838                MockSpaceRoomParameters {
1839                    room_id: parent_id,
1840                    order: None,
1841                    parents: vec![],
1842                    children: vec![child_id],
1843                    power_level: None,
1844                },
1845                MockSpaceRoomParameters {
1846                    room_id: child_id,
1847                    order: None,
1848                    parents: vec![],
1849                    children: vec![],
1850                    power_level: None,
1851                },
1852            ],
1853            &client,
1854            &server,
1855            &factory,
1856            user_id,
1857        )
1858        .await;
1859
1860        let space_service = SpaceService::new(client.clone()).await;
1861
1862        // When removing the child from the space.
1863        let result =
1864            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1865
1866        // Then the child event is removed successfully and the parent event removal is
1867        // not attempted.
1868        assert!(result.is_ok());
1869    }
1870
1871    #[async_test]
1872    async fn test_remove_child_from_space_without_child_event() {
1873        // Given a space with a child where the space's m.space.child event wasn't set.
1874        let server = MatrixMockServer::new().await;
1875        let client = server.client_builder().build().await;
1876        let user_id = client.user_id().unwrap();
1877        let factory = EventFactory::new();
1878
1879        server.mock_room_state_encryption().plain().mount().await;
1880
1881        let space_parent_event_id = event_id!("$2");
1882        server.mock_set_space_child().unauthorized().expect(0).mount().await;
1883        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1884
1885        let parent_id = room_id!("!parent_space:example.org");
1886        let child_id = room_id!("!child_space:example.org");
1887
1888        add_space_rooms(
1889            vec![
1890                MockSpaceRoomParameters {
1891                    room_id: parent_id,
1892                    order: None,
1893                    parents: vec![],
1894                    children: vec![],
1895                    power_level: None,
1896                },
1897                MockSpaceRoomParameters {
1898                    room_id: child_id,
1899                    order: None,
1900                    parents: vec![parent_id],
1901                    children: vec![],
1902                    power_level: None,
1903                },
1904            ],
1905            &client,
1906            &server,
1907            &factory,
1908            user_id,
1909        )
1910        .await;
1911
1912        let space_service = SpaceService::new(client.clone()).await;
1913
1914        // When removing the child from the space.
1915        let result =
1916            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1917
1918        // Then the parent event is removed successfully and the child event removal is
1919        // not attempted.
1920        assert!(result.is_ok());
1921    }
1922
1923    #[async_test]
1924    async fn test_remove_unknown_child_from_space() {
1925        // Given a space with a child room that is unknown (not in the client store).
1926        let server = MatrixMockServer::new().await;
1927        let client = server.client_builder().build().await;
1928        let user_id = client.user_id().unwrap();
1929        let factory = EventFactory::new();
1930
1931        server.mock_room_state_encryption().plain().mount().await;
1932
1933        let space_child_event_id = event_id!("$1");
1934        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1935        // The parent event should not be attempted since the child room is unknown.
1936        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1937
1938        let parent_id = room_id!("!parent_space:example.org");
1939        let unknown_child_id = room_id!("!unknown_child:example.org");
1940
1941        // Only add the parent space, not the child room.
1942        add_space_rooms(
1943            vec![MockSpaceRoomParameters {
1944                room_id: parent_id,
1945                order: None,
1946                parents: vec![],
1947                children: vec![unknown_child_id],
1948                power_level: None,
1949            }],
1950            &client,
1951            &server,
1952            &factory,
1953            user_id,
1954        )
1955        .await;
1956
1957        // Verify that the child room is indeed unknown.
1958        assert!(client.get_room(unknown_child_id).is_none());
1959
1960        let space_service = SpaceService::new(client.clone()).await;
1961
1962        // When removing the unknown child from the space.
1963        let result = space_service
1964            .remove_child_from_space(unknown_child_id.to_owned(), parent_id.to_owned())
1965            .await;
1966
1967        // Then the operation succeeds: the child event is removed from the space,
1968        // and the parent event removal is skipped since the child room is unknown.
1969        assert!(result.is_ok());
1970    }
1971
1972    #[async_test]
1973    async fn test_space_child_updates() {
1974        // Test child updates received via sync.
1975        let server = MatrixMockServer::new().await;
1976        let client = server.client_builder().build().await;
1977        let user_id = client.user_id().unwrap();
1978        let factory = EventFactory::new();
1979
1980        server.mock_room_state_encryption().plain().mount().await;
1981
1982        let space_id = room_id!("!space:localhost");
1983        let first_child_id = room_id!("!first_child:localhost");
1984        let second_child_id = room_id!("!second_child:localhost");
1985
1986        // The space is joined.
1987        server
1988            .sync_room(
1989                &client,
1990                JoinedRoomBuilder::new(space_id)
1991                    .add_state_event(factory.create(user_id, RoomVersionId::V11).with_space_type()),
1992            )
1993            .await;
1994
1995        // Build the `SpaceService` and expect the room to show up with no updates
1996        // pending
1997        let space_service = SpaceService::new(client.clone()).await;
1998
1999        let (initial_values, joined_spaces_subscriber) =
2000            space_service.subscribe_to_top_level_joined_spaces().await;
2001        pin_mut!(joined_spaces_subscriber);
2002        assert_pending!(joined_spaces_subscriber);
2003
2004        assert_eq!(
2005            initial_values,
2006            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await].into()
2007        );
2008
2009        assert_eq!(
2010            space_service.top_level_joined_spaces().await,
2011            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await]
2012        );
2013
2014        // Two children are added.
2015        server
2016            .sync_room(
2017                &client,
2018                JoinedRoomBuilder::new(space_id)
2019                    .add_state_event(
2020                        factory
2021                            .space_child(space_id.to_owned(), first_child_id.to_owned())
2022                            .sender(user_id),
2023                    )
2024                    .add_state_event(
2025                        factory
2026                            .space_child(space_id.to_owned(), second_child_id.to_owned())
2027                            .sender(user_id),
2028                    ),
2029            )
2030            .await;
2031
2032        // And expect the list to update.
2033        assert_eq!(
2034            space_service.top_level_joined_spaces().await,
2035            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2).await]
2036        );
2037        assert_next_eq!(
2038            joined_spaces_subscriber,
2039            vec![
2040                VectorDiff::Clear,
2041                VectorDiff::Append {
2042                    values: vec![
2043                        SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2).await
2044                    ]
2045                    .into()
2046                },
2047            ]
2048        );
2049
2050        // Then remove a child by replacing the state event with an empty one.
2051        server
2052            .sync_room(
2053                &client,
2054                JoinedRoomBuilder::new(space_id).add_state_bulk([Raw::new(&json!({
2055                    "content": {},
2056                    "type": "m.space.child",
2057                    "event_id": "$cancelsecondchild",
2058                    "origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
2059                    "sender": user_id,
2060                    "state_key": second_child_id,
2061                }))
2062                .unwrap()
2063                .cast_unchecked()]),
2064            )
2065            .await;
2066
2067        // And expect the list to update.
2068        assert_eq!(
2069            space_service.top_level_joined_spaces().await,
2070            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1).await]
2071        );
2072        assert_next_eq!(
2073            joined_spaces_subscriber,
2074            vec![
2075                VectorDiff::Clear,
2076                VectorDiff::Append {
2077                    values: vec![
2078                        SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1).await
2079                    ]
2080                    .into()
2081                },
2082            ]
2083        );
2084    }
2085
2086    async fn add_space_rooms(
2087        rooms: Vec<MockSpaceRoomParameters>,
2088        client: &Client,
2089        server: &MatrixMockServer,
2090        factory: &EventFactory,
2091        user_id: &UserId,
2092    ) {
2093        for parameters in rooms {
2094            let mut builder = JoinedRoomBuilder::new(parameters.room_id)
2095                .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type());
2096
2097            if let Some(order) = parameters.order {
2098                builder = builder.add_account_data(factory.space_order(order));
2099            }
2100
2101            for parent_id in parameters.parents {
2102                builder = builder.add_state_event(
2103                    factory
2104                        .space_parent(parent_id.to_owned(), parameters.room_id.to_owned())
2105                        .sender(user_id),
2106                );
2107            }
2108
2109            for child_id in parameters.children {
2110                builder = builder.add_state_event(
2111                    factory
2112                        .space_child(parameters.room_id.to_owned(), child_id.to_owned())
2113                        .sender(user_id),
2114                );
2115            }
2116
2117            let mut power_levels = if let Some(power_level) = parameters.power_level {
2118                BTreeMap::from([(user_id.to_owned(), power_level.into())])
2119            } else {
2120                BTreeMap::from([(user_id.to_owned(), 100.into())])
2121            };
2122
2123            builder = builder.add_state_event(
2124                factory.power_levels(&mut power_levels).state_key("").sender(user_id),
2125            );
2126
2127            server.sync_room(client, builder).await;
2128        }
2129    }
2130
2131    struct MockSpaceRoomParameters {
2132        room_id: &'static RoomId,
2133        order: Option<&'static str>,
2134        parents: Vec<&'static RoomId>,
2135        children: Vec<&'static RoomId>,
2136        power_level: Option<i32>,
2137    }
2138
2139    fn any_room_id_and_space_room_order()
2140    -> impl Strategy<Value = (OwnedRoomId, Option<OwnedSpaceChildOrder>)> {
2141        let room_id = "[a-zA-Z]{1,5}".prop_map(|r| {
2142            RoomId::new_v2(&r).expect("Any string starting with ! should be a valid room ID")
2143        });
2144
2145        let order = prop::option::of("[a-zA-Z]{1,5}").prop_map(|order| {
2146            order.map(|o| SpaceChildOrder::parse(o).expect("Any string should be a valid order"))
2147        });
2148
2149        (room_id, order)
2150    }
2151
2152    proptest! {
2153        #[test]
2154        fn sort_top_level_space_room_never_panics(mut v in prop::collection::vec(any_room_id_and_space_room_order(), 0..100)) {
2155            v.sort_by(|a, b| {
2156                let (a_room_id, a_order) = a;
2157                let (b_room_id, b_order) = b;
2158
2159                let a = (a_room_id.as_ref(), a_order.as_deref());
2160                let b = (b_room_id.as_ref(), b_order.as_deref());
2161
2162                compare_top_level_space_rooms(a, b)
2163            })
2164        }
2165
2166        #[test]
2167        fn test_compare_top_level_rooms_reflexive(a in any_room_id_and_space_room_order()) {
2168            let (a_room_id, a_order) = a;
2169            let a = (a_room_id.as_ref(), a_order.as_deref());
2170
2171            prop_assert_eq!(compare_top_level_space_rooms(a, a), Ordering::Equal);
2172        }
2173
2174        #[test]
2175        fn test_compare_top_level_rooms_antisymmetric(a in any_room_id_and_space_room_order(), b in any_room_id_and_space_room_order()) {
2176            let (a_room_id, a_order) = a;
2177            let (b_room_id, b_order) = b;
2178
2179            let a = (a_room_id.as_ref(), a_order.as_deref());
2180            let b = (b_room_id.as_ref(), b_order.as_deref());
2181
2182            let ab = compare_top_level_space_rooms(a, b);
2183            let ba = compare_top_level_space_rooms(b, a);
2184
2185            prop_assert_eq!(ab, ba.reverse());
2186        }
2187
2188        #[test]
2189        fn test_compare_top_level_rooms_transitive(
2190            a in any_room_id_and_space_room_order(),
2191            b in any_room_id_and_space_room_order(),
2192            c in any_room_id_and_space_room_order()
2193        ) {
2194            let (a_room_id, a_order) = a;
2195            let (b_room_id, b_order) = b;
2196            let (c_room_id, c_order) = c;
2197
2198            let a = (a_room_id.as_ref(), a_order.as_deref());
2199            let b = (b_room_id.as_ref(), b_order.as_deref());
2200            let c = (c_room_id.as_ref(), c_order.as_deref());
2201
2202            let ab = compare_top_level_space_rooms(a, b);
2203            let bc = compare_top_level_space_rooms(b, c);
2204            let ac = compare_top_level_space_rooms(a, c);
2205
2206            if ab == Ordering::Less && bc == Ordering::Less {
2207                prop_assert_eq!(ac, Ordering::Less);
2208            }
2209
2210            if ab == Ordering::Equal && bc == Ordering::Equal {
2211                prop_assert_eq!(ac, Ordering::Equal);
2212            }
2213
2214            if ab == Ordering::Greater && bc == Ordering::Greater {
2215                prop_assert_eq!(ac, Ordering::Greater);
2216            }
2217        }
2218    }
2219}