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