1use ruma_common::{OwnedDeviceId, OwnedEventId, OwnedRoomId, serde::StringEnum};
6use ruma_macros::EventContent;
7use serde::{Deserialize, Serialize};
8
9use crate::PrivOwnedStr;
10
11#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
15#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
16#[ruma_event(
17 type = "org.matrix.msc4471.stream.cancel",
18 alias = "m.stream.cancel",
19 kind = ToDevice,
20)]
21pub struct ToDeviceStreamCancelEventContent {
22 pub room_id: OwnedRoomId,
24
25 pub event_id: OwnedEventId,
27
28 pub subscriber_device_id: OwnedDeviceId,
30
31 pub code: StreamCancelCode,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
38 pub reason: Option<String>,
39}
40
41impl ToDeviceStreamCancelEventContent {
42 pub fn new(
45 room_id: OwnedRoomId,
46 event_id: OwnedEventId,
47 subscriber_device_id: OwnedDeviceId,
48 code: StreamCancelCode,
49 ) -> Self {
50 Self { room_id, event_id, subscriber_device_id, code, reason: None }
51 }
52}
53
54#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
58#[derive(Clone, StringEnum)]
59#[ruma_enum(rename_all(prefix = "m.", rule = "snake_case"))]
60#[non_exhaustive]
61pub enum StreamCancelCode {
62 UnknownStream,
65
66 InvalidSubscription,
68
69 Forbidden,
71
72 LimitExceeded,
74
75 UserCancelled,
77
78 #[doc(hidden)]
79 _Custom(PrivOwnedStr),
80}
81
82#[cfg(test)]
83mod tests {
84 use assert_matches2::assert_matches;
85 use ruma_common::{
86 canonical_json::assert_to_canonical_json_eq, owned_device_id, owned_event_id, owned_room_id,
87 };
88 use serde_json::{from_value as from_json_value, json};
89
90 use super::{StreamCancelCode, ToDeviceStreamCancelEventContent};
91 use crate::{AnyToDeviceEvent, ToDeviceEvent};
92
93 #[test]
94 fn cancel_round_trip() {
95 let mut content = ToDeviceStreamCancelEventContent::new(
96 owned_room_id!("!room:example.org"),
97 owned_event_id!("$event:example.org"),
98 owned_device_id!("SUBSCRIBERDEVICE"),
99 StreamCancelCode::UnknownStream,
100 );
101 content.reason = Some("because".to_owned());
102
103 assert_to_canonical_json_eq!(
104 content,
105 json!({
106 "room_id": "!room:example.org",
107 "event_id": "$event:example.org",
108 "subscriber_device_id": "SUBSCRIBERDEVICE",
109 "code": "m.unknown_stream",
110 "reason": "because",
111 })
112 );
113 }
114
115 #[test]
116 fn cancel_code_serialization() {
117 for (variant, expected) in [
118 (StreamCancelCode::UnknownStream, "m.unknown_stream"),
119 (StreamCancelCode::InvalidSubscription, "m.invalid_subscription"),
120 (StreamCancelCode::Forbidden, "m.forbidden"),
121 (StreamCancelCode::LimitExceeded, "m.limit_exceeded"),
122 (StreamCancelCode::UserCancelled, "m.user_cancelled"),
123 ] {
124 assert_to_canonical_json_eq!(variant, json!(expected));
125 }
126 }
127
128 #[test]
129 fn cancel_code_deserialization() {
130 for (s, expected) in [
131 ("m.unknown_stream", StreamCancelCode::UnknownStream),
132 ("m.invalid_subscription", StreamCancelCode::InvalidSubscription),
133 ("m.forbidden", StreamCancelCode::Forbidden),
134 ("m.limit_exceeded", StreamCancelCode::LimitExceeded),
135 ("m.user_cancelled", StreamCancelCode::UserCancelled),
136 ] {
137 let code = from_json_value::<StreamCancelCode>(json!(s)).unwrap();
138 assert_eq!(code, expected);
139 }
140 }
141
142 #[test]
143 fn unknown_m_namespace_cancel_code_goes_to_custom() {
144 let code = from_json_value::<StreamCancelCode>(json!("m.future_code")).unwrap();
145 assert_to_canonical_json_eq!(code, json!("m.future_code"));
146 assert_ne!(code, StreamCancelCode::UnknownStream);
147 assert_ne!(code, StreamCancelCode::InvalidSubscription);
148 assert_ne!(code, StreamCancelCode::Forbidden);
149 assert_ne!(code, StreamCancelCode::LimitExceeded);
150 assert_ne!(code, StreamCancelCode::UserCancelled);
151 }
152
153 #[test]
154 fn custom_cancel_code_round_trips() {
155 let code = from_json_value::<StreamCancelCode>(json!("io.example.custom_reason")).unwrap();
156 assert_to_canonical_json_eq!(code, json!("io.example.custom_reason"));
157 }
158
159 #[test]
160 fn any_to_device_cancel() {
161 let event = json!({
162 "sender": "@alice:example.org",
163 "type": "org.matrix.msc4471.stream.cancel",
164 "content": {
165 "room_id": "!room:example.org",
166 "event_id": "$event:example.org",
167 "subscriber_device_id": "SUBSCRIBERDEVICE",
168 "code": "m.user_cancelled",
169 },
170 });
171
172 let event = from_json_value::<AnyToDeviceEvent>(event).unwrap();
173 assert_matches!(event, AnyToDeviceEvent::StreamCancel(ToDeviceEvent { content, .. }));
174 assert_eq!(content.code, StreamCancelCode::UserCancelled);
175 }
176
177 #[test]
178 fn any_to_device_cancel_stable_alias() {
179 let event = json!({
180 "sender": "@alice:example.org",
181 "type": "m.stream.cancel",
182 "content": {
183 "room_id": "!room:example.org",
184 "event_id": "$event:example.org",
185 "subscriber_device_id": "SUBSCRIBERDEVICE",
186 "code": "m.user_cancelled",
187 },
188 });
189
190 let event = from_json_value::<AnyToDeviceEvent>(event).unwrap();
191 assert_matches!(event, AnyToDeviceEvent::StreamCancel(ToDeviceEvent { content, .. }));
192 assert_eq!(content.code, StreamCancelCode::UserCancelled);
193 }
194}