spanner_rs/
config.rs

1use bb8::{Builder as PoolBuilder, Pool};
2use tonic::transport::ClientTlsConfig;
3
4use crate::{Client, DatabaseId, Error, InstanceId, ProjectId, SessionManager};
5use derive_builder::Builder;
6
7/// Configuration for building a [`Client`].
8///
9/// # Example
10///
11/// ```no_run
12/// use spanner_rs::Config;
13/// #[tokio::main]
14/// # async fn main() -> Result<(), spanner_rs::Error> {
15/// let mut client = Config::builder()
16///     .project("my-gcp-project")
17///     .instance("my-spanner-instance")
18///     .database("my-database")
19///     .connect()
20///     .await?;
21/// # Ok(()) }
22/// ```
23#[derive(Builder, Debug)]
24#[builder(pattern = "owned", build_fn(error = "crate::Error"))]
25pub struct Config {
26    /// Set the URI to use to reach the Spanner API. Leave unspecified to use Cloud Spanner.
27    #[builder(setter(strip_option, into), default)]
28    endpoint: Option<String>,
29
30    /// Set custom client-side TLS settings.
31    #[builder(setter(strip_option), default = "Some(ClientTlsConfig::default())")]
32    tls_config: Option<ClientTlsConfig>,
33
34    /// Specify the GCP project where the Cloud Spanner instance exists.
35    ///
36    /// This may be left unspecified, in which case, the project will be extracted
37    /// from the credentials. Note that this only works when authenticating using [service accounts](https://cloud.google.com/docs/authentication/production).
38    #[builder(setter(strip_option, into), default)]
39    project: Option<String>,
40
41    /// Set the Cloud Spanner instance ID.
42    #[builder(setter(strip_option, into))]
43    instance: String,
44
45    /// Set the Cloud Spanner database name.
46    #[builder(setter(strip_option, into))]
47    database: String,
48
49    /// Programatically specify the credentials file to use during authentication.
50    ///
51    /// When this is specified, it is used in favor of the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
52    #[builder(setter(strip_option, into), default)]
53    credentials_file: Option<String>,
54
55    /// Configuration for the embedded session pool.
56    #[builder(setter(strip_option), default)]
57    session_pool_config: Option<SessionPoolConfig>,
58}
59
60impl Config {
61    /// Returns a new [`ConfigBuilder`] for configuring a new client.
62    pub fn builder() -> ConfigBuilder {
63        ConfigBuilder::default()
64    }
65
66    /// Connect to Cloud Spanner and return a new [`Client`].
67    ///
68    /// # Example
69    ///
70    /// ```no_run
71    /// use spanner_rs::Config;
72    /// #[tokio::main]
73    /// # async fn main() -> Result<(), spanner_rs::Error> {
74    /// let mut client = Config::builder()
75    ///     .project("my-gcp-project")
76    ///     .instance("my-spanner-instance")
77    ///     .database("my-database")
78    ///     .connect()
79    ///     .await?;
80    /// # Ok(()) }
81    /// ```
82    ///
83    /// # Authentication
84    ///
85    /// Authentication uses the [`gcp_auth`] crate which supports several authentication methods.
86    /// In a typical production environment, nothing needs to be programatically provided during configuration as
87    /// credentials are normally obtained from the environment (i.e.: `GOOGLE_APPLICATION_CREDENTIALS`).
88    ///
89    /// Similarly, for local development, authentication will transparently delegate to the `gcloud` command line tool.
90    pub async fn connect(self) -> Result<Client, Error> {
91        let auth = if self.tls_config.is_none() {
92            None
93        } else {
94            match self.credentials_file {
95                Some(file) => Some(gcp_auth::CustomServiceAccount::from_file(file)?.into()),
96                None => Some(gcp_auth::AuthenticationManager::new().await?),
97            }
98        };
99
100        let project_id = match self.project {
101            Some(project) => project,
102            None => {
103                if let Some(auth) = auth.as_ref() {
104                    auth.project_id().await?
105                } else {
106                    return Err(Error::Config("missing project id".to_string()));
107                }
108            }
109        };
110        let database_id = DatabaseId::new(
111            InstanceId::new(ProjectId::new(&project_id), &self.instance),
112            &self.database,
113        );
114
115        let connection =
116            crate::connection::grpc::connect(self.endpoint, self.tls_config, auth, database_id)
117                .await?;
118
119        let pool = self
120            .session_pool_config
121            .unwrap_or_default()
122            .build()
123            .build(SessionManager::new(connection.clone()))
124            .await?;
125
126        Ok(Client::connect(connection, pool))
127    }
128}
129
130impl ConfigBuilder {
131    /// Disable TLS when connecting to Spanner. This usually only makes sense when using the emulator.
132    /// Note that this also disables authentication to prevent sending secrets in plain text.
133    #[must_use]
134    pub fn disable_tls(self) -> Self {
135        Self {
136            tls_config: Some(None),
137            ..self
138        }
139    }
140
141    /// Configure the client to connect to a Spanner emulator, e.g.: `http://localhost:9092`
142    /// This disables TLS.
143    #[must_use]
144    pub fn with_emulator_host(self, endpoint: String) -> Self {
145        self.endpoint(endpoint).disable_tls()
146    }
147
148    /// Configure the client to connect to a Spanner emulator running on localhost and using the specified port.
149    /// This disables TLS.
150    #[must_use]
151    pub fn with_emulator_grpc_port(self, port: u16) -> Self {
152        self.with_emulator_host(format!("http://localhost:{}", port))
153    }
154
155    /// See [Config::connect]
156    pub async fn connect(self) -> Result<Client, Error> {
157        self.build()?.connect().await
158    }
159}
160
161/// Configuration for the internal Cloud Spanner session pool.
162///
163/// # Example
164///
165/// ```
166/// use spanner_rs::{Config, SessionPoolConfig};
167///
168/// # fn main() -> Result<(), spanner_rs::Error> {
169/// Config::builder().session_pool_config(SessionPoolConfig::builder().max_size(100).build()?);
170/// # Ok(()) }
171/// ```
172#[derive(Builder, Default, Debug)]
173#[builder(pattern = "owned", build_fn(error = "crate::Error"))]
174pub struct SessionPoolConfig {
175    /// Specify the maximum number of sessions that should be maintained in the pool.
176    #[builder(setter(strip_option), default)]
177    max_size: Option<u32>,
178
179    /// Specify the minimum number of sessions that should be maintained in the pool.
180    #[builder(setter(strip_option), default)]
181    min_idle: Option<u32>,
182}
183
184impl SessionPoolConfig {
185    pub fn builder() -> SessionPoolConfigBuilder {
186        SessionPoolConfigBuilder::default()
187    }
188
189    fn build(self) -> PoolBuilder<SessionManager> {
190        let mut builder = Pool::builder().test_on_check_out(false);
191        if let Some(max_size) = self.max_size {
192            builder = builder.max_size(max_size);
193        }
194        builder.min_idle(self.min_idle)
195    }
196}
197
198#[cfg(test)]
199mod test {
200
201    use super::*;
202
203    #[test]
204    fn test_config_database() {
205        let cfg = Config::builder()
206            .project("project")
207            .instance("instance")
208            .database("database")
209            .build()
210            .unwrap();
211
212        assert_eq!(cfg.project, Some("project".to_string()));
213        assert_eq!(cfg.instance, "instance".to_string());
214        assert_eq!(cfg.database, "database".to_string());
215    }
216
217    #[test]
218    fn test_config_endpoint() {
219        let cfg = Config::builder().endpoint("endpoint");
220        assert_eq!(cfg.endpoint, Some(Some("endpoint".to_string())))
221    }
222
223    #[test]
224    fn test_session_pool_config() {
225        let built = SessionPoolConfig::builder()
226            .max_size(10)
227            .min_idle(100)
228            .build()
229            .unwrap();
230
231        assert_eq!(built.max_size, Some(10));
232        assert_eq!(built.min_idle, Some(100));
233    }
234}