1pub mod v3 {
6 use std::marker::PhantomData;
17
18 #[cfg(feature = "client")]
19 use ruma_common::api::EmptyBody;
20 use ruma_common::{
21 OwnedUserId,
22 api::{Metadata, auth_scheme::NoAccessToken, error::Error, path_builder::VersionHistory},
23 metadata,
24 profile::{ProfileFieldName, ProfileFieldValue, StaticProfileField},
25 };
26
27 metadata! {
28 method: GET,
29 rate_limited: false,
30 authentication: NoAccessToken,
31 history: {
33 unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}",
34 1.0 => "/_matrix/client/r0/profile/{user_id}/{field}",
35 1.1 => "/_matrix/client/v3/profile/{user_id}/{field}",
36 }
37 }
38
39 #[derive(Clone, Debug)]
41 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
42 pub struct Request {
43 pub user_id: OwnedUserId,
45
46 pub field: ProfileFieldName,
48 }
49
50 impl Request {
51 pub fn new(user_id: OwnedUserId, field: ProfileFieldName) -> Self {
53 Self { user_id, field }
54 }
55
56 pub fn new_static<F: StaticProfileField>(user_id: OwnedUserId) -> RequestStatic<F> {
58 RequestStatic::new(user_id)
59 }
60 }
61
62 #[cfg(feature = "client")]
63 impl ruma_common::api::OutgoingRequest for Request {
64 type Body = EmptyBody;
65 type EndpointError = Error;
66 type IncomingResponse = Response;
67
68 fn try_into_http_request_inner(
69 self,
70 base_url: &str,
71 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
72 ) -> Result<http::Request<EmptyBody>, ruma_common::api::error::IntoHttpError> {
73 use ruma_common::api::path_builder::PathBuilder;
74
75 use crate::profile::field_existed_before_extended_profiles;
76
77 let url = if field_existed_before_extended_profiles(&self.field) {
78 Self::make_endpoint_url(considering, base_url, &[&self.user_id, &self.field], "")?
79 } else {
80 crate::profile::EXTENDED_PROFILE_FIELD_HISTORY.make_endpoint_url(
81 considering,
82 base_url,
83 &[&self.user_id, &self.field],
84 "",
85 )?
86 };
87
88 let http_request =
89 http::Request::builder().method(Self::METHOD).uri(url).body(EmptyBody)?;
90
91 Ok(http_request)
92 }
93 }
94
95 #[cfg(feature = "server")]
96 impl ruma_common::api::IncomingRequest for Request {
97 type EndpointError = Error;
98 type OutgoingResponse = Response;
99
100 fn try_from_http_request_inner(
101 _request: http::Request<&[u8]>,
102 path_args: &[&str],
103 ) -> Result<Self, ruma_common::api::error::DeserializationError> {
104 let (user_id, field) =
105 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
106 _,
107 serde::de::value::Error,
108 >::new(path_args.iter().copied()))?;
109
110 Ok(Self { user_id, field })
111 }
112 }
113
114 #[derive(Debug)]
116 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
117 pub struct RequestStatic<F: StaticProfileField> {
118 pub user_id: OwnedUserId,
120
121 field: PhantomData<F>,
123 }
124
125 impl<F: StaticProfileField> RequestStatic<F> {
126 pub fn new(user_id: OwnedUserId) -> Self {
128 Self { user_id, field: PhantomData }
129 }
130 }
131
132 impl<F: StaticProfileField> Clone for RequestStatic<F> {
133 fn clone(&self) -> Self {
134 Self { user_id: self.user_id.clone(), field: self.field }
135 }
136 }
137
138 impl<F: StaticProfileField> Metadata for RequestStatic<F> {
139 const METHOD: http::Method = Request::METHOD;
140 const RATE_LIMITED: bool = Request::RATE_LIMITED;
141 type Authentication = <Request as Metadata>::Authentication;
142 type PathBuilder = <Request as Metadata>::PathBuilder;
143 const PATH_BUILDER: VersionHistory = Request::PATH_BUILDER;
144 }
145
146 #[cfg(feature = "client")]
147 impl<F: StaticProfileField> ruma_common::api::OutgoingRequest for RequestStatic<F> {
148 type Body = EmptyBody;
149 type EndpointError = Error;
150 type IncomingResponse = ResponseStatic<F>;
151
152 fn try_into_http_request_inner(
153 self,
154 base_url: &str,
155 considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
156 ) -> Result<http::Request<EmptyBody>, ruma_common::api::error::IntoHttpError> {
157 Request::new(self.user_id, F::NAME.into())
158 .try_into_http_request_inner(base_url, considering)
159 }
160 }
161
162 #[derive(Debug, Clone, Default)]
164 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
165 pub struct Response {
166 pub value: Option<ProfileFieldValue>,
168 }
169
170 impl Response {
171 pub fn new(value: ProfileFieldValue) -> Self {
173 Self { value: Some(value) }
174 }
175 }
176
177 #[doc(hidden)]
178 #[derive(ruma_common::serde::_FakeDeriveSerde)]
179 #[cfg_attr(feature = "server", derive(ruma_common::api::OutgoingBodyJson))]
180 #[serde(transparent)]
181 pub struct ResponseBody(Option<ProfileFieldValue>);
182
183 #[cfg(feature = "server")]
184 impl serde::Serialize for ResponseBody {
185 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
186 where
187 S: serde::Serializer,
188 {
189 use ruma_common::serde::JsonObject;
190
191 if let Some(value) = &self.0 {
192 value.serialize(serializer)
193 } else {
194 JsonObject::new().serialize(serializer)
195 }
196 }
197 }
198
199 #[cfg(feature = "client")]
200 impl<'de> serde::Deserialize<'de> for ResponseBody {
201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202 where
203 D: serde::Deserializer<'de>,
204 {
205 use ruma_common::profile::ProfileFieldValueVisitor;
206
207 let value = deserializer.deserialize_map(ProfileFieldValueVisitor::new(None))?;
208
209 Ok(Self(value))
210 }
211 }
212
213 #[cfg(feature = "client")]
214 impl ruma_common::api::IncomingResponse for Response {
215 type EndpointError = Error;
216
217 fn try_from_http_response_inner(
218 response: http::Response<&[u8]>,
219 ) -> Result<Self, ruma_common::api::error::DeserializationError> {
220 let ResponseBody(value) = serde_json::from_slice(response.body())?;
221 Ok(Self { value })
222 }
223 }
224
225 #[cfg(feature = "server")]
226 impl ruma_common::api::OutgoingResponse for Response {
227 type Body = ResponseBody;
228
229 fn try_into_http_response_inner(
230 self,
231 ) -> Result<http::Response<Self::Body>, ruma_common::api::error::IntoHttpError> {
232 Ok(http::Response::builder()
233 .status(http::StatusCode::OK)
234 .body(ResponseBody(self.value))?)
235 }
236 }
237
238 #[derive(Debug)]
240 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
241 pub struct ResponseStatic<F: StaticProfileField> {
242 pub value: Option<F::Value>,
244 }
245
246 impl<F: StaticProfileField> Clone for ResponseStatic<F>
247 where
248 F::Value: Clone,
249 {
250 fn clone(&self) -> Self {
251 Self { value: self.value.clone() }
252 }
253 }
254
255 #[cfg(feature = "client")]
256 impl<F: StaticProfileField> ruma_common::api::IncomingResponse for ResponseStatic<F> {
257 type EndpointError = Error;
258
259 fn try_from_http_response_inner(
260 response: http::Response<&[u8]>,
261 ) -> Result<Self, ruma_common::api::error::DeserializationError> {
262 use serde::de::Deserializer;
263
264 use crate::profile::profile_field_serde::StaticProfileFieldVisitor;
265
266 let value = serde_json::Deserializer::from_slice(response.into_body())
267 .deserialize_map(StaticProfileFieldVisitor(PhantomData::<F>))?;
268
269 Ok(Self { value })
270 }
271 }
272}
273
274#[cfg(all(test, feature = "client"))]
275mod tests_client {
276 use ruma_common::{
277 owned_mxc_uri, owned_user_id,
278 profile::{ProfileFieldName, ProfileFieldValue},
279 };
280 use serde_json::{json, to_vec as to_json_vec};
281
282 use super::v3::{Request, RequestStatic, Response};
283
284 #[test]
285 fn serialize_request() {
286 use std::borrow::Cow;
287
288 use ruma_common::api::{
289 OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken,
290 };
291
292 let avatar_url_request =
294 Request::new(owned_user_id!("@alice:localhost"), ProfileFieldName::AvatarUrl);
295
296 let http_request = avatar_url_request
298 .clone()
299 .try_into_http_request::<Vec<u8>>(
300 "http://localhost/",
301 SendAccessToken::None,
302 Cow::Owned(SupportedVersions::from_parts(
303 &["v1.11".to_owned()],
304 &Default::default(),
305 )),
306 )
307 .unwrap();
308 assert_eq!(
309 http_request.uri().path(),
310 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
311 );
312
313 let http_request = avatar_url_request
315 .try_into_http_request::<Vec<u8>>(
316 "http://localhost/",
317 SendAccessToken::None,
318 Cow::Owned(SupportedVersions::from_parts(
319 &["v1.16".to_owned()],
320 &Default::default(),
321 )),
322 )
323 .unwrap();
324 assert_eq!(
325 http_request.uri().path(),
326 "/_matrix/client/v3/profile/@alice:localhost/avatar_url"
327 );
328
329 let custom_field_request =
331 Request::new(owned_user_id!("@alice:localhost"), "dev.ruma.custom_field".into());
332
333 let http_request = custom_field_request
335 .clone()
336 .try_into_http_request::<Vec<u8>>(
337 "http://localhost/",
338 SendAccessToken::None,
339 Cow::Owned(SupportedVersions::from_parts(
340 &["v1.11".to_owned()],
341 &Default::default(),
342 )),
343 )
344 .unwrap();
345 assert_eq!(
346 http_request.uri().path(),
347 "/_matrix/client/unstable/uk.tcpip.msc4133/profile/@alice:localhost/dev.ruma.custom_field"
348 );
349
350 let http_request = custom_field_request
352 .try_into_http_request::<Vec<u8>>(
353 "http://localhost/",
354 SendAccessToken::None,
355 Cow::Owned(SupportedVersions::from_parts(
356 &["v1.16".to_owned()],
357 &Default::default(),
358 )),
359 )
360 .unwrap();
361 assert_eq!(
362 http_request.uri().path(),
363 "/_matrix/client/v3/profile/@alice:localhost/dev.ruma.custom_field"
364 );
365 }
366
367 #[test]
368 fn deserialize_response() {
369 use ruma_common::api::IncomingResponseExt as _;
370
371 let body = json!({
372 "custom_field": "value",
373 })
374 .to_string();
375
376 let response =
377 Response::try_from_http_response(http::Response::new(body.as_bytes())).unwrap();
378 let value = response.value.unwrap();
379 assert_eq!(value.field_name().as_str(), "custom_field");
380 assert_eq!(value.value().as_str().unwrap(), "value");
381
382 let response =
383 Response::try_from_http_response(http::Response::new(b"{}".as_slice())).unwrap();
384 assert!(response.value.is_none());
385 }
386
387 fn get_static_response<R: ruma_common::api::OutgoingRequest>(
390 value: Option<ProfileFieldValue>,
391 ) -> Result<R::IncomingResponse, ruma_common::api::error::FromHttpResponseError<R::EndpointError>>
392 {
393 use ruma_common::api::IncomingResponseExt as _;
394
395 let body =
396 value.map(|value| to_json_vec(&value).unwrap()).unwrap_or_else(|| b"{}".to_vec());
397 R::IncomingResponse::try_from_http_response(http::Response::new(body.as_slice()))
398 }
399
400 #[test]
401 fn static_request_and_valid_response() {
402 use crate::profile::AvatarUrl;
403
404 let response = get_static_response::<RequestStatic<AvatarUrl>>(Some(
405 ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")),
406 ))
407 .unwrap();
408 assert_eq!(response.value.unwrap(), "mxc://localhost/abcdef");
409
410 let response = get_static_response::<RequestStatic<AvatarUrl>>(None).unwrap();
411 assert!(response.value.is_none());
412 }
413
414 #[test]
415 fn static_request_and_invalid_response() {
416 use crate::profile::AvatarUrl;
417
418 get_static_response::<RequestStatic<AvatarUrl>>(Some(ProfileFieldValue::DisplayName(
419 "Alice".to_owned(),
420 )))
421 .unwrap_err();
422 }
423}
424
425#[cfg(all(test, feature = "server"))]
426mod tests_server {
427 use ruma_common::{
428 owned_mxc_uri,
429 profile::{ProfileFieldName, ProfileFieldValue},
430 };
431 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
432
433 use super::v3::{Request, Response};
434
435 #[test]
436 fn deserialize_request() {
437 use ruma_common::api::IncomingRequestExt as _;
438
439 let request = Request::try_from_http_request(
440 http::Request::get(
441 "http://localhost/_matrix/client/v3/profile/@alice:localhost/displayname",
442 )
443 .body(&[] as &[u8])
444 .unwrap(),
445 &["@alice:localhost", "displayname"],
446 )
447 .unwrap();
448
449 assert_eq!(request.user_id, "@alice:localhost");
450 assert_eq!(request.field, ProfileFieldName::DisplayName);
451 }
452
453 #[test]
454 fn serialize_response() {
455 use ruma_common::api::OutgoingResponseExt;
456
457 let response =
458 Response::new(ProfileFieldValue::AvatarUrl(owned_mxc_uri!("mxc://localhost/abcdef")));
459
460 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
461
462 assert_eq!(
463 from_json_slice::<JsonValue>(http_response.body().as_ref()).unwrap(),
464 json!({
465 "avatar_url": "mxc://localhost/abcdef",
466 })
467 );
468 }
469}