tsuzuri_libsql/
config.rs

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}