1use std::collections::BTreeMap;
2
3use as_variant::as_variant;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
6
7use crate::serde::from_raw_json_value;
8
9#[derive(Clone, Debug)]
14#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
15pub enum Action {
16 Notify,
18
19 #[cfg(feature = "unstable-msc3768")]
22 NotifyInApp,
23
24 SetTweak(Tweak),
26
27 #[doc(hidden)]
29 _Custom(CustomAction),
30}
31
32impl Action {
33 pub fn is_highlight(&self) -> bool {
35 matches!(self, Action::SetTweak(Tweak::Highlight(true)))
36 }
37
38 pub fn should_notify(&self) -> bool {
40 match self {
41 Action::Notify => true,
42 #[cfg(feature = "unstable-msc3768")]
43 Action::NotifyInApp => true,
44 _ => false,
45 }
46 }
47
48 #[cfg(feature = "unstable-msc3768")]
50 pub fn should_notify_remote(&self) -> bool {
51 matches!(self, Action::Notify)
52 }
53
54 pub fn sound(&self) -> Option<&str> {
56 as_variant!(self, Action::SetTweak(Tweak::Sound(sound)) => sound)
57 }
58}
59
60#[derive(Clone, Debug, Deserialize, Serialize)]
62#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
63#[serde(from = "tweak_serde::Tweak", into = "tweak_serde::Tweak")]
64pub enum Tweak {
65 Sound(String),
70
71 Highlight(#[serde(default = "crate::serde::default_true")] bool),
79
80 Custom {
82 name: String,
84
85 value: Box<RawJsonValue>,
87 },
88}
89
90impl<'de> Deserialize<'de> for Action {
91 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92 where
93 D: Deserializer<'de>,
94 {
95 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
96 let custom: CustomAction = from_raw_json_value(&json)?;
97
98 match &custom {
99 CustomAction::String(s) => match s.as_str() {
100 "notify" => Ok(Action::Notify),
101 #[cfg(feature = "unstable-msc3768")]
102 "org.matrix.msc3768.notify_in_app" => Ok(Action::NotifyInApp),
103 _ => Ok(Action::_Custom(custom)),
104 },
105 CustomAction::Object(o) => {
106 if o.get("set_tweak").is_some() {
107 Ok(Action::SetTweak(from_raw_json_value(&json)?))
108 } else {
109 Ok(Action::_Custom(custom))
110 }
111 }
112 }
113 }
114}
115
116impl Serialize for Action {
117 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118 where
119 S: Serializer,
120 {
121 match self {
122 Action::Notify => serializer.serialize_unit_variant("Action", 0, "notify"),
123 #[cfg(feature = "unstable-msc3768")]
124 Action::NotifyInApp => {
125 serializer.serialize_unit_variant("Action", 0, "org.matrix.msc3768.notify_in_app")
126 }
127 Action::SetTweak(kind) => kind.serialize(serializer),
128 Action::_Custom(custom) => custom.serialize(serializer),
129 }
130 }
131}
132
133#[doc(hidden)]
135#[allow(unknown_lints, unnameable_types)]
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(untagged)]
138pub enum CustomAction {
139 String(String),
141
142 Object(BTreeMap<String, JsonValue>),
144}
145
146mod tweak_serde {
147 use serde::{Deserialize, Serialize};
148 use serde_json::value::RawValue as RawJsonValue;
149
150 #[derive(Clone, Deserialize, Serialize)]
152 #[serde(untagged)]
153 pub(crate) enum Tweak {
154 Sound(SoundTweak),
155 Highlight(HighlightTweak),
156 Custom {
157 #[serde(rename = "set_tweak")]
158 name: String,
159 value: Box<RawJsonValue>,
160 },
161 }
162
163 #[derive(Clone, PartialEq, Deserialize, Serialize)]
164 #[serde(tag = "set_tweak", rename = "sound")]
165 pub(crate) struct SoundTweak {
166 value: String,
167 }
168
169 #[derive(Clone, PartialEq, Deserialize, Serialize)]
170 #[serde(tag = "set_tweak", rename = "highlight")]
171 pub(crate) struct HighlightTweak {
172 #[serde(
173 default = "crate::serde::default_true",
174 skip_serializing_if = "crate::serde::is_true"
175 )]
176 value: bool,
177 }
178
179 impl From<super::Tweak> for Tweak {
180 fn from(tweak: super::Tweak) -> Self {
181 use super::Tweak::*;
182
183 match tweak {
184 Sound(value) => Self::Sound(SoundTweak { value }),
185 Highlight(value) => Self::Highlight(HighlightTweak { value }),
186 Custom { name, value } => Self::Custom { name, value },
187 }
188 }
189 }
190
191 impl From<Tweak> for super::Tweak {
192 fn from(tweak: Tweak) -> Self {
193 use Tweak::*;
194
195 match tweak {
196 Sound(SoundTweak { value }) => Self::Sound(value),
197 Highlight(HighlightTweak { value }) => Self::Highlight(value),
198 Custom { name, value } => Self::Custom { name, value },
199 }
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use assert_matches2::assert_matches;
207 use serde_json::{from_value as from_json_value, json};
208
209 use super::{Action, Tweak};
210 use crate::assert_to_canonical_json_eq;
211
212 #[test]
213 fn serialize_notify() {
214 assert_to_canonical_json_eq!(Action::Notify, json!("notify"));
215 }
216
217 #[cfg(feature = "unstable-msc3768")]
218 #[test]
219 fn serialize_notify_in_app() {
220 assert_to_canonical_json_eq!(
221 Action::NotifyInApp,
222 json!("org.matrix.msc3768.notify_in_app"),
223 );
224 }
225
226 #[test]
227 fn serialize_tweak_sound() {
228 assert_to_canonical_json_eq!(
229 Action::SetTweak(Tweak::Sound("default".into())),
230 json!({ "set_tweak": "sound", "value": "default" })
231 );
232 }
233
234 #[test]
235 fn serialize_tweak_highlight() {
236 assert_to_canonical_json_eq!(
237 Action::SetTweak(Tweak::Highlight(true)),
238 json!({ "set_tweak": "highlight" })
239 );
240
241 assert_to_canonical_json_eq!(
242 Action::SetTweak(Tweak::Highlight(false)),
243 json!({ "set_tweak": "highlight", "value": false })
244 );
245 }
246
247 #[test]
248 fn deserialize_notify() {
249 assert_matches!(from_json_value::<Action>(json!("notify")), Ok(Action::Notify));
250 }
251
252 #[cfg(feature = "unstable-msc3768")]
253 #[test]
254 fn deserialize_notify_in_app() {
255 assert_matches!(
256 from_json_value::<Action>(json!("org.matrix.msc3768.notify_in_app")),
257 Ok(Action::NotifyInApp)
258 );
259 }
260
261 #[test]
262 fn deserialize_tweak_sound() {
263 let json_data = json!({
264 "set_tweak": "sound",
265 "value": "default"
266 });
267 assert_matches!(
268 from_json_value::<Action>(json_data),
269 Ok(Action::SetTweak(Tweak::Sound(value)))
270 );
271 assert_eq!(value, "default");
272 }
273
274 #[test]
275 fn deserialize_tweak_highlight() {
276 let json_data = json!({
277 "set_tweak": "highlight",
278 "value": true
279 });
280 assert_matches!(
281 from_json_value::<Action>(json_data),
282 Ok(Action::SetTweak(Tweak::Highlight(true)))
283 );
284 }
285
286 #[test]
287 fn deserialize_tweak_highlight_with_default_value() {
288 assert_matches!(
289 from_json_value::<Action>(json!({ "set_tweak": "highlight" })),
290 Ok(Action::SetTweak(Tweak::Highlight(true)))
291 );
292 }
293}