1use std::sync::Arc;
2use std::time::Duration;
3
4use launchdarkly_sdk_transport::{HttpTransport, HyperTransport};
5use thiserror::Error;
6
7use crate::data_source_builders::{DataSourceFactory, PollingDataSourceBuilder};
8use crate::data_system::DataSystem;
9use crate::fdv2::data_system::{FDv2DataSystem, InitializerFactory, SynchronizerFactory};
10use crate::fdv2::fdv1_adapter::FDv1AdapterFactory;
11use crate::fdv2::polling::{PollingInitializerFactory, PollingSynchronizerFactory};
12use crate::fdv2::request_headers::RequestHeaders;
13use crate::fdv2::streaming::StreamingSynchronizerFactory;
14use crate::service_endpoints::ServiceEndpoints;
15
16const DEFAULT_INITIAL_RECONNECT_DELAY: Duration = Duration::from_secs(1);
17const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(30);
18const DEFAULT_FALLBACK_TIMEOUT: Duration = Duration::from_secs(120);
19const DEFAULT_RECOVERY_TIMEOUT: Duration = Duration::from_secs(300);
20
21#[non_exhaustive]
23#[derive(Debug, Error)]
24pub enum BuildError {
25 #[error("data system config failed to build: {0}")]
27 InvalidConfig(String),
28}
29
30#[non_exhaustive]
32pub struct DataSourceBuildContext<'a> {
33 pub endpoints: &'a ServiceEndpoints,
35 pub headers: &'a RequestHeaders,
37}
38
39pub trait FDv2SynchronizerConfig {
41 fn build_synchronizer(
43 &self,
44 context: &DataSourceBuildContext,
45 ) -> Result<Box<dyn SynchronizerFactory>, BuildError>;
46
47 fn to_owned(&self) -> Box<dyn FDv2SynchronizerConfig>;
49}
50
51pub trait FDv2InitializerConfig {
53 fn build_initializer(
55 &self,
56 context: &DataSourceBuildContext,
57 ) -> Result<Box<dyn InitializerFactory>, BuildError>;
58
59 fn to_owned(&self) -> Box<dyn FDv2InitializerConfig>;
61}
62
63fn default_https_transport() -> Result<impl HttpTransport + 'static, BuildError> {
65 #[cfg(any(
66 feature = "hyper-rustls-native-roots",
67 feature = "hyper-rustls-webpki-roots",
68 feature = "native-tls"
69 ))]
70 {
71 HyperTransport::new_https().map_err(|e| {
72 BuildError::InvalidConfig(format!("failed to create default https transport: {e:?}"))
73 })
74 }
75 #[cfg(not(any(
76 feature = "hyper-rustls-native-roots",
77 feature = "hyper-rustls-webpki-roots",
78 feature = "native-tls"
79 )))]
80 {
81 Err::<HyperTransport, _>(BuildError::InvalidConfig(
82 "https connector required when hyper-rustls-native-roots, hyper-rustls-webpki-roots, or native-tls features are disabled".into(),
83 ))
84 }
85}
86
87#[derive(Clone)]
89pub struct FDv2StreamingBuilder<T: HttpTransport = HyperTransport> {
90 initial_reconnect_delay: Duration,
91 base_url: Option<String>,
92 transport: Option<T>,
93}
94
95impl<T: HttpTransport + Clone + Send + Sync + 'static> FDv2StreamingBuilder<T> {
96 pub fn new() -> Self {
98 Self {
99 initial_reconnect_delay: DEFAULT_INITIAL_RECONNECT_DELAY,
100 base_url: None,
101 transport: None,
102 }
103 }
104
105 pub fn initial_reconnect_delay(&mut self, duration: Duration) -> &mut Self {
118 self.initial_reconnect_delay = duration;
119 self
120 }
121
122 pub fn base_url(&mut self, url: &str) -> &mut Self {
134 self.base_url = Some(url.to_string());
135 self
136 }
137
138 pub fn transport(&mut self, transport: T) -> &mut Self {
140 self.transport = Some(transport);
141 self
142 }
143}
144
145impl<T: HttpTransport + Clone + Send + Sync + 'static> FDv2SynchronizerConfig
146 for FDv2StreamingBuilder<T>
147{
148 fn build_synchronizer(
149 &self,
150 context: &DataSourceBuildContext,
151 ) -> Result<Box<dyn SynchronizerFactory>, BuildError> {
152 let base_url = self
153 .base_url
154 .clone()
155 .unwrap_or_else(|| context.endpoints.streaming_base_url().to_string());
156 let factory: Box<dyn SynchronizerFactory> = match &self.transport {
157 Some(transport) => Box::new(StreamingSynchronizerFactory::new(
158 transport.clone(),
159 base_url,
160 context.headers.clone(),
161 self.initial_reconnect_delay,
162 )),
163 None => Box::new(StreamingSynchronizerFactory::new(
164 default_https_transport()?,
165 base_url,
166 context.headers.clone(),
167 self.initial_reconnect_delay,
168 )),
169 };
170 Ok(factory)
171 }
172
173 fn to_owned(&self) -> Box<dyn FDv2SynchronizerConfig> {
174 Box::new(self.clone())
175 }
176}
177
178impl<T: HttpTransport + Clone + Send + Sync + 'static> Default for FDv2StreamingBuilder<T> {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184#[derive(Clone)]
186pub struct FDv2PollingBuilder<T: HttpTransport = HyperTransport> {
187 poll_interval: Duration,
188 base_url: Option<String>,
189 transport: Option<T>,
190}
191
192impl<T: HttpTransport + Clone + Send + Sync + 'static> FDv2PollingBuilder<T> {
193 pub fn new() -> Self {
195 Self {
196 poll_interval: DEFAULT_POLL_INTERVAL,
197 base_url: None,
198 transport: None,
199 }
200 }
201
202 pub fn poll_interval(&mut self, poll_interval: Duration) -> &mut Self {
215 self.poll_interval = poll_interval;
216 self
217 }
218
219 pub fn base_url(&mut self, url: &str) -> &mut Self {
231 self.base_url = Some(url.to_string());
232 self
233 }
234
235 pub fn transport(&mut self, transport: T) -> &mut Self {
237 self.transport = Some(transport);
238 self
239 }
240}
241
242impl<T: HttpTransport + Clone + Send + Sync + 'static> FDv2SynchronizerConfig
243 for FDv2PollingBuilder<T>
244{
245 fn build_synchronizer(
246 &self,
247 context: &DataSourceBuildContext,
248 ) -> Result<Box<dyn SynchronizerFactory>, BuildError> {
249 let base_url = self
250 .base_url
251 .clone()
252 .unwrap_or_else(|| context.endpoints.polling_base_url().to_string());
253 let factory: Box<dyn SynchronizerFactory> = match &self.transport {
254 Some(transport) => Box::new(PollingSynchronizerFactory::new(
255 transport.clone(),
256 base_url,
257 context.headers.clone(),
258 self.poll_interval,
259 )),
260 None => Box::new(PollingSynchronizerFactory::new(
261 default_https_transport()?,
262 base_url,
263 context.headers.clone(),
264 self.poll_interval,
265 )),
266 };
267 Ok(factory)
268 }
269
270 fn to_owned(&self) -> Box<dyn FDv2SynchronizerConfig> {
271 Box::new(self.clone())
272 }
273}
274
275impl<T: HttpTransport + Clone + Send + Sync + 'static> FDv2InitializerConfig
276 for FDv2PollingBuilder<T>
277{
278 fn build_initializer(
279 &self,
280 context: &DataSourceBuildContext,
281 ) -> Result<Box<dyn InitializerFactory>, BuildError> {
282 let base_url = self
283 .base_url
284 .clone()
285 .unwrap_or_else(|| context.endpoints.polling_base_url().to_string());
286 let factory: Box<dyn InitializerFactory> = match &self.transport {
287 Some(transport) => Box::new(PollingInitializerFactory::new(
288 transport.clone(),
289 base_url,
290 context.headers.clone(),
291 )),
292 None => Box::new(PollingInitializerFactory::new(
293 default_https_transport()?,
294 base_url,
295 context.headers.clone(),
296 )),
297 };
298 Ok(factory)
299 }
300
301 fn to_owned(&self) -> Box<dyn FDv2InitializerConfig> {
302 Box::new(self.clone())
303 }
304}
305
306impl<T: HttpTransport + Clone + Send + Sync + 'static> Default for FDv2PollingBuilder<T> {
307 fn default() -> Self {
308 Self::new()
309 }
310}
311
312pub struct DataSystemBuilder {
338 initializers: Vec<Box<dyn FDv2InitializerConfig>>,
339 synchronizers: Vec<Box<dyn FDv2SynchronizerConfig>>,
340 fdv1_fallback: Option<Box<dyn DataSourceFactory>>,
341}
342
343impl Clone for DataSystemBuilder {
344 fn clone(&self) -> Self {
345 Self {
346 initializers: self.initializers.iter().map(|c| (**c).to_owned()).collect(),
347 synchronizers: self
348 .synchronizers
349 .iter()
350 .map(|c| (**c).to_owned())
351 .collect(),
352 fdv1_fallback: self.fdv1_fallback.as_ref().map(|f| (**f).to_owned()),
353 }
354 }
355}
356
357impl DataSystemBuilder {
358 pub fn custom() -> Self {
360 Self {
361 initializers: Vec::new(),
362 synchronizers: Vec::new(),
363 fdv1_fallback: None,
364 }
365 }
366
367 pub fn initializer(&mut self, source: impl FDv2InitializerConfig + 'static) -> &mut Self {
369 self.initializers.push(Box::new(source));
370 self
371 }
372
373 pub fn synchronizer(&mut self, source: impl FDv2SynchronizerConfig + 'static) -> &mut Self {
375 self.synchronizers.push(Box::new(source));
376 self
377 }
378
379 pub fn fdv1_fallback(&mut self, factory: &dyn DataSourceFactory) -> &mut Self {
381 self.fdv1_fallback = Some(factory.to_owned());
382 self
383 }
384
385 pub fn disable_fdv1_fallback(&mut self) -> &mut Self {
387 self.fdv1_fallback = None;
388 self
389 }
390}
391
392impl Default for DataSystemBuilder {
393 fn default() -> Self {
395 let mut builder = Self::custom();
396 builder.initializer(FDv2PollingBuilder::<HyperTransport>::new());
397 builder.synchronizer(FDv2StreamingBuilder::<HyperTransport>::new());
398 builder.synchronizer(FDv2PollingBuilder::<HyperTransport>::new());
399 builder.fdv1_fallback(&PollingDataSourceBuilder::<HyperTransport>::new());
400 builder
401 }
402}
403
404pub(crate) trait DataSystemFactory {
406 fn build(
407 &self,
408 endpoints: &ServiceEndpoints,
409 sdk_key: &str,
410 tags: Option<&str>,
411 instance_id: &str,
412 ) -> Result<Arc<dyn DataSystem>, BuildError>;
413}
414
415impl DataSystemFactory for DataSystemBuilder {
416 fn build(
417 &self,
418 endpoints: &ServiceEndpoints,
419 sdk_key: &str,
420 tags: Option<&str>,
421 instance_id: &str,
422 ) -> Result<Arc<dyn DataSystem>, BuildError> {
423 let headers = RequestHeaders::new(sdk_key, tags, instance_id);
424 let context = DataSourceBuildContext {
425 endpoints,
426 headers: &headers,
427 };
428
429 let initializer_factories: Vec<Arc<dyn InitializerFactory>> = self
430 .initializers
431 .iter()
432 .map(|c| c.build_initializer(&context).map(Arc::from))
433 .collect::<Result<_, _>>()?;
434
435 let mut synchronizer_factories: Vec<Arc<dyn SynchronizerFactory>> = self
436 .synchronizers
437 .iter()
438 .map(|c| c.build_synchronizer(&context).map(Arc::from))
439 .collect::<Result<_, _>>()?;
440
441 if let Some(fdv1_factory) = &self.fdv1_fallback {
444 let mut fdv1_factory = (**fdv1_factory).to_owned();
445 fdv1_factory.set_instance_id(instance_id.to_string());
446 let source = fdv1_factory
447 .build(endpoints, sdk_key, tags.map(|t| t.to_string()))
448 .map_err(|e| {
449 BuildError::InvalidConfig(format!("failed to build FDv1 fallback source: {e}"))
450 })?;
451 let adapter = FDv1AdapterFactory::new(Box::new(move || source.clone()));
452 synchronizer_factories.push(Arc::new(adapter));
453 }
454
455 let system: Arc<dyn DataSystem> = Arc::new(FDv2DataSystem::new(
456 initializer_factories,
457 synchronizer_factories,
458 DEFAULT_FALLBACK_TIMEOUT,
459 DEFAULT_RECOVERY_TIMEOUT,
460 ));
461 Ok(system)
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use bytes::Bytes;
468 use launchdarkly_sdk_transport::{Request, ResponseFuture};
469
470 use super::*;
471
472 #[test]
473 fn custom_starts_empty() {
474 let builder = DataSystemBuilder::custom();
475
476 assert!(builder.initializers.is_empty());
477 assert!(builder.synchronizers.is_empty());
478 assert!(builder.fdv1_fallback.is_none());
479 }
480
481 #[test]
482 fn default_has_recommended_sources() {
483 let builder = DataSystemBuilder::default();
484
485 assert_eq!(builder.initializers.len(), 1);
486 assert_eq!(builder.synchronizers.len(), 2);
487 assert!(builder.fdv1_fallback.is_some());
488 }
489
490 #[test]
491 fn disable_fdv1_fallback_clears_it() {
492 let mut builder = DataSystemBuilder::default();
493 assert!(builder.fdv1_fallback.is_some());
494
495 builder.disable_fdv1_fallback();
496
497 assert!(builder.fdv1_fallback.is_none());
498 }
499
500 #[derive(Debug, Clone)]
501 struct TestTransport;
502
503 impl HttpTransport for TestTransport {
504 fn request(&self, _request: Request<Option<Bytes>>) -> ResponseFuture {
505 unreachable!();
506 }
507 }
508
509 #[test]
510 fn builders_build_factories_with_injected_transport() {
511 let endpoints = crate::ServiceEndpointsBuilder::new().build().unwrap();
512 let headers = RequestHeaders::new("sdk-key", None, "test-instance");
513 let context = DataSourceBuildContext {
514 endpoints: &endpoints,
515 headers: &headers,
516 };
517
518 assert!(FDv2StreamingBuilder::<TestTransport>::new()
520 .transport(TestTransport)
521 .build_synchronizer(&context)
522 .is_ok());
523 assert!(FDv2PollingBuilder::<TestTransport>::new()
524 .transport(TestTransport)
525 .build_synchronizer(&context)
526 .is_ok());
527 assert!(FDv2PollingBuilder::<TestTransport>::new()
528 .transport(TestTransport)
529 .build_initializer(&context)
530 .is_ok());
531 }
532
533 #[test]
536 #[cfg(any(
537 feature = "hyper-rustls-native-roots",
538 feature = "hyper-rustls-webpki-roots",
539 feature = "native-tls"
540 ))]
541 fn builders_build_factories_with_default_transport() {
542 let endpoints = crate::ServiceEndpointsBuilder::new().build().unwrap();
543 let headers = RequestHeaders::new("sdk-key", None, "test-instance");
544 let context = DataSourceBuildContext {
545 endpoints: &endpoints,
546 headers: &headers,
547 };
548
549 assert!(FDv2StreamingBuilder::<HyperTransport>::new()
551 .build_synchronizer(&context)
552 .is_ok());
553 assert!(FDv2PollingBuilder::<HyperTransport>::new()
554 .build_synchronizer(&context)
555 .is_ok());
556 assert!(FDv2PollingBuilder::<HyperTransport>::new()
557 .build_initializer(&context)
558 .is_ok());
559 }
560}