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::UserProfileChanges;
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    /// Removes the given profile field, returning it's value if it was previously set.
48    pub fn remove(&mut self, field: &str) -> Option<JsonValue> {
49        self.0.remove(field)
50    }
51
52    /// Applies the changes from a [`UserProfileChanges`] to this profile.
53    #[cfg(feature = "unstable-msc4262")]
54    pub fn apply(&mut self, changes: UserProfileChanges) {
55        for (field, value) in changes.updated {
56            self.0.insert(field.to_string(), value);
57        }
58
59        for field in changes.removed {
60            self.0.remove(field.as_str());
61        }
62    }
63}
64
65impl FromIterator<(String, JsonValue)> for UserProfile {
66    fn from_iter<T: IntoIterator<Item = (String, JsonValue)>>(iter: T) -> Self {
67        Self(iter.into_iter().collect())
68    }
69}
70
71impl FromIterator<(ProfileFieldName, JsonValue)> for UserProfile {
72    fn from_iter<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(iter: T) -> Self {
73        iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)).collect()
74    }
75}
76
77impl FromIterator<ProfileFieldValue> for UserProfile {
78    fn from_iter<T: IntoIterator<Item = ProfileFieldValue>>(iter: T) -> Self {
79        iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())).collect()
80    }
81}
82
83impl Extend<(String, JsonValue)> for UserProfile {
84    fn extend<T: IntoIterator<Item = (String, JsonValue)>>(&mut self, iter: T) {
85        self.0.extend(iter);
86    }
87}
88
89impl Extend<(ProfileFieldName, JsonValue)> for UserProfile {
90    fn extend<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(&mut self, iter: T) {
91        self.extend(iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)));
92    }
93}
94
95impl Extend<ProfileFieldValue> for UserProfile {
96    fn extend<T: IntoIterator<Item = ProfileFieldValue>>(&mut self, iter: T) {
97        self.extend(iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())));
98    }
99}
100
101impl IntoIterator for UserProfile {
102    type Item = (String, JsonValue);
103    type IntoIter = btree_map::IntoIter<String, JsonValue>;
104
105    fn into_iter(self) -> Self::IntoIter {
106        self.0.into_iter()
107    }
108}
109
110#[cfg(test)]
111#[cfg(all(feature = "unstable-msc4262", feature = "unstable-msc4426"))]
112mod tests {
113    use std::collections::BTreeMap;
114
115    use serde_json::json;
116
117    use crate::{
118        owned_mxc_uri,
119        profile::{
120            AvatarUrl, Call, CallProfileField, DisplayName, ProfileFieldName, ProfileFieldValue,
121            Status, StatusProfileField, UserProfile, UserProfileChanges,
122        },
123    };
124
125    #[test]
126    fn apply_profile_update() {
127        let mut profile = UserProfile::from_iter([
128            ProfileFieldValue::DisplayName("Alice".to_owned()),
129            ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
130            ProfileFieldValue::Status(StatusProfileField {
131                text: "Working".to_owned(),
132                emoji: "🧑‍💻".to_owned(),
133            }),
134        ]);
135
136        let mut profile_update = UserProfileChanges::new();
137        profile_update.removed = vec![ProfileFieldName::AvatarUrl];
138        profile_update.updated = BTreeMap::from([
139            (ProfileFieldName::Status, json!({ "text": "Holiday", "emoji": "🏖️"})),
140            (ProfileFieldName::Call, json!({})),
141        ]);
142
143        profile.apply(profile_update);
144
145        assert_eq!(
146            profile.get_static::<DisplayName>().unwrap().unwrap(),
147            "Alice".to_owned(),
148            "The display name should be preserved."
149        );
150        assert!(
151            profile.get_static::<AvatarUrl>().unwrap().is_none(),
152            "The avatar should be removed."
153        );
154        assert_eq!(
155            profile.get_static::<Status>().unwrap().unwrap(),
156            StatusProfileField { text: "Holiday".to_owned(), emoji: "🏖️".to_owned() },
157            "The status should be updated."
158        );
159        assert_eq!(
160            profile.get_static::<Call>().unwrap().unwrap(),
161            CallProfileField { call_joined_ts: None },
162            "The call indicator should be set."
163        );
164    }
165}