1use thiserror::Error;
2
3use crate::data_source_builders::{DataSourceFactory, NullDataSourceBuilder};
4use crate::data_system_builders::{DataSystemBuilder, DataSystemFactory};
5
6#[cfg(any(
7 feature = "hyper-rustls-native-roots",
8 feature = "hyper-rustls-webpki-roots",
9 feature = "native-tls"
10))]
11use crate::events::processor_builders::EventProcessorBuilder;
12use crate::events::processor_builders::{EventProcessorFactory, NullEventProcessorBuilder};
13
14use crate::stores::store_builders::{DataStoreFactory, InMemoryDataStoreBuilder};
15use crate::ServiceEndpointsBuilder;
16#[cfg(any(
17 feature = "hyper-rustls-native-roots",
18 feature = "hyper-rustls-webpki-roots",
19 feature = "native-tls"
20))]
21use crate::StreamingDataSourceBuilder;
22
23use std::borrow::Borrow;
24
25#[derive(Debug)]
26struct Tag {
27 key: String,
28 value: String,
29}
30
31impl Tag {
32 fn is_valid(&self) -> Result<(), &str> {
33 if self.value.chars().count() > 64 {
34 return Err("Value was longer than 64 characters and was discarded");
35 }
36
37 if self.key.is_empty() || !self.key.chars().all(Tag::valid_characters) {
38 return Err("Key was empty or contained invalid characters");
39 }
40
41 if self.value.is_empty() || !self.value.chars().all(Tag::valid_characters) {
42 return Err("Value was empty or contained invalid characters");
43 }
44
45 Ok(())
46 }
47
48 fn valid_characters(c: char) -> bool {
49 c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_')
50 }
51}
52
53impl std::fmt::Display for Tag {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{}/{}", self.key, self.value)
56 }
57}
58
59pub struct ApplicationInfo {
64 tags: Vec<Tag>,
65}
66
67impl ApplicationInfo {
68 pub fn new() -> Self {
70 Self { tags: Vec::new() }
71 }
72
73 pub fn application_identifier(&mut self, application_id: impl Into<String>) -> &mut Self {
79 self.add_tag("application-id", application_id)
80 }
81
82 pub fn application_version(&mut self, application_version: impl Into<String>) -> &mut Self {
89 self.add_tag("application-version", application_version)
90 }
91
92 fn add_tag(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
93 let tag = Tag {
94 key: key.into(),
95 value: value.into(),
96 };
97
98 match tag.is_valid() {
99 Ok(_) => self.tags.push(tag),
100 Err(e) => {
101 warn!("{e}")
102 }
103 }
104
105 self
106 }
107
108 pub(crate) fn build(&self) -> Option<String> {
109 if self.tags.is_empty() {
110 return None;
111 }
112
113 let mut tags = self
114 .tags
115 .iter()
116 .map(|tag| tag.to_string())
117 .collect::<Vec<String>>();
118
119 tags.sort();
120 tags.dedup();
121
122 Some(tags.join(" "))
123 }
124}
125
126impl Default for ApplicationInfo {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132pub struct Config {
136 sdk_key: String,
137 service_endpoints_builder: ServiceEndpointsBuilder,
138 data_store_builder: Box<dyn DataStoreFactory>,
139 data_source_builder: Box<dyn DataSourceFactory>,
140 data_system_builder: Option<Box<dyn DataSystemFactory>>,
141 event_processor_builder: Box<dyn EventProcessorFactory>,
142 application_tag: Option<String>,
143 instance_id: String,
144 offline: bool,
145 daemon_mode: bool,
146}
147
148impl Config {
149 pub fn sdk_key(&self) -> &str {
151 &self.sdk_key
152 }
153
154 pub fn service_endpoints_builder(&self) -> &ServiceEndpointsBuilder {
156 &self.service_endpoints_builder
157 }
158
159 pub fn data_store_builder(&self) -> &dyn DataStoreFactory {
161 self.data_store_builder.borrow()
162 }
163
164 pub fn data_source_builder(&self) -> &dyn DataSourceFactory {
166 self.data_source_builder.borrow()
167 }
168
169 pub(crate) fn data_system_builder(&self) -> Option<&dyn DataSystemFactory> {
171 self.data_system_builder.as_deref()
172 }
173
174 pub fn event_processor_builder(&self) -> &dyn EventProcessorFactory {
176 self.event_processor_builder.borrow()
177 }
178
179 pub fn offline(&self) -> bool {
181 self.offline
182 }
183
184 pub fn daemon_mode(&self) -> bool {
186 self.daemon_mode
187 }
188
189 pub fn application_tag(&self) -> &Option<String> {
191 &self.application_tag
192 }
193
194 pub fn instance_id(&self) -> &str {
198 &self.instance_id
199 }
200}
201
202#[non_exhaustive]
204#[derive(Debug, Error)]
205pub enum BuildError {
206 #[error("config failed to build: {0}")]
208 InvalidConfig(String),
209}
210
211pub struct ConfigBuilder {
219 service_endpoints_builder: Option<ServiceEndpointsBuilder>,
220 data_store_builder: Option<Box<dyn DataStoreFactory>>,
221 data_source_builder: Option<Box<dyn DataSourceFactory>>,
222 data_system_builder: Option<Box<dyn DataSystemFactory>>,
223 event_processor_builder: Option<Box<dyn EventProcessorFactory>>,
224 application_info: Option<ApplicationInfo>,
225 offline: bool,
226 daemon_mode: bool,
227 sdk_key: String,
228}
229
230impl ConfigBuilder {
231 pub fn new(sdk_key: &str) -> Self {
233 Self {
234 service_endpoints_builder: None,
235 data_store_builder: None,
236 data_source_builder: None,
237 data_system_builder: None,
238 event_processor_builder: None,
239 offline: false,
240 daemon_mode: false,
241 application_info: None,
242 sdk_key: sdk_key.to_string(),
243 }
244 }
245
246 pub fn service_endpoints(mut self, builder: &ServiceEndpointsBuilder) -> Self {
248 self.service_endpoints_builder = Some(builder.clone());
249 self
250 }
251
252 pub fn data_store(mut self, builder: &dyn DataStoreFactory) -> Self {
257 self.data_store_builder = Some(builder.to_owned());
258 self
259 }
260
261 pub fn data_source(mut self, builder: &dyn DataSourceFactory) -> Self {
266 self.data_source_builder = Some(builder.to_owned());
267 self
268 }
269
270 pub fn data_system(mut self, builder: &DataSystemBuilder) -> Self {
275 let factory: Box<dyn DataSystemFactory> = Box::new(builder.clone());
276 self.data_system_builder = Some(factory);
277 self
278 }
279
280 pub fn event_processor(mut self, builder: &dyn EventProcessorFactory) -> Self {
285 self.event_processor_builder = Some(builder.to_owned());
286 self
287 }
288
289 pub fn offline(mut self, offline: bool) -> Self {
294 self.offline = offline;
295 self
296 }
297
298 pub fn daemon_mode(mut self, enable: bool) -> Self {
304 self.daemon_mode = enable;
305 self
306 }
307
308 pub fn application_info(mut self, application_info: ApplicationInfo) -> Self {
313 self.application_info = Some(application_info);
314 self
315 }
316
317 pub fn build(self) -> Result<Config, BuildError> {
319 let service_endpoints_builder = match &self.service_endpoints_builder {
320 None => ServiceEndpointsBuilder::new(),
321 Some(service_endpoints_builder) => service_endpoints_builder.clone(),
322 };
323
324 let data_store_builder = match &self.data_store_builder {
325 None => Box::new(InMemoryDataStoreBuilder::new()),
326 Some(_data_store_builder) => self.data_store_builder.unwrap(),
327 };
328
329 let data_system_builder = match self.data_system_builder {
332 Some(_) if self.offline => {
333 warn!("Custom data system builders will be ignored when in offline mode");
334 None
335 }
336 Some(_) if self.daemon_mode => {
337 warn!("Custom data system builders will be ignored when in daemon mode");
338 None
339 }
340 other => other,
341 };
342
343 let data_source_builder_result: Result<Box<dyn DataSourceFactory>, BuildError> =
344 match self.data_source_builder {
345 None if data_system_builder.is_some() => Ok(Box::new(NullDataSourceBuilder::new())),
346 Some(_) if data_system_builder.is_some() => {
347 warn!("Custom data source builders will be ignored when a data system is configured");
348 Ok(Box::new(NullDataSourceBuilder::new()))
349 }
350 None if self.offline => Ok(Box::new(NullDataSourceBuilder::new())),
351 Some(_) if self.offline => {
352 warn!("Custom data source builders will be ignored when in offline mode");
353 Ok(Box::new(NullDataSourceBuilder::new()))
354 }
355 None if self.daemon_mode => Ok(Box::new(NullDataSourceBuilder::new())),
356 Some(_) if self.daemon_mode => {
357 warn!("Custom data source builders will be ignored when in daemon mode");
358 Ok(Box::new(NullDataSourceBuilder::new()))
359 }
360 Some(builder) => Ok(builder),
361 #[cfg(any(
362 feature = "hyper-rustls-native-roots",
363 feature = "hyper-rustls-webpki-roots",
364 feature = "native-tls"
365 ))]
366 None => {
367 let transport = launchdarkly_sdk_transport::HyperTransport::new_https()
368 .map_err(|e| {
369 BuildError::InvalidConfig(format!(
370 "failed to create default transport: {}",
371 e
372 ))
373 })?;
374 let mut builder = StreamingDataSourceBuilder::new();
375 builder.transport(transport);
376 Ok(Box::new(builder))
377 }
378 #[cfg(not(any(
379 feature = "hyper-rustls-native-roots",
380 feature = "hyper-rustls-webpki-roots",
381 feature = "native-tls"
382 )))]
383 None => Err(BuildError::InvalidConfig(
384 "data source builder required when hyper-rustls-native-roots, hyper-rustls-webpki-roots, or native-tls features are disabled".into(),
385 )),
386 };
387 let data_source_builder = data_source_builder_result?;
388
389 let event_processor_builder_result: Result<Box<dyn EventProcessorFactory>, BuildError> =
390 match self.event_processor_builder {
391 None if self.offline => Ok(Box::new(NullEventProcessorBuilder::new())),
392 Some(_) if self.offline => {
393 warn!("Custom event processor builders will be ignored when in offline mode");
394 Ok(Box::new(NullEventProcessorBuilder::new()))
395 }
396 Some(builder) => Ok(builder),
397 #[cfg(any(
398 feature = "hyper-rustls-native-roots",
399 feature = "hyper-rustls-webpki-roots",
400 feature = "native-tls"
401 ))]
402 None => {
403 let transport = launchdarkly_sdk_transport::HyperTransport::new_https()
404 .map_err(|e| {
405 BuildError::InvalidConfig(format!(
406 "failed to create default transport: {}",
407 e
408 ))
409 })?;
410 let mut builder = EventProcessorBuilder::new();
411 builder.transport(transport);
412 Ok(Box::new(builder))
413 }
414 #[cfg(not(any(
415 feature = "hyper-rustls-native-roots",
416 feature = "hyper-rustls-webpki-roots",
417 feature = "native-tls"
418 )))]
419 None => Err(BuildError::InvalidConfig(
420 "event processor factory required when hyper-rustls-native-roots, hyper-rustls-webpki-roots, or native-tls features are disabled".into(),
421 )),
422 };
423 let event_processor_builder = event_processor_builder_result?;
424
425 let application_tag = match self.application_info {
426 Some(tb) => tb.build(),
427 _ => None,
428 };
429
430 let instance_id = uuid::Uuid::new_v4().to_string();
436
437 Ok(Config {
438 sdk_key: self.sdk_key,
439 service_endpoints_builder,
440 data_store_builder,
441 data_source_builder,
442 data_system_builder,
443 event_processor_builder,
444 application_tag,
445 instance_id,
446 offline: self.offline,
447 daemon_mode: self.daemon_mode,
448 })
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use test_case::test_case;
455
456 use super::*;
457
458 #[test]
459 fn client_configured_with_custom_endpoints() {
460 let builder = ConfigBuilder::new("sdk-key").service_endpoints(
461 ServiceEndpointsBuilder::new().relay_proxy("http://my-relay-hostname:8080"),
462 );
463
464 let endpoints = builder.service_endpoints_builder.unwrap().build().unwrap();
465 assert_eq!(
466 endpoints.streaming_base_url(),
467 "http://my-relay-hostname:8080"
468 );
469 assert_eq!(
470 endpoints.polling_base_url(),
471 "http://my-relay-hostname:8080"
472 );
473 assert_eq!(endpoints.events_base_url(), "http://my-relay-hostname:8080");
474 }
475
476 #[test]
477 #[cfg(any(
478 feature = "hyper-rustls-native-roots",
479 feature = "hyper-rustls-webpki-roots",
480 feature = "native-tls"
481 ))]
482 fn unconfigured_config_builder_handles_application_tags_correctly() {
483 let builder = ConfigBuilder::new("sdk-key");
484 let config = builder.build().expect("config should build");
485
486 assert_eq!(None, config.application_tag);
487 }
488
489 #[test]
490 #[cfg(any(
491 feature = "hyper-rustls-native-roots",
492 feature = "hyper-rustls-webpki-roots",
493 feature = "native-tls"
494 ))]
495 fn instance_id_is_a_uuid_v4() {
496 let config = ConfigBuilder::new("sdk-key")
497 .build()
498 .expect("config should build");
499
500 let parsed = uuid::Uuid::parse_str(config.instance_id())
501 .expect("instance id should be a parseable UUID");
502 assert_eq!(
503 uuid::Version::Random,
504 parsed.get_version().expect("uuid should have a version"),
505 "instance id must be UUID v4"
506 );
507 }
508
509 #[test]
510 #[cfg(any(
511 feature = "hyper-rustls-native-roots",
512 feature = "hyper-rustls-webpki-roots",
513 feature = "native-tls"
514 ))]
515 fn instance_id_is_unique_per_config() {
516 let c1 = ConfigBuilder::new("sdk-key")
519 .build()
520 .expect("config should build");
521 let c2 = ConfigBuilder::new("sdk-key")
522 .build()
523 .expect("config should build");
524 assert!(!c1.instance_id().is_empty());
525 assert!(!c2.instance_id().is_empty());
526 assert_ne!(
527 c1.instance_id(),
528 c2.instance_id(),
529 "each SDK instance should generate its own instance id"
530 );
531 }
532
533 #[test_case("id", "version", Some("application-id/id application-version/version".to_string()))]
534 #[test_case("Invalid id", "version", Some("application-version/version".to_string()))]
535 #[test_case("id", "Invalid version", Some("application-id/id".to_string()))]
536 #[test_case("Invalid id", "Invalid version", None)]
537 #[cfg(any(
538 feature = "hyper-rustls-native-roots",
539 feature = "hyper-rustls-webpki-roots",
540 feature = "native-tls"
541 ))]
542 fn config_builder_handles_application_tags_appropriately(
543 id: impl Into<String>,
544 version: impl Into<String>,
545 expected: Option<String>,
546 ) {
547 let mut application_info = ApplicationInfo::new();
548 application_info
549 .application_identifier(id)
550 .application_version(version);
551 let builder = ConfigBuilder::new("sdk-key");
552 let config = builder
553 .application_info(application_info)
554 .build()
555 .expect("config should build");
556
557 assert_eq!(expected, config.application_tag);
558 }
559
560 #[test_case("", "abc", Err("Key was empty or contained invalid characters"); "Empty key")]
561 #[test_case(" ", "abc", Err("Key was empty or contained invalid characters"); "Key with whitespace")]
562 #[test_case("/", "abc", Err("Key was empty or contained invalid characters"); "Key with slash")]
563 #[test_case(":", "abc", Err("Key was empty or contained invalid characters"); "Key with colon")]
564 #[test_case("🦀", "abc", Err("Key was empty or contained invalid characters"); "Key with emoji")]
565 #[test_case("abcABC123.-_", "abc", Ok(()); "Valid key")]
566 #[test_case("abc", "", Err("Value was empty or contained invalid characters"); "Empty value")]
567 #[test_case("abc", " ", Err("Value was empty or contained invalid characters"); "Value with whitespace")]
568 #[test_case("abc", "/", Err("Value was empty or contained invalid characters"); "Value with slash")]
569 #[test_case("abc", ":", Err("Value was empty or contained invalid characters"); "Value with colon")]
570 #[test_case("abc", "🦀", Err("Value was empty or contained invalid characters"); "Value with emoji")]
571 #[test_case("abc", "abcABC123.-_", Ok(()); "Valid value")]
572 #[test_case("abc", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl", Ok(()); "64 is the max length")]
573 #[test_case("abc", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm", Err("Value was longer than 64 characters and was discarded"); "65 is too far")]
574 fn tag_can_determine_valid_values(key: &str, value: &str, expected_result: Result<(), &str>) {
575 let tag = Tag {
576 key: key.to_string(),
577 value: value.to_string(),
578 };
579 assert_eq!(expected_result, tag.is_valid());
580 }
581
582 #[test_case(vec![], None; "No tags returns None")]
583 #[test_case(vec![("application-id".into(), "gonfalon-be".into()), ("application-sha".into(), "abcdef".into())], Some("application-id/gonfalon-be application-sha/abcdef".into()); "Tags are formatted correctly")]
584 #[test_case(vec![("key".into(), "xyz".into()), ("key".into(), "abc".into())], Some("key/abc key/xyz".into()); "Keys are ordered correctly")]
585 #[test_case(vec![("key".into(), "abc".into()), ("key".into(), "abc".into())], Some("key/abc".into()); "Tags are deduped")]
586 #[test_case(vec![("XYZ".into(), "xyz".into()), ("abc".into(), "abc".into())], Some("XYZ/xyz abc/abc".into()); "Keys are ascii sorted correctly")]
587 #[test_case(vec![("abc".into(), "XYZ".into()), ("abc".into(), "abc".into())], Some("abc/XYZ abc/abc".into()); "Values are ascii sorted correctly")]
588 #[test_case(vec![("".into(), "XYZ".into()), ("abc".into(), "xyz".into())], Some("abc/xyz".into()); "Invalid tags are filtered")]
589 #[test_case(Vec::new(), None; "Empty tags returns None")]
590 fn application_tag_builder_can_create_tag_string_correctly(
591 tags: Vec<(String, String)>,
592 expected_value: Option<String>,
593 ) {
594 let mut application_info = ApplicationInfo::new();
595
596 tags.into_iter().for_each(|(key, value)| {
597 application_info.add_tag(key, value);
598 });
599
600 assert_eq!(expected_value, application_info.build());
601 }
602}