1use crate::read::{ConnectionConfig, EmbeddedReplicaConfig, RemoteConfig};
2use std::time::Duration;
3
4#[derive(Debug, Clone)]
5pub struct LibSqlConfig {
6 pub connection: ConnectionConfig,
7}
8
9impl LibSqlConfig {
10 pub fn builder() -> LibSqlConfigBuilder {
11 LibSqlConfigBuilder::new()
12 }
13
14 pub fn from_remote(url: impl Into<String>, auth_token: impl Into<String>) -> Self {
15 Self {
16 connection: ConnectionConfig::Remote(RemoteConfig {
17 url: url.into(),
18 auth_token: auth_token.into(),
19 }),
20 }
21 }
22
23 pub fn from_embedded_replica(
24 local_path: impl Into<String>,
25 sync_url: impl Into<String>,
26 auth_token: impl Into<String>,
27 ) -> Self {
28 Self {
29 connection: ConnectionConfig::EmbeddedReplica(EmbeddedReplicaConfig {
30 local_path: local_path.into(),
31 sync_url: sync_url.into(),
32 auth_token: auth_token.into(),
33 sync_interval: None,
34 encryption_key: None,
35 }),
36 }
37 }
38
39 pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
40 use std::env;
41
42 if env::var("DATABASE_USE_EMBEDDED_REPLICA").unwrap_or_default() == "true" {
43 let config = EmbeddedReplicaConfig {
44 local_path: env::var("DATABASE_LOCAL_PATH").unwrap_or_else(|_| "local.db".to_string()),
45 sync_url: env::var("DATABASE_URL")?,
46 auth_token: env::var("DATABASE_TOKEN")?,
47 sync_interval: env::var("DATABASE_SYNC_INTERVAL_SECS")
48 .ok()
49 .and_then(|s| s.parse::<u64>().ok())
50 .map(Duration::from_secs),
51 encryption_key: env::var("DATABASE_ENCRYPTION_KEY").ok(),
52 };
53 Ok(Self {
54 connection: ConnectionConfig::EmbeddedReplica(config),
55 })
56 } else {
57 let config = RemoteConfig {
58 url: env::var("DATABASE_URL")?,
59 auth_token: env::var("DATABASE_TOKEN")?,
60 };
61 Ok(Self {
62 connection: ConnectionConfig::Remote(config),
63 })
64 }
65 }
66
67 pub fn validate(&self) -> Result<(), ConfigError> {
68 match &self.connection {
69 ConnectionConfig::Remote(config) => {
70 if config.url.is_empty() {
71 return Err(ConfigError::InvalidConfiguration("URL cannot be empty".to_string()));
72 }
73 if config.auth_token.is_empty() {
74 return Err(ConfigError::InvalidConfiguration("Auth token cannot be empty".to_string()));
75 }
76 if !config.url.starts_with("libsql://") && !config.url.starts_with("https://") {
77 return Err(ConfigError::InvalidConfiguration(
78 "URL must start with libsql:// or https://".to_string(),
79 ));
80 }
81 }
82 ConnectionConfig::EmbeddedReplica(config) => {
83 if config.local_path.is_empty() {
84 return Err(ConfigError::InvalidConfiguration("Local path cannot be empty".to_string()));
85 }
86 if config.sync_url.is_empty() {
87 return Err(ConfigError::InvalidConfiguration("Sync URL cannot be empty".to_string()));
88 }
89 if config.auth_token.is_empty() {
90 return Err(ConfigError::InvalidConfiguration("Auth token cannot be empty".to_string()));
91 }
92 if !config.sync_url.starts_with("libsql://") && !config.sync_url.starts_with("https://") {
93 return Err(ConfigError::InvalidConfiguration(
94 "Sync URL must start with libsql:// or https://".to_string(),
95 ));
96 }
97 if let Some(ref key) = config.encryption_key {
98 let key_len = if key.len() == 64 {
99 32
100 } else {
101 key.len()
102 };
103 if key_len != 32 {
104 return Err(ConfigError::InvalidConfiguration(
105 "Encryption key must be exactly 32 bytes (256 bits)".to_string(),
106 ));
107 }
108 }
109 }
110 }
111 Ok(())
112 }
113}
114
115impl Default for LibSqlConfig {
116 fn default() -> Self {
117 Self {
118 connection: ConnectionConfig::Remote(RemoteConfig {
119 url: String::new(),
120 auth_token: String::new(),
121 }),
122 }
123 }
124}
125
126#[derive(Debug, Default)]
127pub struct LibSqlConfigBuilder {
128 connection_type: Option<ConnectionType>,
129 url: Option<String>,
130 auth_token: Option<String>,
131 local_path: Option<String>,
132 sync_interval: Option<Duration>,
133 encryption_key: Option<String>,
134}
135
136#[derive(Debug)]
137enum ConnectionType {
138 Remote,
139 EmbeddedReplica,
140}
141
142impl LibSqlConfigBuilder {
143 pub fn new() -> Self {
144 Self::default()
145 }
146
147 pub fn remote(mut self) -> Self {
148 self.connection_type = Some(ConnectionType::Remote);
149 self
150 }
151
152 pub fn embedded_replica(mut self) -> Self {
153 self.connection_type = Some(ConnectionType::EmbeddedReplica);
154 self
155 }
156
157 pub fn url(mut self, url: impl Into<String>) -> Self {
158 self.url = Some(url.into());
159 self
160 }
161
162 pub fn auth_token(mut self, token: impl Into<String>) -> Self {
163 self.auth_token = Some(token.into());
164 self
165 }
166
167 pub fn local_path(mut self, path: impl Into<String>) -> Self {
168 self.local_path = Some(path.into());
169 self
170 }
171
172 pub fn sync_interval(mut self, interval: Duration) -> Self {
173 self.sync_interval = Some(interval);
174 self
175 }
176
177 pub fn encryption_key(mut self, key: impl Into<String>) -> Self {
178 self.encryption_key = Some(key.into());
179 self
180 }
181
182 pub fn build(self) -> Result<LibSqlConfig, ConfigError> {
183 let connection_type = self.connection_type.ok_or(ConfigError::MissingConnectionType)?;
184 let url = self.url.ok_or(ConfigError::MissingUrl)?;
185 let auth_token = self.auth_token.ok_or(ConfigError::MissingAuthToken)?;
186
187 let connection = match connection_type {
188 ConnectionType::Remote => ConnectionConfig::Remote(RemoteConfig { url, auth_token }),
189 ConnectionType::EmbeddedReplica => {
190 let local_path = self.local_path.ok_or(ConfigError::MissingLocalPath)?;
191 ConnectionConfig::EmbeddedReplica(EmbeddedReplicaConfig {
192 local_path,
193 sync_url: url,
194 auth_token,
195 sync_interval: self.sync_interval,
196 encryption_key: self.encryption_key,
197 })
198 }
199 };
200
201 let config = LibSqlConfig { connection };
202 config.validate()?;
203 Ok(config)
204 }
205}
206
207#[derive(Debug, thiserror::Error)]
208pub enum ConfigError {
209 #[error("Connection type not specified. Use .remote() or .embedded_replica()")]
210 MissingConnectionType,
211 #[error("URL is required")]
212 MissingUrl,
213 #[error("Authentication token is required")]
214 MissingAuthToken,
215 #[error("Local path is required for embedded replica")]
216 MissingLocalPath,
217 #[error("Invalid configuration: {0}")]
218 InvalidConfiguration(String),
219}