ruma_common/profile/
user_profile.rs1use 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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct UserProfile(BTreeMap<String, JsonValue>);
16
17impl UserProfile {
18 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn get(&self, field: &str) -> Option<&JsonValue> {
25 self.0.get(field)
26 }
27
28 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 pub fn iter(&self) -> btree_map::Iter<'_, String, JsonValue> {
39 self.0.iter()
40 }
41
42 pub fn set(&mut self, field: String, value: JsonValue) {
44 self.0.insert(field, value);
45 }
46
47 pub fn remove(&mut self, field: &str) -> Option<JsonValue> {
49 self.0.remove(field)
50 }
51
52 #[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}