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