Skip to main content

arminius_lib_roomservice_client/
ruma_request_wrapper.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Module containing a serializing wrapper for Ruma requests
6//!
7//! This module implements a type [SerializingRumaRequestWrapper], which wraps a
8//! ruma request object, and allows us to serialize it, by converting it into
9//! a HTTP request. This is the only way that we can send Ruma requests over
10//! NATS.
11
12mod tests;
13
14use std::{borrow::Cow, collections::BTreeSet};
15
16use axum::http::Request;
17use itertools::Itertools as _;
18use ruma::api::{
19	IncomingRequest, IncomingRequestExt, MatrixVersion, Metadata, OutgoingRequest,
20	OutgoingRequestExt, SupportedVersions,
21	auth_scheme::{AccessToken, AuthScheme, SendAccessToken},
22	path_builder::{PathBuilder, SinglePath, VersionHistory},
23};
24use serde::{Deserialize, Serialize, de::Error as _, ser::Error as _};
25
26/// Allows us to pick a version for sending simple ruma requests
27///
28/// In the [`SerializingRumaRequestWrapper`] we don't actually care about
29/// endpoint versioning, since the request is only serialized and deserialized
30/// locally. Therefore, this just always picks V1_16.
31///
32/// NOTE: A second implementation is also in
33/// `crates/libs/arminius-lib-testing/src/utils::ruma.rs`, but due to
34/// dependency resolution problems, they cannot depend on one another.
35pub trait DefaultPathInput<T: PathBuilder> {
36	/// Get a default input to the path builder
37	fn get_default<'a>() -> T::Input<'a>;
38}
39
40impl DefaultPathInput<VersionHistory> for VersionHistory {
41	fn get_default<'a>() -> <VersionHistory as PathBuilder>::Input<'a> {
42		Cow::Owned(SupportedVersions {
43			versions: BTreeSet::from([MatrixVersion::V1_16]),
44			features: BTreeSet::new(),
45		})
46	}
47}
48
49impl DefaultPathInput<SinglePath> for SinglePath {
50	fn get_default<'a>() -> <SinglePath as PathBuilder>::Input<'a> {}
51}
52
53/// Allows us to set a placeholder auth scheme
54///
55/// Use when we don't actually care about authenticating requests. With the
56/// [`SerializingRumaRequestWrapper`], the authentication isn't information we
57/// actually care about serializing, so we just put anything to make the ruma
58/// trait happy.
59pub trait DefaultAuthScheme<T: AuthScheme> {
60	/// Get a default input to the path builder
61	fn get_default<'a>() -> T::Input<'a>;
62}
63
64impl DefaultAuthScheme<AccessToken> for AccessToken {
65	fn get_default<'a>() -> <AccessToken as AuthScheme>::Input<'a> {
66		SendAccessToken::Always("")
67	}
68}
69
70/// A wrapper around ruma Request types, that serializes them by turning them
71/// into an URI, and the body as bytes
72#[derive(Debug)]
73pub struct SerializingRumaRequestWrapper<T>(pub T)
74where
75	T: IncomingRequest + OutgoingRequest + Clone,
76	<T as Metadata>::Authentication: DefaultAuthScheme<<T as Metadata>::Authentication>,
77	<T as Metadata>::PathBuilder: DefaultPathInput<<T as Metadata>::PathBuilder>;
78
79/// A struct that gets created by the serialization method of
80/// SerializingRumaRequestWrapper, solely for the purpose of getting serialized
81/// immediately
82#[derive(Debug, Serialize, Deserialize)]
83struct SerializedRumaRequest {
84	/// The URI used in the HTTP request
85	pub uri: String,
86	/// Bytes of the body
87	pub request_bytes: Vec<u8>,
88}
89
90impl<T> Serialize for SerializingRumaRequestWrapper<T>
91where
92	T: Clone + IncomingRequest + OutgoingRequest,
93	<T as Metadata>::Authentication: DefaultAuthScheme<<T as Metadata>::Authentication>,
94	<T as Metadata>::PathBuilder: DefaultPathInput<<T as Metadata>::PathBuilder>,
95{
96	fn serialize<'a, S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97	where
98		S: serde::Serializer,
99		T: Clone + IncomingRequest + OutgoingRequest,
100		<T as Metadata>::Authentication: DefaultAuthScheme<<T as Metadata>::Authentication>,
101		<T as Metadata>::PathBuilder: DefaultPathInput<<T as Metadata>::PathBuilder>,
102	{
103		let auth_input: <T::Authentication as AuthScheme>::Input<'a> =
104			<T::Authentication as DefaultAuthScheme<T::Authentication>>::get_default::<'a>();
105		let path_input: <T::PathBuilder as PathBuilder>::Input<'a> =
106			<T::PathBuilder as DefaultPathInput<T::PathBuilder>>::get_default::<'a>();
107		let request: Request<Vec<u8>> =
108			self.0.clone().try_into_http_request("", auth_input, path_input).map_err(|err| {
109				S::Error::custom(format!("Failed serializing into HTTP request: {err}"))
110			})?;
111		let (parts, body) = request.into_parts();
112		let serializer_struct =
113			SerializedRumaRequest { uri: parts.uri.to_string(), request_bytes: body };
114		serializer_struct.serialize(serializer)
115	}
116}
117
118impl<'de, T> Deserialize<'de> for SerializingRumaRequestWrapper<T>
119where
120	T: IncomingRequestExt + OutgoingRequestExt + Clone,
121	<T as Metadata>::Authentication: DefaultAuthScheme<<T as Metadata>::Authentication>,
122	<T as Metadata>::PathBuilder: DefaultPathInput<<T as Metadata>::PathBuilder>,
123{
124	fn deserialize<'a, D>(deserializer: D) -> Result<Self, D::Error>
125	where
126		D: serde::Deserializer<'de>,
127		<T as Metadata>::Authentication: DefaultAuthScheme<<T as Metadata>::Authentication>,
128		<T as Metadata>::PathBuilder: DefaultPathInput<<T as Metadata>::PathBuilder>,
129	{
130		let serializer_struct = SerializedRumaRequest::deserialize(deserializer)?;
131		let mut request = Request::new(&serializer_struct.request_bytes[..]);
132		*request.uri_mut() = serializer_struct
133			.uri
134			.clone()
135			.try_into()
136			.map_err(|err| D::Error::custom(format!("Failed parsing serialized URI: {err}")))?;
137		*request.method_mut() = <T as Metadata>::METHOD;
138
139		// Here we essentially re-implement Axum's path parsing.
140		// It's kind of a terrible thing to do, but we should be able to remove
141		// this when we have OpenAPI parsing
142		let path_segments: Vec<&str> =
143			serializer_struct.uri.split('/').filter(|segment| !segment.is_empty()).collect();
144
145		let path_input: <T::PathBuilder as PathBuilder>::Input<'a> =
146			<T::PathBuilder as DefaultPathInput<T::PathBuilder>>::get_default::<'a>();
147		let endpoint_path = T::PathBuilder::select_path(&<T as Metadata>::PATH_BUILDER, path_input)
148			.map_err(|_| {
149				D::Error::custom("Failed to select a path when deserializing request".to_owned())
150			})?;
151		let path_with_placeholders = endpoint_path.split("/").skip(1);
152		let path_args = path_segments
153			.iter()
154			.zip_longest(path_with_placeholders)
155			.map(|segment| {
156				let (path_segment, template_segment) = match segment {
157					itertools::EitherOrBoth::Both(path, template) => Ok((*path, template)),
158					itertools::EitherOrBoth::Left(_) => {
159						Err(D::Error::custom("Too many path segments in serialized request"))
160					}
161					itertools::EitherOrBoth::Right(template) => Ok(("", template)),
162				}?;
163
164				if template_segment.starts_with(':')
165					|| (template_segment.starts_with('{') && template_segment.ends_with('}'))
166				{
167					Ok(Some(path_segment))
168				} else {
169					Ok(None)
170				}
171			})
172			.collect::<Result<Vec<Option<&str>>, _>>()?
173			.into_iter()
174			.flatten()
175			.collect::<Vec<&str>>();
176
177		Ok(SerializingRumaRequestWrapper(T::try_from_http_request(request, &path_args).map_err(
178			|err| D::Error::custom(format!("Failed deserializing HTTP request: {err}")),
179		)?))
180	}
181}