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