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 #[cfg(feature = "unstable-msc4262")]
49 pub fn apply(&mut self, changes: UserProfileChanges) {
50 for (field, value) in changes.updated {
51 self.0.insert(field.to_string(), value);
52 }
53
54 for field in changes.removed {
55 self.0.remove(field.as_str());
56 }
57 }
58}
59
60impl FromIterator<(String, JsonValue)> for UserProfile {
61 fn from_iter<T: IntoIterator<Item = (String, JsonValue)>>(iter: T) -> Self {
62 Self(iter.into_iter().collect())
63 }
64}
65
66impl FromIterator<(ProfileFieldName, JsonValue)> for UserProfile {
67 fn from_iter<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(iter: T) -> Self {
68 iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)).collect()
69 }
70}
71
72impl FromIterator<ProfileFieldValue> for UserProfile {
73 fn from_iter<T: IntoIterator<Item = ProfileFieldValue>>(iter: T) -> Self {
74 iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())).collect()
75 }
76}
77
78impl Extend<(String, JsonValue)> for UserProfile {
79 fn extend<T: IntoIterator<Item = (String, JsonValue)>>(&mut self, iter: T) {
80 self.0.extend(iter);
81 }
82}
83
84impl Extend<(ProfileFieldName, JsonValue)> for UserProfile {
85 fn extend<T: IntoIterator<Item = (ProfileFieldName, JsonValue)>>(&mut self, iter: T) {
86 self.extend(iter.into_iter().map(|(field, value)| (field.as_str().to_owned(), value)));
87 }
88}
89
90impl Extend<ProfileFieldValue> for UserProfile {
91 fn extend<T: IntoIterator<Item = ProfileFieldValue>>(&mut self, iter: T) {
92 self.extend(iter.into_iter().map(|value| (value.field_name(), value.value().into_owned())));
93 }
94}
95
96impl IntoIterator for UserProfile {
97 type Item = (String, JsonValue);
98 type IntoIter = btree_map::IntoIter<String, JsonValue>;
99
100 fn into_iter(self) -> Self::IntoIter {
101 self.0.into_iter()
102 }
103}
104
105#[cfg(test)]
106#[cfg(all(feature = "unstable-msc4262", feature = "unstable-msc4426"))]
107mod tests {
108 use std::collections::BTreeMap;
109
110 use serde_json::json;
111
112 use crate::{
113 owned_mxc_uri,
114 profile::{
115 AvatarUrl, Call, CallProfileField, DisplayName, ProfileFieldName, ProfileFieldValue,
116 Status, StatusProfileField, UserProfile, UserProfileChanges,
117 },
118 };
119
120 #[test]
121 fn apply_profile_update() {
122 let mut profile = UserProfile::from_iter([
123 ProfileFieldValue::DisplayName("Alice".to_owned()),
124 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
125 ProfileFieldValue::Status(StatusProfileField {
126 text: "Working".to_owned(),
127 emoji: "🧑💻".to_owned(),
128 }),
129 ]);
130
131 let mut profile_update = UserProfileChanges::new();
132 profile_update.removed = vec![ProfileFieldName::AvatarUrl];
133 profile_update.updated = BTreeMap::from([
134 (ProfileFieldName::Status, json!({ "text": "Holiday", "emoji": "🏖️"})),
135 (ProfileFieldName::Call, json!({})),
136 ]);
137
138 profile.apply(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}