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 auth_handle = (self.auth_callback.is_some() || self.auth_handle.is_some())
68            .then(|| self.auth_handle.clone().unwrap_or_default());
69        let client = if let Some(handle) = auth_handle.clone() {
70            AuthMiddleware::new(client, self.auth_callback.clone(), Some(handle)).arced()
71        } else {
72            client.arced()
73        };
74        let client = if self.readonly {
75            ReadonlyClient { inner: client }.arced()
76        } else {
77            client
78        };
79        Ok(Arc::new(TrackedStatsClient::new(
80            BackendClient::new(client).with_auth_handle(auth_handle),
81        )))
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use xmtp_proto::{api::ApiClientError, api_client::XmtpBackendClient};
89
90    #[xmtp_common::test(unwrap_try = true)]
91    async fn builder_requires_a_host_and_attaches_readonly_policy() {
92        assert!(matches!(
93            MessageBackendBuilder::default().build(),
94            Err(MessageBackendBuilderError::MissingHost)
95        ));
96        let client = MessageBackendBuilder::default()
97            .host(xmtp_configuration::backend_test_url())
98            .readonly(true)
99            .build()?;
100        assert!(matches!(
101            client.publish(Default::default()).await,
102            Err(ApiClientError::WritesDisabled)
103        ));
104    }
105}