Skip to main content

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