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
198        // values.
199        let (spaces, filters, graph) = Self::build_space_state(&client).await;
200        Self::update_space_state_if_needed(
201            Vector::from(spaces),
202            Vector::from(filters),
203            graph,
204            &space_state,
205        )
206        .await;
207
208        Self { client, space_state, _room_update_handle: AsyncMutex::new(room_update_handle) }
209    }
210
211    /// Subscribes to updates on the joined spaces list. If space rooms are
212    /// joined or left, the stream will yield diffs that reflect the changes.
213    pub async fn subscribe_to_top_level_joined_spaces(
214        &self,
215    ) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
216        self.space_state
217            .lock()
218            .await
219            .top_level_joined_spaces
220            .subscribe()
221            .into_values_and_batched_stream()
222    }
223
224    /// Returns a list of all the top-level joined spaces. It will eagerly
225    /// compute the latest version and also notify subscribers if there were any
226    /// changes.
227    pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
228        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
229
230        Self::update_space_state_if_needed(
231            Vector::from(top_level_joined_spaces.clone()),
232            Vector::from(filters),
233            graph,
234            &self.space_state,
235        )
236        .await;
237
238        top_level_joined_spaces
239    }
240
241    /// Space filters provide access to a custom subset of the space graph that
242    /// can be used in tandem with the [`crate::RoomListService`] to narrow down
243    /// the presented rooms. A [`crate::room_list_service::RoomList`]'s
244    /// [`crate::room_list_service::RoomListDynamicEntriesController`] can take
245    /// a filter, which in this case can be a
246    /// [`crate::room_list_service::filters::new_filter_identifiers`] pointing
247    /// to the space descendants retrieved from the filters.
248    ///
249    /// They are limited to the first 2 levels of the graph, with the first
250    /// level only containing direct descendants while the second holds the rest
251    /// of them recursively.
252    ///
253    /// # Examples
254    ///
255    /// ```no_run
256    /// use futures_util::StreamExt;
257    /// use matrix_sdk::Client;
258    /// use matrix_sdk_ui::{
259    ///     room_list_service::{RoomListService, filters},
260    ///     spaces::SpaceService,
261    /// };
262    /// use ruma::owned_room_id;
263    ///
264    /// # async {
265    /// # let client: Client = todo!();
266    /// let space_service = SpaceService::new(client.clone()).await;
267    /// let room_list_service = RoomListService::new(client.clone()).await?;
268    ///
269    /// // Get the list of filters derived from the space hierarchy.
270    /// let space_filters = space_service.space_filters().await;
271    /// // Pick a filter/space
272    /// let space_filter = space_filters.first().unwrap();
273    ///
274    /// // Create a room list stream and a controller that accepts filters.
275    /// let all_rooms = room_list_service.all_rooms().await?;
276    /// let (_, controller) = all_rooms.entries_with_dynamic_adapters(25);
277    ///
278    /// // Apply an identifiers filter built from the space filter descendants.
279    /// controller.set_filter(Box::new(filters::new_filter_identifiers(
280    ///     space_filter.descendants.clone(),
281    /// )));
282    ///
283    /// # anyhow::Ok(()) };
284    /// ```
285    pub async fn space_filters(&self) -> Vec<SpaceFilter> {
286        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
287
288        Self::update_space_state_if_needed(
289            Vector::from(top_level_joined_spaces),
290            Vector::from(filters.clone()),
291            graph,
292            &self.space_state,
293        )
294        .await;
295
296        filters
297    }
298
299    /// Subscribe to changes or updates to the space filters.
300    pub async fn subscribe_to_space_filters(
301        &self,
302    ) -> (Vector<SpaceFilter>, VectorSubscriberBatchedStream<SpaceFilter>) {
303        self.space_state.lock().await.space_filters.subscribe().into_values_and_batched_stream()
304    }
305
306    /// Returns a flattened list containing all the spaces where the user has
307    /// permission to send `m.space.child` state events.
308    ///
309    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
310    /// recompute graph, nor does it notify subscribers about changes.
311    pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
312        let Some(user_id) = self.client.user_id() else {
313            return vec![];
314        };
315
316        let graph = &self.space_state.lock().await.graph;
317        let rooms = self.client.joined_space_rooms();
318
319        let mut editable_spaces = Vec::new();
320        for room in &rooms {
321            if let Ok(power_levels) = room.power_levels().await
322                && power_levels.user_can_send_state(user_id, StateEventType::SpaceChild)
323            {
324                let room_id = room.room_id();
325                editable_spaces.push(
326                    SpaceRoom::new_from_known(room, graph.children_of(room_id).len() as u64).await,
327                );
328            }
329        }
330
331        editable_spaces
332    }
333
334    /// Returns a `SpaceRoomList` for the given space ID.
335    pub async fn space_room_list(&self, space_id: OwnedRoomId) -> SpaceRoomList {
336        SpaceRoomList::new(self.client.clone(), space_id).await
337    }
338
339    /// Returns all known direct-parents of a given space room ID.
340    pub async fn joined_parents_of_child(&self, child_id: &RoomId) -> Vec<SpaceRoom> {
341        let graph = &self.space_state.lock().await.graph;
342
343        let rooms = graph
344            .parents_of(child_id)
345            .into_iter()
346            .filter_map(|parent_id| self.client.get_room(parent_id));
347
348        join_all(rooms.map(|room| async move {
349            SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64).await
350        }))
351        .await
352    }
353
354    /// Returns the room IDs of all known direct parents of the given child
355    /// space or room.
356    ///
357    /// This is a much cheaper version of [`Self::joined_parents_of_child()`]
358    /// that doesn't build any [`SpaceRoom`] instances, it only reads the
359    /// existing space graph.
360    ///
361    /// The returned IDs are always joined spaces, as that's all the space graph
362    /// includes. Note that an empty result either means that the child is a
363    /// top-level space (which has no direct parents) or the child isn't part of
364    /// the space graph at all. See [`Self::top_level_ancestors_of()`] if you
365    /// need that particular level 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
492            // state resolution so behaves quite differently from e.g. sending
493            // an empty form of 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
585        // based 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. As per product
690    /// requirements, the first level space filters only include direct
691    /// 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. At this point in
778    /// 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. For top level
782    /// spaces (level 0) these will be direct descendants while for first level
783    /// 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 eyeball_im::VectorDiff;
792    use futures_util::{StreamExt, pin_mut};
793    use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
794    use matrix_sdk_test::{
795        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
796    };
797    use proptest::prelude::*;
798    use ruma::{
799        MilliSecondsSinceUnixEpoch, OwnedSpaceChildOrder, RoomVersionId, UserId, event_id,
800        owned_room_id, room_id, serde::Raw,
801    };
802    use serde_json::json;
803    use strass::assert_let;
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
931        // updates 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 already
952        // 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
1234        // subspaces.
1235        let server = MatrixMockServer::new().await;
1236        let client = server.client_builder().build().await;
1237        let user_id = client.user_id().unwrap();
1238        let factory = EventFactory::new();
1239
1240        server.mock_room_state_encryption().plain().mount().await;
1241
1242        let admin_space_id = room_id!("!admin_space:example.org");
1243        let admin_subspace_id = room_id!("!admin_subspace:example.org");
1244        let regular_space_id = room_id!("!regular_space:example.org");
1245        let regular_subspace_id = room_id!("!regular_subspace:example.org");
1246
1247        add_space_rooms(
1248            vec![
1249                MockSpaceRoomParameters {
1250                    room_id: admin_space_id,
1251                    order: None,
1252                    parents: vec![],
1253                    children: vec![regular_subspace_id],
1254                    power_level: Some(100),
1255                },
1256                MockSpaceRoomParameters {
1257                    room_id: admin_subspace_id,
1258                    order: None,
1259                    parents: vec![regular_space_id],
1260                    children: vec![],
1261                    power_level: Some(100),
1262                },
1263                MockSpaceRoomParameters {
1264                    room_id: regular_space_id,
1265                    order: None,
1266                    parents: vec![],
1267                    children: vec![admin_subspace_id],
1268                    power_level: Some(0),
1269                },
1270                MockSpaceRoomParameters {
1271                    room_id: regular_subspace_id,
1272                    order: None,
1273                    parents: vec![admin_space_id],
1274                    children: vec![],
1275                    power_level: Some(0),
1276                },
1277            ],
1278            &client,
1279            &server,
1280            &factory,
1281            user_id,
1282        )
1283        .await;
1284
1285        let space_service = SpaceService::new(client.clone()).await;
1286
1287        // When retrieving all editable joined spaces.
1288        let editable_spaces = space_service.editable_spaces().await;
1289
1290        // Then only the spaces where the user is admin are returned.
1291        assert_eq!(
1292            editable_spaces.iter().map(|room| room.room_id.to_owned()).collect::<Vec<_>>(),
1293            vec![admin_space_id.to_owned(), admin_subspace_id.to_owned()]
1294        );
1295    }
1296
1297    #[async_test]
1298    async fn test_joined_parents_of_child() {
1299        // Given a space with three parent spaces, two of which are joined.
1300        let server = MatrixMockServer::new().await;
1301        let client = server.client_builder().build().await;
1302        let user_id = client.user_id().unwrap();
1303        let factory = EventFactory::new();
1304
1305        server.mock_room_state_encryption().plain().mount().await;
1306
1307        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
1308        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
1309        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
1310        let child_space_id = room_id!("!child_space:example.org");
1311
1312        add_space_rooms(
1313            vec![
1314                MockSpaceRoomParameters {
1315                    room_id: child_space_id,
1316                    order: None,
1317                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
1318                    children: vec![],
1319                    power_level: None,
1320                },
1321                MockSpaceRoomParameters {
1322                    room_id: parent_space_id_1,
1323                    order: None,
1324                    parents: vec![],
1325                    children: vec![child_space_id],
1326                    power_level: None,
1327                },
1328                MockSpaceRoomParameters {
1329                    room_id: parent_space_id_2,
1330                    order: None,
1331                    parents: vec![],
1332                    children: vec![child_space_id],
1333                    power_level: None,
1334                },
1335            ],
1336            &client,
1337            &server,
1338            &factory,
1339            user_id,
1340        )
1341        .await;
1342
1343        let space_service = SpaceService::new(client.clone()).await;
1344
1345        // When retrieving the joined parents of the child space
1346        let parents = space_service.joined_parents_of_child(child_space_id).await;
1347
1348        // Then both parent spaces are returned
1349        assert_eq!(
1350            parents.iter().map(|space| space.room_id.to_owned()).collect::<Vec<_>>(),
1351            vec![parent_space_id_1, parent_space_id_2]
1352        );
1353    }
1354
1355    #[async_test]
1356    async fn test_joined_parent_ids_of_child() {
1357        // Given a space with three parent spaces, two of which are joined, and
1358        // a plain room.
1359        let server = MatrixMockServer::new().await;
1360        let client = server.client_builder().build().await;
1361        let user_id = client.user_id().unwrap();
1362        let factory = EventFactory::new();
1363
1364        server.mock_room_state_encryption().plain().mount().await;
1365
1366        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
1367        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
1368        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
1369        let child_space_id = room_id!("!child_space:example.org");
1370        let child_room_id = room_id!("!child_room:example.org");
1371
1372        add_space_rooms(
1373            vec![
1374                MockSpaceRoomParameters {
1375                    room_id: child_space_id,
1376                    order: None,
1377                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
1378                    children: vec![child_room_id],
1379                    power_level: None,
1380                },
1381                MockSpaceRoomParameters {
1382                    room_id: parent_space_id_1,
1383                    order: None,
1384                    parents: vec![],
1385                    children: vec![child_space_id],
1386                    power_level: None,
1387                },
1388                MockSpaceRoomParameters {
1389                    room_id: parent_space_id_2,
1390                    order: None,
1391                    parents: vec![],
1392                    children: vec![child_space_id],
1393                    power_level: None,
1394                },
1395            ],
1396            &client,
1397            &server,
1398            &factory,
1399            user_id,
1400        )
1401        .await;
1402
1403        let space_service = SpaceService::new(client.clone()).await;
1404
1405        // When retrieving the parent IDs of the child space.
1406        let parent_ids = space_service.joined_parent_ids_of_child(child_space_id).await;
1407
1408        // Then only the two joined parent spaces are returned, ordered by room
1409        // ID. The unjoined one never made it into the graph in the first place,
1410        // since `m.space.parent` events pointing at a room that isn't a joined
1411        // space are dropped while building it.
1412        assert_eq!(parent_ids, vec![parent_space_id_1.to_owned(), parent_space_id_2.to_owned()]);
1413
1414        // And the result matches the one of the more expensive
1415        // `joined_parents_of_child`.
1416        assert_eq!(
1417            space_service
1418                .joined_parents_of_child(child_space_id)
1419                .await
1420                .into_iter()
1421                .map(|space| space.room_id)
1422                .collect::<Vec<_>>(),
1423            parent_ids
1424        );
1425
1426        // And a plain room, which is only known as the child of a joined space,
1427        // still reports its parent.
1428        assert_eq!(
1429            space_service.joined_parent_ids_of_child(child_room_id).await,
1430            vec![child_space_id.to_owned()]
1431        );
1432
1433        // And a top-level space has no parents at all.
1434        assert!(space_service.joined_parent_ids_of_child(parent_space_id_1).await.is_empty());
1435
1436        // And neither does a room the graph doesn't know about.
1437        assert!(
1438            space_service
1439                .joined_parent_ids_of_child(room_id!("!unknown_room:example.org"))
1440                .await
1441                .is_empty()
1442        );
1443    }
1444
1445    #[async_test]
1446    async fn test_top_level_ancestors_of() {
1447        // Given two top-level spaces sharing a subspace, which in turn contains
1448        // a plain room.
1449        let server = MatrixMockServer::new().await;
1450        let client = server.client_builder().build().await;
1451        let user_id = client.user_id().unwrap();
1452        let factory = EventFactory::new();
1453
1454        server.mock_room_state_encryption().plain().mount().await;
1455
1456        let top_level_space_id_1 = room_id!("!top_level_space_1:example.org");
1457        let top_level_space_id_2 = room_id!("!top_level_space_2:example.org");
1458        let middle_space_id = room_id!("!middle_space:example.org");
1459        let leaf_room_id = room_id!("!leaf_room:example.org");
1460
1461        add_space_rooms(
1462            vec![
1463                MockSpaceRoomParameters {
1464                    room_id: top_level_space_id_1,
1465                    order: None,
1466                    parents: vec![],
1467                    children: vec![middle_space_id],
1468                    power_level: None,
1469                },
1470                MockSpaceRoomParameters {
1471                    room_id: top_level_space_id_2,
1472                    order: None,
1473                    parents: vec![],
1474                    children: vec![middle_space_id],
1475                    power_level: None,
1476                },
1477                MockSpaceRoomParameters {
1478                    room_id: middle_space_id,
1479                    order: None,
1480                    parents: vec![top_level_space_id_1, top_level_space_id_2],
1481                    children: vec![leaf_room_id],
1482                    power_level: None,
1483                },
1484            ],
1485            &client,
1486            &server,
1487            &factory,
1488            user_id,
1489        )
1490        .await;
1491
1492        let space_service = SpaceService::new(client.clone()).await;
1493
1494        // Then a room several levels down resolves to both top-level spaces.
1495        assert_eq!(
1496            space_service.top_level_ancestors_of(leaf_room_id).await,
1497            HashSet::from([top_level_space_id_1.to_owned(), top_level_space_id_2.to_owned()])
1498        );
1499
1500        // And so does the subspace they share.
1501        assert_eq!(
1502            space_service.top_level_ancestors_of(middle_space_id).await,
1503            HashSet::from([top_level_space_id_1.to_owned(), top_level_space_id_2.to_owned()])
1504        );
1505
1506        // And a top-level space is its own only ancestor.
1507        assert_eq!(
1508            space_service.top_level_ancestors_of(top_level_space_id_1).await,
1509            HashSet::from([top_level_space_id_1.to_owned()])
1510        );
1511
1512        // And a room the graph doesn't know about has no ancestors, which is
1513        // how it can be told apart from a top-level space.
1514        assert!(
1515            space_service
1516                .top_level_ancestors_of(room_id!("!unknown_room:example.org"))
1517                .await
1518                .is_empty()
1519        );
1520    }
1521
1522    #[async_test]
1523    async fn test_top_level_ancestors_of_cyclic_spaces() {
1524        // Given two spaces that are each other's parent, which the homeserver
1525        // doesn't prevent.
1526        let server = MatrixMockServer::new().await;
1527        let client = server.client_builder().build().await;
1528        let user_id = client.user_id().unwrap();
1529        let factory = EventFactory::new();
1530
1531        server.mock_room_state_encryption().plain().mount().await;
1532
1533        let space_id_1 = room_id!("!cycle_space_1:example.org");
1534        let space_id_2 = room_id!("!cycle_space_2:example.org");
1535
1536        add_space_rooms(
1537            vec![
1538                MockSpaceRoomParameters {
1539                    room_id: space_id_1,
1540                    order: None,
1541                    parents: vec![],
1542                    children: vec![space_id_2],
1543                    power_level: None,
1544                },
1545                MockSpaceRoomParameters {
1546                    room_id: space_id_2,
1547                    order: None,
1548                    parents: vec![],
1549                    children: vec![space_id_1],
1550                    power_level: None,
1551                },
1552            ],
1553            &client,
1554            &server,
1555            &factory,
1556            user_id,
1557        )
1558        .await;
1559
1560        let space_service = SpaceService::new(client.clone()).await;
1561
1562        // Then the walk terminates, on the cycle-free graph the service builds:
1563        // one of the two back edges has been removed, leaving a single root
1564        // that both spaces resolve to. Which of the two it is depends on the
1565        // order the de-cycling happens to visit them in, which isn't part of
1566        // the contract, so it isn't asserted here.
1567        let ancestors_of_1 = space_service.top_level_ancestors_of(space_id_1).await;
1568        let ancestors_of_2 = space_service.top_level_ancestors_of(space_id_2).await;
1569
1570        assert_eq!(ancestors_of_1.len(), 1);
1571        assert_eq!(ancestors_of_1, ancestors_of_2);
1572        let root = ancestors_of_1.iter().next().unwrap();
1573        assert!([space_id_1, space_id_2].contains(&&**root));
1574    }
1575
1576    #[async_test]
1577    async fn test_get_space_room_for_id() {
1578        let server = MatrixMockServer::new().await;
1579        let client = server.client_builder().build().await;
1580        let user_id = client.user_id().unwrap();
1581        let factory = EventFactory::new();
1582
1583        server.mock_room_state_encryption().plain().mount().await;
1584
1585        let space_id = room_id!("!single_space:example.org");
1586
1587        add_space_rooms(
1588            vec![MockSpaceRoomParameters {
1589                room_id: space_id,
1590                order: None,
1591                parents: vec![],
1592                children: vec![],
1593                power_level: None,
1594            }],
1595            &client,
1596            &server,
1597            &factory,
1598            user_id,
1599        )
1600        .await;
1601
1602        let space_service = SpaceService::new(client.clone()).await;
1603
1604        let found = space_service.get_space_room(space_id).await;
1605        assert!(found.is_some());
1606
1607        let expected = SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await;
1608        assert_eq!(found.unwrap(), expected);
1609    }
1610
1611    #[async_test]
1612    async fn test_add_child_to_space() {
1613        // Given a space and child room where the user is admin of both.
1614        let server = MatrixMockServer::new().await;
1615        let client = server.client_builder().build().await;
1616        let user_id = client.user_id().unwrap();
1617        let factory = EventFactory::new();
1618
1619        server.mock_room_state_encryption().plain().mount().await;
1620
1621        let space_child_event_id = event_id!("$1");
1622        let space_parent_event_id = event_id!("$2");
1623        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1624        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1625
1626        let space_id = room_id!("!my_space:example.org");
1627        let child_id = room_id!("!my_child:example.org");
1628
1629        add_space_rooms(
1630            vec![
1631                MockSpaceRoomParameters {
1632                    room_id: space_id,
1633                    order: None,
1634                    parents: vec![],
1635                    children: vec![],
1636                    power_level: Some(100),
1637                },
1638                MockSpaceRoomParameters {
1639                    room_id: child_id,
1640                    order: None,
1641                    parents: vec![],
1642                    children: vec![],
1643                    power_level: Some(100),
1644                },
1645            ],
1646            &client,
1647            &server,
1648            &factory,
1649            user_id,
1650        )
1651        .await;
1652
1653        let space_service = SpaceService::new(client.clone()).await;
1654
1655        // When adding the child to the space.
1656        let result =
1657            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1658
1659        // Then both space child and parent events are set successfully.
1660        assert!(result.is_ok());
1661    }
1662
1663    #[async_test]
1664    async fn test_add_child_to_space_without_space_admin() {
1665        // Given a space and child room where the user is a regular member of
1666        // both.
1667        let server = MatrixMockServer::new().await;
1668        let client = server.client_builder().build().await;
1669        let user_id = client.user_id().unwrap();
1670        let factory = EventFactory::new();
1671
1672        server.mock_room_state_encryption().plain().mount().await;
1673
1674        server.mock_set_space_child().unauthorized().expect(1).mount().await;
1675        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1676
1677        let space_id = room_id!("!my_space:example.org");
1678        let child_id = room_id!("!my_child:example.org");
1679
1680        add_space_rooms(
1681            vec![
1682                MockSpaceRoomParameters {
1683                    room_id: space_id,
1684                    order: None,
1685                    parents: vec![],
1686                    children: vec![],
1687                    power_level: Some(0),
1688                },
1689                MockSpaceRoomParameters {
1690                    room_id: child_id,
1691                    order: None,
1692                    parents: vec![],
1693                    children: vec![],
1694                    power_level: Some(0),
1695                },
1696            ],
1697            &client,
1698            &server,
1699            &factory,
1700            user_id,
1701        )
1702        .await;
1703
1704        let space_service = SpaceService::new(client.clone()).await;
1705
1706        // When adding the child to the space.
1707        let result =
1708            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1709
1710        // Then the operation fails when trying to set the space child event and
1711        // the parent event is not attempted.
1712        assert!(result.is_err());
1713    }
1714
1715    #[async_test]
1716    async fn test_add_child_to_space_without_child_admin() {
1717        // Given a space and child room where the user is admin of the space but
1718        // not of the child.
1719        let server = MatrixMockServer::new().await;
1720        let client = server.client_builder().build().await;
1721        let user_id = client.user_id().unwrap();
1722        let factory = EventFactory::new();
1723
1724        server.mock_room_state_encryption().plain().mount().await;
1725
1726        let space_child_event_id = event_id!("$1");
1727        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1728        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1729
1730        let space_id = room_id!("!my_space:example.org");
1731        let child_id = room_id!("!my_child:example.org");
1732
1733        add_space_rooms(
1734            vec![
1735                MockSpaceRoomParameters {
1736                    room_id: space_id,
1737                    order: None,
1738                    parents: vec![],
1739                    children: vec![],
1740                    power_level: Some(100),
1741                },
1742                MockSpaceRoomParameters {
1743                    room_id: child_id,
1744                    order: None,
1745                    parents: vec![],
1746                    children: vec![],
1747                    power_level: Some(0),
1748                },
1749            ],
1750            &client,
1751            &server,
1752            &factory,
1753            user_id,
1754        )
1755        .await;
1756
1757        let space_service = SpaceService::new(client.clone()).await;
1758
1759        // When adding the child to the space.
1760        let result =
1761            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1762
1763        error!("result: {:?}", result);
1764        // Then the operation succeeds in setting the space child event and the
1765        // parent event is not attempted.
1766        assert!(result.is_ok());
1767    }
1768
1769    #[async_test]
1770    async fn test_remove_child_from_space() {
1771        // Given a space and child room where the user is admin of both.
1772        let server = MatrixMockServer::new().await;
1773        let client = server.client_builder().build().await;
1774        let user_id = client.user_id().unwrap();
1775        let factory = EventFactory::new();
1776
1777        server.mock_room_state_encryption().plain().mount().await;
1778
1779        let space_child_event_id = event_id!("$1");
1780        let space_parent_event_id = event_id!("$2");
1781        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1782        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1783
1784        let parent_id = room_id!("!parent_space:example.org");
1785        let child_id = room_id!("!child_space:example.org");
1786
1787        add_space_rooms(
1788            vec![
1789                MockSpaceRoomParameters {
1790                    room_id: parent_id,
1791                    order: None,
1792                    parents: vec![],
1793                    children: vec![child_id],
1794                    power_level: None,
1795                },
1796                MockSpaceRoomParameters {
1797                    room_id: child_id,
1798                    order: None,
1799                    parents: vec![parent_id],
1800                    children: vec![],
1801                    power_level: None,
1802                },
1803            ],
1804            &client,
1805            &server,
1806            &factory,
1807            user_id,
1808        )
1809        .await;
1810
1811        let space_service = SpaceService::new(client.clone()).await;
1812
1813        // When removing the child from the space.
1814        let result =
1815            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1816
1817        // Then both space child and parent events are removed successfully.
1818        assert!(result.is_ok());
1819    }
1820
1821    #[async_test]
1822    async fn test_remove_child_from_space_without_parent_event() {
1823        // Given a space with a child where the m.space.parent event wasn't set.
1824        let server = MatrixMockServer::new().await;
1825        let client = server.client_builder().build().await;
1826        let user_id = client.user_id().unwrap();
1827        let factory = EventFactory::new();
1828
1829        server.mock_room_state_encryption().plain().mount().await;
1830
1831        let space_child_event_id = event_id!("$1");
1832        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1833        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1834
1835        let parent_id = room_id!("!parent_space:example.org");
1836        let child_id = room_id!("!child_space:example.org");
1837
1838        add_space_rooms(
1839            vec![
1840                MockSpaceRoomParameters {
1841                    room_id: parent_id,
1842                    order: None,
1843                    parents: vec![],
1844                    children: vec![child_id],
1845                    power_level: None,
1846                },
1847                MockSpaceRoomParameters {
1848                    room_id: child_id,
1849                    order: None,
1850                    parents: vec![],
1851                    children: vec![],
1852                    power_level: None,
1853                },
1854            ],
1855            &client,
1856            &server,
1857            &factory,
1858            user_id,
1859        )
1860        .await;
1861
1862        let space_service = SpaceService::new(client.clone()).await;
1863
1864        // When removing the child from the space.
1865        let result =
1866            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1867
1868        // Then the child event is removed successfully and the parent event
1869        // removal is not attempted.
1870        assert!(result.is_ok());
1871    }
1872
1873    #[async_test]
1874    async fn test_remove_child_from_space_without_child_event() {
1875        // Given a space with a child where the space's m.space.child event
1876        // wasn't set.
1877        let server = MatrixMockServer::new().await;
1878        let client = server.client_builder().build().await;
1879        let user_id = client.user_id().unwrap();
1880        let factory = EventFactory::new();
1881
1882        server.mock_room_state_encryption().plain().mount().await;
1883
1884        let space_parent_event_id = event_id!("$2");
1885        server.mock_set_space_child().unauthorized().expect(0).mount().await;
1886        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1887
1888        let parent_id = room_id!("!parent_space:example.org");
1889        let child_id = room_id!("!child_space:example.org");
1890
1891        add_space_rooms(
1892            vec![
1893                MockSpaceRoomParameters {
1894                    room_id: parent_id,
1895                    order: None,
1896                    parents: vec![],
1897                    children: vec![],
1898                    power_level: None,
1899                },
1900                MockSpaceRoomParameters {
1901                    room_id: child_id,
1902                    order: None,
1903                    parents: vec![parent_id],
1904                    children: vec![],
1905                    power_level: None,
1906                },
1907            ],
1908            &client,
1909            &server,
1910            &factory,
1911            user_id,
1912        )
1913        .await;
1914
1915        let space_service = SpaceService::new(client.clone()).await;
1916
1917        // When removing the child from the space.
1918        let result =
1919            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1920
1921        // Then the parent event is removed successfully and the child event
1922        // removal is not attempted.
1923        assert!(result.is_ok());
1924    }
1925
1926    #[async_test]
1927    async fn test_remove_unknown_child_from_space() {
1928        // Given a space with a child room that is unknown (not in the client
1929        // store).
1930        let server = MatrixMockServer::new().await;
1931        let client = server.client_builder().build().await;
1932        let user_id = client.user_id().unwrap();
1933        let factory = EventFactory::new();
1934
1935        server.mock_room_state_encryption().plain().mount().await;
1936
1937        let space_child_event_id = event_id!("$1");
1938        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1939        // The parent event should not be attempted since the child room is
1940        // unknown.
1941        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1942
1943        let parent_id = room_id!("!parent_space:example.org");
1944        let unknown_child_id = room_id!("!unknown_child:example.org");
1945
1946        // Only add the parent space, not the child room.
1947        add_space_rooms(
1948            vec![MockSpaceRoomParameters {
1949                room_id: parent_id,
1950                order: None,
1951                parents: vec![],
1952                children: vec![unknown_child_id],
1953                power_level: None,
1954            }],
1955            &client,
1956            &server,
1957            &factory,
1958            user_id,
1959        )
1960        .await;
1961
1962        // Verify that the child room is indeed unknown.
1963        assert!(client.get_room(unknown_child_id).is_none());
1964
1965        let space_service = SpaceService::new(client.clone()).await;
1966
1967        // When removing the unknown child from the space.
1968        let result = space_service
1969            .remove_child_from_space(unknown_child_id.to_owned(), parent_id.to_owned())
1970            .await;
1971
1972        // Then the operation succeeds: the child event is removed from the
1973        // space, and the parent event removal is skipped since the child room
1974        // is unknown.
1975        assert!(result.is_ok());
1976    }
1977
1978    #[async_test]
1979    async fn test_space_child_updates() {
1980        // Test child updates received via sync.
1981        let server = MatrixMockServer::new().await;
1982        let client = server.client_builder().build().await;
1983        let user_id = client.user_id().unwrap();
1984        let factory = EventFactory::new();
1985
1986        server.mock_room_state_encryption().plain().mount().await;
1987
1988        let space_id = room_id!("!space:localhost");
1989        let first_child_id = room_id!("!first_child:localhost");
1990        let second_child_id = room_id!("!second_child:localhost");
1991
1992        // The space is joined.
1993        server
1994            .sync_room(
1995                &client,
1996                JoinedRoomBuilder::new(space_id)
1997                    .add_state_event(factory.create(user_id, RoomVersionId::V11).with_space_type()),
1998            )
1999            .await;
2000
2001        // Build the `SpaceService` and expect the room to show up with no
2002        // updates pending
2003        let space_service = SpaceService::new(client.clone()).await;
2004
2005        let (initial_values, joined_spaces_subscriber) =
2006            space_service.subscribe_to_top_level_joined_spaces().await;
2007        pin_mut!(joined_spaces_subscriber);
2008        assert_pending!(joined_spaces_subscriber);
2009
2010        assert_eq!(
2011            initial_values,
2012            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await].into()
2013        );
2014
2015        assert_eq!(
2016            space_service.top_level_joined_spaces().await,
2017            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0).await]
2018        );
2019
2020        // Two children are added.
2021        server
2022            .sync_room(
2023                &client,
2024                JoinedRoomBuilder::new(space_id)
2025                    .add_state_event(
2026                        factory
2027                            .space_child(space_id.to_owned(), first_child_id.to_owned())
2028                            .sender(user_id),
2029                    )
2030                    .add_state_event(
2031                        factory
2032                            .space_child(space_id.to_owned(), second_child_id.to_owned())
2033                            .sender(user_id),
2034                    ),
2035            )
2036            .await;
2037
2038        // And expect the list to update.
2039        assert_eq!(
2040            space_service.top_level_joined_spaces().await,
2041            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2).await]
2042        );
2043        assert_next_eq!(
2044            joined_spaces_subscriber,
2045            vec![
2046                VectorDiff::Clear,
2047                VectorDiff::Append {
2048                    values: vec![
2049                        SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2).await
2050                    ]
2051                    .into()
2052                },
2053            ]
2054        );
2055
2056        // Then remove a child by replacing the state event with an empty one.
2057        server
2058            .sync_room(
2059                &client,
2060                JoinedRoomBuilder::new(space_id).add_state_bulk([Raw::new(&json!({
2061                    "content": {},
2062                    "type": "m.space.child",
2063                    "event_id": "$cancelsecondchild",
2064                    "origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
2065                    "sender": user_id,
2066                    "state_key": second_child_id,
2067                }))
2068                .unwrap()
2069                .cast_unchecked()]),
2070            )
2071            .await;
2072
2073        // And expect the list to update.
2074        assert_eq!(
2075            space_service.top_level_joined_spaces().await,
2076            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1).await]
2077        );
2078        assert_next_eq!(
2079            joined_spaces_subscriber,
2080            vec![
2081                VectorDiff::Clear,
2082                VectorDiff::Append {
2083                    values: vec![
2084                        SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1).await
2085                    ]
2086                    .into()
2087                },
2088            ]
2089        );
2090    }
2091
2092    async fn add_space_rooms(
2093        rooms: Vec<MockSpaceRoomParameters>,
2094        client: &Client,
2095        server: &MatrixMockServer,
2096        factory: &EventFactory,
2097        user_id: &UserId,
2098    ) {
2099        for parameters in rooms {
2100            let mut builder = JoinedRoomBuilder::new(parameters.room_id)
2101                .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type());
2102
2103            if let Some(order) = parameters.order {
2104                builder = builder.add_account_data(factory.space_order(order));
2105            }
2106
2107            for parent_id in parameters.parents {
2108                builder = builder.add_state_event(
2109                    factory
2110                        .space_parent(parent_id.to_owned(), parameters.room_id.to_owned())
2111                        .sender(user_id),
2112                );
2113            }
2114
2115            for child_id in parameters.children {
2116                builder = builder.add_state_event(
2117                    factory
2118                        .space_child(parameters.room_id.to_owned(), child_id.to_owned())
2119                        .sender(user_id),
2120                );
2121            }
2122
2123            let mut power_levels = if let Some(power_level) = parameters.power_level {
2124                BTreeMap::from([(user_id.to_owned(), power_level.into())])
2125            } else {
2126                BTreeMap::from([(user_id.to_owned(), 100.into())])
2127            };
2128
2129            builder = builder.add_state_event(
2130                factory.power_levels(&mut power_levels).state_key("").sender(user_id),
2131            );
2132
2133            server.sync_room(client, builder).await;
2134        }
2135    }
2136
2137    struct MockSpaceRoomParameters {
2138        room_id: &'static RoomId,
2139        order: Option<&'static str>,
2140        parents: Vec<&'static RoomId>,
2141        children: Vec<&'static RoomId>,
2142        power_level: Option<i32>,
2143    }
2144
2145    fn any_room_id_and_space_room_order()
2146    -> impl Strategy<Value = (OwnedRoomId, Option<OwnedSpaceChildOrder>)> {
2147        let room_id = "[a-zA-Z]{1,5}".prop_map(|r| {
2148            RoomId::new_v2(&r).expect("Any string starting with ! should be a valid room ID")
2149        });
2150
2151        let order = prop::option::of("[a-zA-Z]{1,5}").prop_map(|order| {
2152            order.map(|o| SpaceChildOrder::parse(o).expect("Any string should be a valid order"))
2153        });
2154
2155        (room_id, order)
2156    }
2157
2158    proptest! {
2159        #[test]
2160        fn sort_top_level_space_room_never_panics(mut v in prop::collection::vec(any_room_id_and_space_room_order(), 0..100)) {
2161            v.sort_by(|a, b| {
2162                let (a_room_id, a_order) = a;
2163                let (b_room_id, b_order) = b;
2164
2165                let a = (a_room_id.as_ref(), a_order.as_deref());
2166                let b = (b_room_id.as_ref(), b_order.as_deref());
2167
2168                compare_top_level_space_rooms(a, b)
2169            })
2170        }
2171
2172        #[test]
2173        fn test_compare_top_level_rooms_reflexive(a in any_room_id_and_space_room_order()) {
2174            let (a_room_id, a_order) = a;
2175            let a = (a_room_id.as_ref(), a_order.as_deref());
2176
2177            prop_assert_eq!(compare_top_level_space_rooms(a, a), Ordering::Equal);
2178        }
2179
2180        #[test]
2181        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()) {
2182            let (a_room_id, a_order) = a;
2183            let (b_room_id, b_order) = b;
2184
2185            let a = (a_room_id.as_ref(), a_order.as_deref());
2186            let b = (b_room_id.as_ref(), b_order.as_deref());
2187
2188            let ab = compare_top_level_space_rooms(a, b);
2189            let ba = compare_top_level_space_rooms(b, a);
2190
2191            prop_assert_eq!(ab, ba.reverse());
2192        }
2193
2194        #[test]
2195        fn test_compare_top_level_rooms_transitive(
2196            a in any_room_id_and_space_room_order(),
2197            b in any_room_id_and_space_room_order(),
2198            c in any_room_id_and_space_room_order()
2199        ) {
2200            let (a_room_id, a_order) = a;
2201            let (b_room_id, b_order) = b;
2202            let (c_room_id, c_order) = c;
2203
2204            let a = (a_room_id.as_ref(), a_order.as_deref());
2205            let b = (b_room_id.as_ref(), b_order.as_deref());
2206            let c = (c_room_id.as_ref(), c_order.as_deref());
2207
2208            let ab = compare_top_level_space_rooms(a, b);
2209            let bc = compare_top_level_space_rooms(b, c);
2210            let ac = compare_top_level_space_rooms(a, c);
2211
2212            if ab == Ordering::Less && bc == Ordering::Less {
2213                prop_assert_eq!(ac, Ordering::Less);
2214            }
2215
2216            if ab == Ordering::Equal && bc == Ordering::Equal {
2217                prop_assert_eq!(ac, Ordering::Equal);
2218            }
2219
2220            if ab == Ordering::Greater && bc == Ordering::Greater {
2221                prop_assert_eq!(ac, Ordering::Greater);
2222            }
2223        }
2224    }
2225}