Skip to main content

ruma_common/profile/
user_profile.rs

1//! All the profile information for a user.
2
3use std::collections::{BTreeMap, btree_map};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value as JsonValue;
7
8#[cfg(feature = "unstable-msc4262")]
9use super::UserProfileUpdate;
10use super::{ProfileFieldName, ProfileFieldValue, static_profile_field::StaticProfileField};
11
12/// All the profile information for a user.
13#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct UserProfile(BTreeMap<String, JsonValue>);
16
17impl UserProfile {
18    /// Creates a new empty `UserProfile`.
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Returns the value of the given profile field.
24    pub fn get(&self, field: &str) -> Option<&JsonValue> {
25        self.0.get(field)
26    }
27
28    /// Returns the value of the given [`StaticProfileField`].
29    ///
30    /// Returns `Ok(Some(_))` if the field is present and the value was deserialized
31    /// successfully, `Ok(None)` if the field is not set, or an error if deserialization of the
32    /// value failed.
33    pub fn get_static<F: StaticProfileField>(&self) -> Result<Option<F::Value>, serde_json::Error> {
34        self.0.get(F::NAME).map(|value| serde_json::from_value(value.clone())).transpose()
35    }
36
37    /// Gets an iterator over the fields of the profile.
38    pub fn iter(&self) -> btree_map::Iter<'_, String, JsonValue> {
39        self.0.iter()
40    }
41
42    /// Sets a field to the given value.
43    pub fn set(&mut self, field: String, value: JsonValue) {
44        self.0.insert(field, value);
45    }
46
47    /// Merges a profile that contains updates (such as from a sync response) with this
48    /// profile.
49    ///
50    /// This operation preserves omitted values and removes null values.
51    #[cfg(feature = "unstable-msc4262")]
52    pub fn merge(&mut self, profile_update: UserProfileUpdate) {
53        for (field, value) in profile_update {
54            if value.is_null() {
55                self.0.remove(&field);
56            } else {
57                self.0.insert(field, value);
58            }
59        }
60    }
61}
62
63impl FromIterator<(String, JsonValue)> for UserProfile {
64    fn from_iter<T: IntoIterator<Item = (String, JsonValue)>>(iter: T) -> Self {
65        Self(iter.into_iter().collect())
66    }
67}
68
69impl FromIterator<(ProfileFieldName, JsonValue)> for UserProfile {
70    fn from_iter<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(iter: T) -> Self {
71        iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)).collect()
72    }
73}
74
75impl FromIterator<ProfileFieldValue> for UserProfile {
76    fn from_iter<T: IntoIterator<Item = ProfileFieldValue>>(iter: T) -> Self {
77        iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())).collect()
78    }
79}
80
81impl Extend<(String, JsonValue)> for UserProfile {
82    fn extend<T: IntoIterator<Item = (String, JsonValue)>>(&mut self, iter: T) {
83        self.0.extend(iter);
84    }
85}
86
87impl Extend<(ProfileFieldName, JsonValue)> for UserProfile {
88    fn extend<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(&mut self, iter: T) {
89        self.extend(iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)));
90    }
91}
92
93impl Extend<ProfileFieldValue> for UserProfile {
94    fn extend<T: IntoIterator<Item = ProfileFieldValue>>(&mut self, iter: T) {
95        self.extend(iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())));
96    }
97}
98
99impl IntoIterator for UserProfile {
100    type Item = (String, JsonValue);
101    type IntoIter = btree_map::IntoIter<String, JsonValue>;
102
103    fn into_iter(self) -> Self::IntoIter {
104        self.0.into_iter()
105    }
106}
107
108#[cfg(test)]
109#[cfg(all(feature = "unstable-msc4262", feature = "unstable-msc4426"))]
110mod tests {
111    use serde_json::{Value as JsonValue, json};
112
113    use crate::{
114        owned_mxc_uri,
115        profile::{
116            AvatarUrl, Call, CallProfileField, DisplayName, ProfileFieldValue, Status,
117            StatusProfileField, UserProfile, UserProfileUpdate,
118        },
119    };
120
121    #[test]
122    fn merge_profile() {
123        let mut profile = UserProfile::from_iter([
124            ProfileFieldValue::DisplayName("Alice".to_owned()),
125            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
126            ProfileFieldValue::Status(StatusProfileField {
127                text: "Working".to_owned(),
128                emoji: "🧑‍💻".to_owned(),
129            }),
130        ]);
131
132        let profile_update = UserProfileUpdate::from_iter([
133            ("avatar_url".to_owned(), JsonValue::Null),
134            ("org.matrix.msc4426.status".to_owned(), json!({ "text": "Holiday", "emoji": "🏖️"})),
135            ("org.matrix.msc4426.call".to_owned(), json!({})),
136        ]);
137
138        profile.merge(profile_update);
139
140        assert_eq!(
141            profile.get_static::<DisplayName>().unwrap().unwrap(),
142            "Alice".to_owned(),
143            "The display name should be preserved."
144        );
145        assert!(
146            profile.get_static::<AvatarUrl>().unwrap().is_none(),
147            "The avatar should be removed."
148        );
149        assert_eq!(
150            profile.get_static::<Status>().unwrap().unwrap(),
151            StatusProfileField { text: "Holiday".to_owned(), emoji: "🏖️".to_owned() },
152            "The status should be updated."
153        );
154        assert_eq!(
155            profile.get_static::<Call>().unwrap().unwrap(),
156            CallProfileField { call_joined_ts: None },
157            "The call indicator should be set."
158        );
159    }
160}