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}