1#[non_exhaustive]
11#[derive(Clone)]
12pub struct ReplicationConfig {
13 pub replica_path: String,
15 pub sync_url: Option<String>,
17 pub auth_token: Option<String>,
19 pub sync_interval_secs: u64,
21}
22
23impl ReplicationConfig {
24 pub fn new(replica_path: impl Into<String>) -> Self {
26 Self {
27 replica_path: replica_path.into(),
28 sync_url: None,
29 auth_token: None,
30 sync_interval_secs: 10,
31 }
32 }
33
34 pub fn with_sync_url(mut self, sync_url: impl Into<String>) -> Self {
36 self.sync_url = Some(sync_url.into());
37 self
38 }
39
40 pub fn with_auth_token(mut self, auth_token: impl Into<String>) -> Self {
42 self.auth_token = Some(auth_token.into());
43 self
44 }
45
46 pub fn with_sync_interval(mut self, secs: u64) -> Self {
48 self.sync_interval_secs = secs;
49 self
50 }
51}
52
53impl std::fmt::Debug for ReplicationConfig {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("ReplicationConfig")
56 .field("replica_path", &self.replica_path)
57 .field(
58 "sync_url",
59 &self.sync_url.as_deref().map(crate::config::redacted_url),
60 )
61 .field(
62 "auth_token",
63 &self.auth_token.as_ref().map(|_| "<redacted>"),
64 )
65 .field("sync_interval_secs", &self.sync_interval_secs)
66 .finish()
67 }
68}
69
70pub struct ReplicationManager;
72
73#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum ReplicationError {
77 #[error("SQLite replication backend is not implemented for `{sync_url}`")]
79 Unsupported {
80 sync_url: String,
82 },
83}
84
85impl ReplicationManager {
86 #[cfg_attr(mutants, mutants::skip)]
92 pub fn start(config: ReplicationConfig) -> Result<(), ReplicationError> {
93 if let Some(sync_url) = config.sync_url {
94 return Err(ReplicationError::Unsupported { sync_url });
95 }
96 Ok(())
97 }
98}
99
100#[cfg(all(not(target_arch = "wasm32"), feature = "orm"))]
102pub use rullst_orm::{Orm, RullstModel, RullstPool, async_trait, schema};
103#[cfg(all(not(target_arch = "wasm32"), feature = "orm"))]
104pub use sqlx;
105#[cfg(all(not(target_arch = "wasm32"), feature = "orm"))]
106pub use sqlx::FromRow;
107
108#[cfg(all(not(target_arch = "wasm32"), feature = "orm"))]
110#[cfg_attr(mutants, mutants::skip)]
111pub fn safe_pool() -> Option<&'static rullst_orm::RullstPool> {
112 rullst_orm::Orm::try_pool().ok()
113}
114
115#[cfg(all(not(target_arch = "wasm32"), feature = "orm"))]
117#[cfg_attr(mutants, mutants::skip)]
118pub fn safe_driver() -> Option<&'static str> {
119 rullst_orm::Orm::try_driver().ok()
120}
121
122#[cfg(test)]
123#[allow(clippy::unwrap_used, clippy::expect_used)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn replication_debug_output_redacts_credentials() {
129 let config = ReplicationConfig::new("replica.db")
130 .with_sync_url("libsql://db-org.turso.io?authToken=url-secret")
131 .with_auth_token("bearer-secret");
132 let debug = format!("{config:?}");
133 assert!(
134 !debug.contains("bearer-secret"),
135 "Debug must redact the auth token"
136 );
137 assert!(
138 !debug.contains("url-secret"),
139 "Debug must redact the sync URL"
140 );
141 assert!(
142 debug.contains("replica.db"),
143 "Debug must keep the replica path"
144 );
145 assert!(
146 debug.contains("libsql://<redacted>"),
147 "Debug must keep the URL scheme"
148 );
149 assert!(
150 debug.contains("auth_token: Some(\"<redacted>\")"),
151 "Debug must mark the auth token as redacted"
152 );
153 }
154
155 #[tokio::test]
156 async fn test_db_get_pool() {
157 let config = ReplicationConfig::new("test.db")
158 .with_sync_interval(20)
159 .with_auth_token("secret");
160 assert_eq!(config.replica_path, "test.db");
161 assert_eq!(config.sync_interval_secs, 20);
162 assert_eq!(config.auth_token, Some("secret".to_string()));
163 }
164
165 #[test]
166 fn test_replication_config_with_sync_url() {
167 let config = ReplicationConfig::new("test.db").with_sync_url("http://sync");
168 assert_eq!(config.sync_url, Some("http://sync".to_string()));
169 }
170
171 #[test]
172 fn test_replication_config_with_auth_token() {
173 let config = ReplicationConfig::new("test.db").with_auth_token("token123");
174 assert_eq!(config.auth_token, Some("token123".to_string()));
175 }
176
177 #[test]
178 #[cfg(all(not(target_arch = "wasm32"), feature = "orm"))]
179 fn test_safe_pool_uninitialized() {
180 let pool = safe_pool();
182 assert!(pool.is_none());
183 }
184
185 #[test]
186 #[cfg(all(not(target_arch = "wasm32"), feature = "orm"))]
187 fn test_safe_driver_uninitialized() {
188 let driver = safe_driver();
189 assert!(driver.is_none());
190 }
191
192 #[test]
193 fn test_replication_manager_start() {
194 let config = ReplicationConfig::new("test.db")
195 .with_sync_url("https://sync.rullst.dev")
196 .with_sync_interval(1);
197 assert_eq!(
198 ReplicationManager::start(config),
199 Err(ReplicationError::Unsupported {
200 sync_url: "https://sync.rullst.dev".to_string(),
201 })
202 );
203 }
204
205 #[test]
206 fn test_replication_manager_start_no_url() {
207 let config = ReplicationConfig::new("test.db");
208 assert!(ReplicationManager::start(config).is_ok());
209 }
210}