ruma_common/
room.rs

1//! Common types for rooms.
2
3use std::{borrow::Cow, collections::BTreeMap};
4
5use as_variant::as_variant;
6use js_int::UInt;
7use serde::{de, Deserialize, Serialize};
8use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
9
10use crate::{
11    serde::{from_raw_json_value, StringEnum},
12    EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
13    RoomVersionId,
14};
15
16/// An enum of possible room types.
17#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
18#[derive(Clone, PartialEq, Eq, StringEnum)]
19#[non_exhaustive]
20pub enum RoomType {
21    /// Defines the room as a space.
22    #[ruma_enum(rename = "m.space")]
23    Space,
24
25    /// Defines the room as a custom type.
26    #[doc(hidden)]
27    _Custom(PrivOwnedStr),
28}
29
30/// The rule used for users wishing to join this room.
31///
32/// This type can hold an arbitrary string. To check for values that are not available as a
33/// documented variant here, use its string representation, obtained through `.as_str()`.
34#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
35#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
36#[serde(tag = "join_rule", rename_all = "snake_case")]
37pub enum JoinRule {
38    /// A user who wishes to join the room must first receive an invite to the room from someone
39    /// already inside of the room.
40    Invite,
41
42    /// Users can join the room if they are invited, or they can request an invite to the room.
43    ///
44    /// They can be allowed (invited) or denied (kicked/banned) access.
45    Knock,
46
47    /// Reserved but not yet implemented by the Matrix specification.
48    Private,
49
50    /// Users can join the room if they are invited, or if they meet any of the conditions
51    /// described in a set of [`AllowRule`]s.
52    Restricted(Restricted),
53
54    /// Users can join the room if they are invited, or if they meet any of the conditions
55    /// described in a set of [`AllowRule`]s, or they can request an invite to the room.
56    KnockRestricted(Restricted),
57
58    /// Anyone can join the room without any prior action.
59    Public,
60
61    #[doc(hidden)]
62    #[serde(skip_serializing)]
63    _Custom(PrivOwnedStr),
64}
65
66impl JoinRule {
67    /// Returns the kind of this `JoinRule`.
68    pub fn kind(&self) -> JoinRuleKind {
69        match self {
70            Self::Invite => JoinRuleKind::Invite,
71            Self::Knock => JoinRuleKind::Knock,
72            Self::Private => JoinRuleKind::Private,
73            Self::Restricted(_) => JoinRuleKind::Restricted,
74            Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted,
75            Self::Public => JoinRuleKind::Public,
76            Self::_Custom(rule) => JoinRuleKind::_Custom(rule.clone()),
77        }
78    }
79
80    /// Returns the string name of this `JoinRule`
81    pub fn as_str(&self) -> &str {
82        match self {
83            JoinRule::Invite => "invite",
84            JoinRule::Knock => "knock",
85            JoinRule::Private => "private",
86            JoinRule::Restricted(_) => "restricted",
87            JoinRule::KnockRestricted(_) => "knock_restricted",
88            JoinRule::Public => "public",
89            JoinRule::_Custom(rule) => &rule.0,
90        }
91    }
92}
93
94impl<'de> Deserialize<'de> for JoinRule {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: de::Deserializer<'de>,
98    {
99        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
100
101        #[derive(Deserialize)]
102        struct ExtractType<'a> {
103            #[serde(borrow)]
104            join_rule: Option<Cow<'a, str>>,
105        }
106
107        let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
108            .map_err(de::Error::custom)?
109            .join_rule
110            .ok_or_else(|| de::Error::missing_field("join_rule"))?;
111
112        match join_rule.as_ref() {
113            "invite" => Ok(Self::Invite),
114            "knock" => Ok(Self::Knock),
115            "private" => Ok(Self::Private),
116            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
117            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
118            "public" => Ok(Self::Public),
119            _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
120        }
121    }
122}
123
124/// Configuration of the `Restricted` join rule.
125#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
126#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
127pub struct Restricted {
128    /// Allow rules which describe conditions that allow joining a room.
129    #[serde(default, deserialize_with = "crate::serde::ignore_invalid_vec_items")]
130    pub allow: Vec<AllowRule>,
131}
132
133impl Restricted {
134    /// Constructs a new rule set for restricted rooms with the given rules.
135    pub fn new(allow: Vec<AllowRule>) -> Self {
136        Self { allow }
137    }
138}
139
140/// An allow rule which defines a condition that allows joining a room.
141#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
142#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
143#[serde(untagged)]
144pub enum AllowRule {
145    /// Joining is allowed if a user is already a member of the room with the id `room_id`.
146    RoomMembership(RoomMembership),
147
148    #[doc(hidden)]
149    _Custom(Box<CustomAllowRule>),
150}
151
152impl AllowRule {
153    /// Constructs an `AllowRule` with membership of the room with the given id as its predicate.
154    pub fn room_membership(room_id: OwnedRoomId) -> Self {
155        Self::RoomMembership(RoomMembership::new(room_id))
156    }
157}
158
159/// Allow rule which grants permission to join based on the membership of another room.
160#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
161#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
162#[serde(tag = "type", rename = "m.room_membership")]
163pub struct RoomMembership {
164    /// The id of the room which being a member of grants permission to join another room.
165    pub room_id: OwnedRoomId,
166}
167
168impl RoomMembership {
169    /// Constructs a new room membership rule for the given room id.
170    pub fn new(room_id: OwnedRoomId) -> Self {
171        Self { room_id }
172    }
173}
174
175#[doc(hidden)]
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
177#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
178pub struct CustomAllowRule {
179    #[serde(rename = "type")]
180    rule_type: String,
181    #[serde(flatten)]
182    extra: BTreeMap<String, JsonValue>,
183}
184
185impl<'de> Deserialize<'de> for AllowRule {
186    fn deserialize<D>(deserializer: D) -> Result<AllowRule, D::Error>
187    where
188        D: de::Deserializer<'de>,
189    {
190        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
191
192        // Extracts the `type` value.
193        #[derive(Deserialize)]
194        struct ExtractType<'a> {
195            #[serde(borrow, rename = "type")]
196            rule_type: Option<Cow<'a, str>>,
197        }
198
199        // Get the value of `type` if present.
200        let rule_type = serde_json::from_str::<ExtractType<'_>>(json.get())
201            .map_err(de::Error::custom)?
202            .rule_type;
203
204        match rule_type.as_deref() {
205            Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),
206            Some(_) => from_raw_json_value(&json).map(Self::_Custom),
207            None => Err(de::Error::missing_field("type")),
208        }
209    }
210}
211
212/// The kind of rule used for users wishing to join this room.
213#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
214#[derive(Clone, Default, PartialEq, Eq, StringEnum)]
215#[ruma_enum(rename_all = "snake_case")]
216#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
217pub enum JoinRuleKind {
218    /// A user who wishes to join the room must first receive an invite to the room from someone
219    /// already inside of the room.
220    Invite,
221
222    /// Users can join the room if they are invited, or they can request an invite to the room.
223    ///
224    /// They can be allowed (invited) or denied (kicked/banned) access.
225    Knock,
226
227    /// Reserved but not yet implemented by the Matrix specification.
228    Private,
229
230    /// Users can join the room if they are invited, or if they meet any of the conditions
231    /// described in a set of rules.
232    Restricted,
233
234    /// Users can join the room if they are invited, or if they meet any of the conditions
235    /// described in a set of rules, or they can request an invite to the room.
236    KnockRestricted,
237
238    /// Anyone can join the room without any prior action.
239    #[default]
240    Public,
241
242    #[doc(hidden)]
243    _Custom(PrivOwnedStr),
244}
245
246impl From<JoinRuleKind> for JoinRuleSummary {
247    fn from(value: JoinRuleKind) -> Self {
248        match value {
249            JoinRuleKind::Invite => Self::Invite,
250            JoinRuleKind::Knock => Self::Knock,
251            JoinRuleKind::Private => Self::Private,
252            JoinRuleKind::Restricted => Self::Restricted(Default::default()),
253            JoinRuleKind::KnockRestricted => Self::KnockRestricted(Default::default()),
254            JoinRuleKind::Public => Self::Public,
255            JoinRuleKind::_Custom(s) => Self::_Custom(s),
256        }
257    }
258}
259
260/// The summary of a room's state.
261#[derive(Debug, Clone, Serialize)]
262#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
263pub struct RoomSummary {
264    /// The ID of the room.
265    pub room_id: OwnedRoomId,
266
267    /// The canonical alias of the room, if any.
268    ///
269    /// If the `compat-empty-string-null` cargo feature is enabled, this field being an empty
270    /// string in JSON will result in `None` here during deserialization.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub canonical_alias: Option<OwnedRoomAliasId>,
273
274    /// The name of the room, if any.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub name: Option<String>,
277
278    /// The topic of the room, if any.
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub topic: Option<String>,
281
282    /// The URL for the room's avatar, if one is set.
283    ///
284    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
285    /// JSON will result in `None` here during deserialization.
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub avatar_url: Option<OwnedMxcUri>,
288
289    /// The type of room from `m.room.create`, if any.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub room_type: Option<RoomType>,
292
293    /// The number of members joined to the room.
294    pub num_joined_members: UInt,
295
296    /// The join rule of the room.
297    #[serde(flatten, skip_serializing_if = "ruma_common::serde::is_default")]
298    pub join_rule: JoinRuleSummary,
299
300    /// Whether the room may be viewed by users without joining.
301    pub world_readable: bool,
302
303    /// Whether guest users may join the room and participate in it.
304    ///
305    /// If they can, they will be subject to ordinary power level rules like any other user.
306    pub guest_can_join: bool,
307
308    /// If the room is encrypted, the algorithm used for this room.
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub encryption: Option<EventEncryptionAlgorithm>,
311
312    /// The version of the room.
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub room_version: Option<RoomVersionId>,
315}
316
317impl RoomSummary {
318    /// Construct a new `RoomSummary` with the given required fields.
319    pub fn new(
320        room_id: OwnedRoomId,
321        join_rule: JoinRuleSummary,
322        guest_can_join: bool,
323        num_joined_members: UInt,
324        world_readable: bool,
325    ) -> Self {
326        Self {
327            room_id,
328            canonical_alias: None,
329            name: None,
330            topic: None,
331            avatar_url: None,
332            room_type: None,
333            num_joined_members,
334            join_rule,
335            world_readable,
336            guest_can_join,
337            encryption: None,
338            room_version: None,
339        }
340    }
341}
342
343impl<'de> Deserialize<'de> for RoomSummary {
344    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
345    where
346        D: de::Deserializer<'de>,
347    {
348        /// Helper type to deserialize [`RoomSummary`] because using `flatten` on `join_rule`
349        /// returns an error.
350        #[derive(Deserialize)]
351        struct RoomSummaryDeHelper {
352            room_id: OwnedRoomId,
353            #[cfg_attr(
354                feature = "compat-empty-string-null",
355                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
356            )]
357            canonical_alias: Option<OwnedRoomAliasId>,
358            name: Option<String>,
359            topic: Option<String>,
360            #[cfg_attr(
361                feature = "compat-empty-string-null",
362                serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
363            )]
364            avatar_url: Option<OwnedMxcUri>,
365            room_type: Option<RoomType>,
366            num_joined_members: UInt,
367            world_readable: bool,
368            guest_can_join: bool,
369            encryption: Option<EventEncryptionAlgorithm>,
370            room_version: Option<RoomVersionId>,
371        }
372
373        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
374        let RoomSummaryDeHelper {
375            room_id,
376            canonical_alias,
377            name,
378            topic,
379            avatar_url,
380            room_type,
381            num_joined_members,
382            world_readable,
383            guest_can_join,
384            encryption,
385            room_version,
386        } = from_raw_json_value(&json)?;
387        let join_rule: JoinRuleSummary = from_raw_json_value(&json)?;
388
389        Ok(Self {
390            room_id,
391            canonical_alias,
392            name,
393            topic,
394            avatar_url,
395            room_type,
396            num_joined_members,
397            join_rule,
398            world_readable,
399            guest_can_join,
400            encryption,
401            room_version,
402        })
403    }
404}
405
406/// The rule used for users wishing to join a room.
407///
408/// In contrast to the regular `JoinRule` in `ruma_events`, this enum holds only simplified
409/// conditions for joining restricted rooms.
410#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
411#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
412#[serde(tag = "join_rule", rename_all = "snake_case")]
413pub enum JoinRuleSummary {
414    /// A user who wishes to join the room must first receive an invite to the room from someone
415    /// already inside of the room.
416    Invite,
417
418    /// Users can join the room if they are invited, or they can request an invite to the room.
419    ///
420    /// They can be allowed (invited) or denied (kicked/banned) access.
421    Knock,
422
423    /// Reserved but not yet implemented by the Matrix specification.
424    Private,
425
426    /// Users can join the room if they are invited, or if they meet one of the conditions
427    /// described in the [`RestrictedSummary`].
428    Restricted(RestrictedSummary),
429
430    /// Users can join the room if they are invited, or if they meet one of the conditions
431    /// described in the [`RestrictedSummary`], or they can request an invite to the room.
432    KnockRestricted(RestrictedSummary),
433
434    /// Anyone can join the room without any prior action.
435    #[default]
436    Public,
437
438    #[doc(hidden)]
439    #[serde(skip_serializing)]
440    _Custom(PrivOwnedStr),
441}
442
443impl JoinRuleSummary {
444    /// Returns the kind of this `JoinRuleSummary`.
445    pub fn kind(&self) -> JoinRuleKind {
446        match self {
447            Self::Invite => JoinRuleKind::Invite,
448            Self::Knock => JoinRuleKind::Knock,
449            Self::Private => JoinRuleKind::Private,
450            Self::Restricted(_) => JoinRuleKind::Restricted,
451            Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted,
452            Self::Public => JoinRuleKind::Public,
453            Self::_Custom(rule) => JoinRuleKind::_Custom(rule.clone()),
454        }
455    }
456
457    /// Returns the string name of this `JoinRuleSummary`.
458    pub fn as_str(&self) -> &str {
459        match self {
460            Self::Invite => "invite",
461            Self::Knock => "knock",
462            Self::Private => "private",
463            Self::Restricted(_) => "restricted",
464            Self::KnockRestricted(_) => "knock_restricted",
465            Self::Public => "public",
466            Self::_Custom(rule) => &rule.0,
467        }
468    }
469}
470
471impl From<JoinRule> for JoinRuleSummary {
472    fn from(value: JoinRule) -> Self {
473        match value {
474            JoinRule::Invite => Self::Invite,
475            JoinRule::Knock => Self::Knock,
476            JoinRule::Private => Self::Private,
477            JoinRule::Restricted(restricted) => Self::Restricted(restricted.into()),
478            JoinRule::KnockRestricted(restricted) => Self::KnockRestricted(restricted.into()),
479            JoinRule::Public => Self::Public,
480            JoinRule::_Custom(rule) => Self::_Custom(rule),
481        }
482    }
483}
484
485impl<'de> Deserialize<'de> for JoinRuleSummary {
486    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
487    where
488        D: de::Deserializer<'de>,
489    {
490        let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
491
492        #[derive(Deserialize)]
493        struct ExtractType<'a> {
494            #[serde(borrow)]
495            join_rule: Option<Cow<'a, str>>,
496        }
497
498        let Some(join_rule) = serde_json::from_str::<ExtractType<'_>>(json.get())
499            .map_err(de::Error::custom)?
500            .join_rule
501        else {
502            return Ok(Self::default());
503        };
504
505        match join_rule.as_ref() {
506            "invite" => Ok(Self::Invite),
507            "knock" => Ok(Self::Knock),
508            "private" => Ok(Self::Private),
509            "restricted" => from_raw_json_value(&json).map(Self::Restricted),
510            "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
511            "public" => Ok(Self::Public),
512            _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
513        }
514    }
515}
516
517/// A summary of the conditions for joining a restricted room.
518#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
519#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
520pub struct RestrictedSummary {
521    /// The room IDs which are specified by the join rules.
522    #[serde(default)]
523    pub allowed_room_ids: Vec<OwnedRoomId>,
524}
525
526impl RestrictedSummary {
527    /// Constructs a new `RestrictedSummary` with the given room IDs.
528    pub fn new(allowed_room_ids: Vec<OwnedRoomId>) -> Self {
529        Self { allowed_room_ids }
530    }
531}
532
533impl From<Restricted> for RestrictedSummary {
534    fn from(value: Restricted) -> Self {
535        let allowed_room_ids = value
536            .allow
537            .into_iter()
538            .filter_map(|allow_rule| {
539                let membership = as_variant!(allow_rule, AllowRule::RoomMembership)?;
540                Some(membership.room_id)
541            })
542            .collect();
543
544        Self::new(allowed_room_ids)
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use std::collections::BTreeMap;
551
552    use assert_matches2::assert_matches;
553    use js_int::uint;
554    use ruma_common::{owned_room_id, OwnedRoomId};
555    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
556
557    use super::{
558        AllowRule, CustomAllowRule, JoinRule, JoinRuleSummary, Restricted, RestrictedSummary,
559        RoomMembership, RoomSummary,
560    };
561
562    #[test]
563    fn deserialize_summary_no_join_rule() {
564        let json = json!({
565            "room_id": "!room:localhost",
566            "num_joined_members": 5,
567            "world_readable": false,
568            "guest_can_join": false,
569        });
570
571        let summary: RoomSummary = from_json_value(json).unwrap();
572        assert_eq!(summary.room_id, "!room:localhost");
573        assert_eq!(summary.num_joined_members, uint!(5));
574        assert!(!summary.world_readable);
575        assert!(!summary.guest_can_join);
576        assert_matches!(summary.join_rule, JoinRuleSummary::Public);
577    }
578
579    #[test]
580    fn deserialize_summary_private_join_rule() {
581        let json = json!({
582            "room_id": "!room:localhost",
583            "num_joined_members": 5,
584            "world_readable": false,
585            "guest_can_join": false,
586            "join_rule": "private",
587        });
588
589        let summary: RoomSummary = from_json_value(json).unwrap();
590        assert_eq!(summary.room_id, "!room:localhost");
591        assert_eq!(summary.num_joined_members, uint!(5));
592        assert!(!summary.world_readable);
593        assert!(!summary.guest_can_join);
594        assert_matches!(summary.join_rule, JoinRuleSummary::Private);
595    }
596
597    #[test]
598    fn deserialize_summary_restricted_join_rule() {
599        let json = json!({
600            "room_id": "!room:localhost",
601            "num_joined_members": 5,
602            "world_readable": false,
603            "guest_can_join": false,
604            "join_rule": "restricted",
605            "allowed_room_ids": ["!otherroom:localhost"],
606        });
607
608        let summary: RoomSummary = from_json_value(json).unwrap();
609        assert_eq!(summary.room_id, "!room:localhost");
610        assert_eq!(summary.num_joined_members, uint!(5));
611        assert!(!summary.world_readable);
612        assert!(!summary.guest_can_join);
613        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
614        assert_eq!(restricted.allowed_room_ids.len(), 1);
615    }
616
617    #[test]
618    fn deserialize_summary_restricted_join_rule_no_allowed_room_ids() {
619        let json = json!({
620            "room_id": "!room:localhost",
621            "num_joined_members": 5,
622            "world_readable": false,
623            "guest_can_join": false,
624            "join_rule": "restricted",
625        });
626
627        let summary: RoomSummary = from_json_value(json).unwrap();
628        assert_eq!(summary.room_id, "!room:localhost");
629        assert_eq!(summary.num_joined_members, uint!(5));
630        assert!(!summary.world_readable);
631        assert!(!summary.guest_can_join);
632        assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
633        assert_eq!(restricted.allowed_room_ids.len(), 0);
634    }
635
636    #[test]
637    fn serialize_summary_knock_join_rule() {
638        let summary = RoomSummary::new(
639            owned_room_id!("!room:localhost"),
640            JoinRuleSummary::Knock,
641            false,
642            uint!(5),
643            false,
644        );
645
646        assert_eq!(
647            to_json_value(&summary).unwrap(),
648            json!({
649                "room_id": "!room:localhost",
650                "num_joined_members": 5,
651                "world_readable": false,
652                "guest_can_join": false,
653                "join_rule": "knock",
654            })
655        );
656    }
657
658    #[test]
659    fn serialize_summary_restricted_join_rule() {
660        let summary = RoomSummary::new(
661            owned_room_id!("!room:localhost"),
662            JoinRuleSummary::Restricted(RestrictedSummary::new(vec![owned_room_id!(
663                "!otherroom:localhost"
664            )])),
665            false,
666            uint!(5),
667            false,
668        );
669
670        assert_eq!(
671            to_json_value(&summary).unwrap(),
672            json!({
673                "room_id": "!room:localhost",
674                "num_joined_members": 5,
675                "world_readable": false,
676                "guest_can_join": false,
677                "join_rule": "restricted",
678                "allowed_room_ids": ["!otherroom:localhost"],
679            })
680        );
681    }
682
683    #[test]
684    fn join_rule_to_join_rule_summary() {
685        assert_eq!(JoinRuleSummary::Invite, JoinRule::Invite.into());
686        assert_eq!(JoinRuleSummary::Knock, JoinRule::Knock.into());
687        assert_eq!(JoinRuleSummary::Public, JoinRule::Public.into());
688        assert_eq!(JoinRuleSummary::Private, JoinRule::Private.into());
689
690        assert_matches!(
691            JoinRule::KnockRestricted(Restricted::default()).into(),
692            JoinRuleSummary::KnockRestricted(restricted)
693        );
694        assert_eq!(restricted.allowed_room_ids, &[] as &[OwnedRoomId]);
695
696        let room_id = owned_room_id!("!room:localhost");
697        assert_matches!(
698            JoinRule::Restricted(Restricted::new(vec![AllowRule::RoomMembership(
699                RoomMembership::new(room_id.clone())
700            )]))
701            .into(),
702            JoinRuleSummary::Restricted(restricted)
703        );
704        assert_eq!(restricted.allowed_room_ids, [room_id]);
705    }
706
707    #[test]
708    fn roundtrip_custom_allow_rule() {
709        let json = r#"{"type":"org.msc9000.something","foo":"bar"}"#;
710        let allow_rule: AllowRule = serde_json::from_str(json).unwrap();
711        assert_matches!(&allow_rule, AllowRule::_Custom(_));
712        assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
713    }
714
715    #[test]
716    fn invalid_allow_items() {
717        let json = r#"{
718            "join_rule": "restricted",
719            "allow": [
720                {
721                    "type": "m.room_membership",
722                    "room_id": "!mods:example.org"
723                },
724                {
725                    "type": "m.room_membership",
726                    "room_id": ""
727                },
728                {
729                    "type": "m.room_membership",
730                    "room_id": "not a room id"
731                },
732                {
733                    "type": "org.example.custom",
734                    "org.example.minimum_role": "developer"
735                },
736                {
737                    "not even close": "to being correct",
738                    "any object": "passes this test",
739                    "only non-objects in this array": "cause deserialization to fail"
740                }
741            ]
742        }"#;
743        let join_rule: JoinRule = serde_json::from_str(json).unwrap();
744
745        assert_matches!(join_rule, JoinRule::Restricted(restricted));
746        assert_eq!(
747            restricted.allow,
748            &[
749                AllowRule::room_membership(owned_room_id!("!mods:example.org")),
750                AllowRule::_Custom(Box::new(CustomAllowRule {
751                    rule_type: "org.example.custom".into(),
752                    extra: BTreeMap::from([(
753                        "org.example.minimum_role".into(),
754                        "developer".into()
755                    )])
756                }))
757            ]
758        );
759    }
760}