Skip to main content

xmtp_api_backend/queries/
builder.rs

1use crate::{
2    AuthCallback, AuthHandle, AuthMiddleware, BackendClient, ReadonlyClient, TrackedStatsClient,
3    XmtpApiClient,
4};
5use std::sync::Arc;
6use xmtp_api_grpc::GrpcClient;
7use xmtp_proto::{
8    api::ToBoxedClient,
9    api_client::{ApiBuilder, NetConnectConfig},
10    types::AppVersion,
11};
12
13#[derive(Default, Clone)]
14pub struct MessageBackendBuilder {
15    host: Option<String>,
16    app_version: Option<AppVersion>,
17    readonly: bool,
18    auth_callback: Option<Arc<dyn AuthCallback>>,
19    auth_handle: Option<AuthHandle>,
20}
21
22#[derive(Debug, thiserror::Error)]
23pub enum MessageBackendBuilderError {
24    #[error("backend URL is required")]
25    MissingHost,
26    #[error(transparent)]
27    InvalidUrl(#[from] url::ParseError),
28    #[error(transparent)]
29    Grpc(#[from] xmtp_api_grpc::error::GrpcBuilderError),
30}
31
32impl MessageBackendBuilder {
33    pub fn new() -> Self {
34        Self::default()
35    }
36    pub fn host(&mut self, host: impl AsRef<str>) -> &mut Self {
37        self.host = Some(host.as_ref().into());
38        self
39    }
40    pub fn app_version(&mut self, version: impl Into<AppVersion>) -> &mut Self {
41        self.app_version = Some(version.into());
42        self
43    }
44    pub fn readonly(&mut self, value: bool) -> &mut Self {
45        self.readonly = value;
46        self
47    }
48    pub fn maybe_auth_callback(&mut self, value: Option<Arc<dyn AuthCallback>>) -> &mut Self {
49        self.auth_callback = value;
50        self
51    }
52    pub fn maybe_auth_handle(&mut self, value: Option<AuthHandle>) -> &mut Self {
53        self.auth_handle = value;
54        self
55    }
56    pub fn build(&mut self) -> Result<XmtpApiClient, MessageBackendBuilderError> {
57        let host = self
58            .host
59            .as_ref()
60            .ok_or(MessageBackendBuilderError::MissingHost)?;
61        let mut builder = GrpcClient::builder();
62        builder.set_host(url::Url::parse(host)?);
63        if let Some(version) = self.app_version.clone() {
64            builder.set_app_version(version)?;
65        }
66        let client = builder.build()?;
67        let client = if self.auth_callback.is_some() || self.auth_handle.is_some() {
68            AuthMiddleware::new(client, self.auth_callback.clone(), self.auth_handle.clone())
69                .arced()
70        } else {
71            client.arced()
72        };
73        let client = if self.readonly {
74            ReadonlyClient { inner: client }.arced()
75        } else {
76            client
77        };
78        Ok(Arc::new(TrackedStatsClient::new(BackendClient::new(
79            client,
80        ))))
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use xmtp_proto::{api::ApiClientError, api_client::XmtpBackendClient};
88
89    #[xmtp_common::test(unwrap_try = true)]
90    async fn builder_requires_a_host_and_attaches_readonly_policy() {
91        assert!(matches!(
92            MessageBackendBuilder::default().build(),
93            Err(MessageBackendBuilderError::MissingHost)
94        ));
95        let client = MessageBackendBuilder::default()
96            .host(xmtp_configuration::backend_test_url())
97            .readonly(true)
98            .build()?;
99        assert!(matches!(
100            client.publish(Default::default()).await,
101            Err(ApiClientError::WritesDisabled)
102        ));
103    }
104}