xmtp_api_backend/middleware/
readonly_client.rs1xmtp_common::if_test! {
4 mod test;
5}
6
7use crate::backend::CREATE_UPLOAD_PATH;
8use derive_builder::Builder;
9use prost::bytes::Bytes;
10use xmtp_proto::api::{ApiClientError, Client};
11use xmtp_proto::api::{BytesStream, IsConnectedCheck};
12
13const PUBLISH_PATH: &str = "/xmtp.backend.v1.PublishService/Publish";
14
15#[derive(Debug, Builder, Default, Clone)]
17#[builder(public)]
18pub struct ReadonlyClient<Client> {
19 #[builder(public)]
20 pub(crate) inner: Client,
21}
22
23impl<C: Clone> ReadonlyClient<C> {
24 pub fn builder() -> ReadonlyClientBuilder<C> {
25 ReadonlyClientBuilder::default()
26 }
27}
28
29#[xmtp_common::async_trait]
30impl<C> Client for ReadonlyClient<C>
31where
32 C: Client,
33{
34 fn host(&self) -> &str {
35 self.inner.host()
36 }
37
38 fn has_credential_source(&self) -> bool {
39 self.inner.has_credential_source()
40 }
41
42 async fn request(
43 &self,
44 request: http::request::Builder,
45 path: http::uri::PathAndQuery,
46 body: Bytes,
47 ) -> Result<http::Response<Bytes>, ApiClientError> {
48 let p = path.path();
49 if p == PUBLISH_PATH || p == CREATE_UPLOAD_PATH {
50 return Err(ApiClientError::WritesDisabled);
51 }
52
53 self.inner.request(request, path, body).await
54 }
55
56 async fn stream(
57 &self,
58 request: http::request::Builder,
59 path: http::uri::PathAndQuery,
60 body: Bytes,
61 ) -> Result<http::Response<BytesStream>, ApiClientError> {
62 let p = path.path();
63 if p == PUBLISH_PATH || p == CREATE_UPLOAD_PATH {
64 return Err(ApiClientError::WritesDisabled);
65 }
66
67 self.inner.stream(request, path, body).await
68 }
69
70 async fn bidi_stream(
71 &self,
72 request: http::request::Builder,
73 path: http::uri::PathAndQuery,
74 body: xmtp_common::BoxDynStream<'static, Bytes>,
75 ) -> Result<http::Response<BytesStream>, ApiClientError> {
76 let p = path.path();
77 if p == PUBLISH_PATH || p == CREATE_UPLOAD_PATH {
78 return Err(ApiClientError::WritesDisabled);
79 }
80
81 self.inner.bidi_stream(request, path, body).await
82 }
83}
84
85#[xmtp_common::async_trait]
86impl<C> IsConnectedCheck for ReadonlyClient<C>
87where
88 C: IsConnectedCheck,
89{
90 async fn is_connected(&self) -> bool {
91 self.inner.is_connected().await
92 }
93}
94
95xmtp_common::if_test! {
96 use derive_builder::UninitializedFieldError;
97 use xmtp_proto::prelude::ApiBuilder;
98 #[allow(clippy::unwrap_used)]
99 impl<C> ReadonlyClientBuilder<C>
100 where
101 C: ApiBuilder,
102 {
103 pub(crate) fn build_builder(
104 self,
105 ) -> Result<ReadonlyClient<C::Output>, UninitializedFieldError> {
106 Ok(ReadonlyClient {
107 inner: <C as ApiBuilder>::build(
108 self.inner
109 .ok_or(UninitializedFieldError::new("read"))
110 .unwrap(),
111 )
112 .unwrap(),
113 })
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use crate::backend::{CreateUpload, GetInboxIds, Publish};
121
122 use super::*;
123 use rstest::*;
124
125 use xmtp_proto::api::{Query, mock::MockNetworkClient};
126 type MockClient = ReadonlyClient<MockNetworkClient>;
127
128 #[fixture]
129 fn ro() -> MockClient {
130 ReadonlyClient {
131 inner: MockNetworkClient::default(),
132 }
133 }
134
135 #[rstest]
136 #[xmtp_common::test(unwrap_try = true)]
137 async fn test_forwards_to_inner(mut ro: MockClient) {
138 ro.inner
139 .expect_request()
140 .times(1)
141 .returning(|_, _, _| Ok(http::Response::new(vec![].into())));
142 let mut e = GetInboxIds(Default::default());
143 e.query(&ro).await?;
144 }
145
146 #[rstest]
147 #[xmtp_common::test(unwrap_try = true)]
148 async fn test_errors_on_write(ro: MockClient) {
149 let mut e = Publish(Default::default());
150 let result = e.query(&ro).await;
151 assert!(matches!(result, Err(ApiClientError::WritesDisabled)));
152 }
153
154 #[rstest]
155 #[xmtp_common::test(unwrap_try = true)]
156 async fn readonly_blocks_create_upload(ro: MockClient) {
157 let mut endpoint = CreateUpload(Default::default());
158 let result = endpoint.query(&ro).await;
159 assert!(matches!(result, Err(ApiClientError::WritesDisabled)));
160 }
161}