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 offline: bool,
142 daemon_mode: bool,
143}
144
145impl Config {
146 pub fn sdk_key(&self) -> &str {
148 &self.sdk_key
149 }
150
151 pub fn service_endpoints_builder(&self) -> &ServiceEndpointsBuilder {
153 &self.service_endpoints_builder
154 }
155
156 pub fn data_store_builder(&self) -> &dyn DataStoreFactory {
158 self.data_store_builder.borrow()
159 }
160
161 pub fn data_source_builder(&self) -> &dyn DataSourceFactory {
163 self.data_source_builder.borrow()
164 }
165
166 pub fn event_processor_builder(&self) -> &dyn EventProcessorFactory {
168 self.event_processor_builder.borrow()
169 }
170
171 pub fn offline(&self) -> bool {
173 self.offline
174 }
175
176 pub fn daemon_mode(&self) -> bool {
178 self.daemon_mode
179 }
180
181 pub fn application_tag(&self) -> &Option<String> {
183 &self.application_tag
184 }
185}
186
187#[non_exhaustive]
189#[derive(Debug, Error)]
190pub enum BuildError {
191 #[error("config failed to build: {0}")]
193 InvalidConfig(String),
194}
195
196pub struct ConfigBuilder {
204 service_endpoints_builder: Option<ServiceEndpointsBuilder>,
205 data_store_builder: Option<Box<dyn DataStoreFactory>>,
206 data_source_builder: Option<Box<dyn DataSourceFactory>>,
207 event_processor_builder: Option<Box<dyn EventProcessorFactory>>,
208 application_info: Option<ApplicationInfo>,
209 offline: bool,
210 daemon_mode: bool,
211 sdk_key: String,
212}
213
214impl ConfigBuilder {
215 pub fn new(sdk_key: &str) -> Self {
217 Self {
218 service_endpoints_builder: None,
219 data_store_builder: None,
220 data_source_builder: None,
221 event_processor_builder: None,
222 offline: false,
223 daemon_mode: false,
224 application_info: None,
225 sdk_key: sdk_key.to_string(),
226 }
227 }
228
229 pub fn service_endpoints(mut self, builder: &ServiceEndpointsBuilder) -> Self {
231 self.service_endpoints_builder = Some(builder.clone());
232 self
233 }
234
235 pub fn data_store(mut self, builder: &dyn DataStoreFactory) -> Self {
240 self.data_store_builder = Some(builder.to_owned());
241 self
242 }
243
244 pub fn data_source(mut self, builder: &dyn DataSourceFactory) -> Self {
249 self.data_source_builder = Some(builder.to_owned());
250 self
251 }
252
253 pub fn event_processor(mut self, builder: &dyn EventProcessorFactory) -> Self {
258 self.event_processor_builder = Some(builder.to_owned());
259 self
260 }
261
262 pub fn offline(mut self, offline: bool) -> Self {
267 self.offline = offline;
268 self
269 }
270
271 pub fn daemon_mode(mut self, enable: bool) -> Self {
277 self.daemon_mode = enable;
278 self
279 }
280
281 pub fn application_info(mut self, application_info: ApplicationInfo) -> Self {
286 self.application_info = Some(application_info);
287 self
288 }
289
290 pub fn build(self) -> Result<Config, BuildError> {
292 let service_endpoints_builder = match &self.service_endpoints_builder {
293 None => ServiceEndpointsBuilder::new(),
294 Some(service_endpoints_builder) => service_endpoints_builder.clone(),
295 };
296
297 let data_store_builder = match &self.data_store_builder {
298 None => Box::new(InMemoryDataStoreBuilder::new()),
299 Some(_data_store_builder) => self.data_store_builder.unwrap(),
300 };
301
302 let data_source_builder_result: Result<Box<dyn DataSourceFactory>, BuildError> =
303 match self.data_source_builder {
304 None if self.offline => Ok(Box::new(NullDataSourceBuilder::new())),
305 Some(_) if self.offline => {
306 warn!("Custom data source builders will be ignored when in offline mode");
307 Ok(Box::new(NullDataSourceBuilder::new()))
308 }
309 None if self.daemon_mode => Ok(Box::new(NullDataSourceBuilder::new())),
310 Some(_) if self.daemon_mode => {
311 warn!("Custom data source builders will be ignored when in daemon mode");
312 Ok(Box::new(NullDataSourceBuilder::new()))
313 }
314 Some(builder) => Ok(builder),
315 #[cfg(any(
316 feature = "hyper-rustls-native-roots",
317 feature = "hyper-rustls-webpki-roots",
318 feature = "native-tls"
319 ))]
320 None => {
321 let transport = launchdarkly_sdk_transport::HyperTransport::new_https()
322 .map_err(|e| {
323 BuildError::InvalidConfig(format!(
324 "failed to create default transport: {}",
325 e
326 ))
327 })?;
328 let mut builder = StreamingDataSourceBuilder::new();
329 builder.transport(transport);
330 Ok(Box::new(builder))
331 }
332 #[cfg(not(any(
333 feature = "hyper-rustls-native-roots",
334 feature = "hyper-rustls-webpki-roots",
335 feature = "native-tls"
336 )))]
337 None => Err(BuildError::InvalidConfig(
338 "data source builder required when hyper-rustls-native-roots, hyper-rustls-webpki-roots, or native-tls features are disabled".into(),
339 )),
340 };
341 let data_source_builder = data_source_builder_result?;
342
343 let event_processor_builder_result: Result<Box<dyn EventProcessorFactory>, BuildError> =
344 match self.event_processor_builder {
345 None if self.offline => Ok(Box::new(NullEventProcessorBuilder::new())),
346 Some(_) if self.offline => {
347 warn!("Custom event processor builders will be ignored when in offline mode");
348 Ok(Box::new(NullEventProcessorBuilder::new()))
349 }
350 Some(builder) => Ok(builder),
351 #[cfg(any(
352 feature = "hyper-rustls-native-roots",
353 feature = "hyper-rustls-webpki-roots",
354 feature = "native-tls"
355 ))]
356 None => {
357 let transport = launchdarkly_sdk_transport::HyperTransport::new_https()
358 .map_err(|e| {
359 BuildError::InvalidConfig(format!(
360 "failed to create default transport: {}",
361 e
362 ))
363 })?;
364 let mut builder = EventProcessorBuilder::new();
365 builder.transport(transport);
366 Ok(Box::new(builder))
367 }
368 #[cfg(not(any(
369 feature = "hyper-rustls-native-roots",
370 feature = "hyper-rustls-webpki-roots",
371 feature = "native-tls"
372 )))]
373 None => Err(BuildError::InvalidConfig(
374 "event processor factory required when hyper-rustls-native-roots, hyper-rustls-webpki-roots, or native-tls features are disabled".into(),
375 )),
376 };
377 let event_processor_builder = event_processor_builder_result?;
378
379 let application_tag = match self.application_info {
380 Some(tb) => tb.build(),
381 _ => None,
382 };
383
384 Ok(Config {
385 sdk_key: self.sdk_key,
386 service_endpoints_builder,
387 data_store_builder,
388 data_source_builder,
389 event_processor_builder,
390 application_tag,
391 offline: self.offline,
392 daemon_mode: self.daemon_mode,
393 })
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use test_case::test_case;
400
401 use super::*;
402
403 #[test]
404 fn client_configured_with_custom_endpoints() {
405 let builder = ConfigBuilder::new("sdk-key").service_endpoints(
406 ServiceEndpointsBuilder::new().relay_proxy("http://my-relay-hostname:8080"),
407 );
408
409 let endpoints = builder.service_endpoints_builder.unwrap().build().unwrap();
410 assert_eq!(
411 endpoints.streaming_base_url(),
412 "http://my-relay-hostname:8080"
413 );
414 assert_eq!(
415 endpoints.polling_base_url(),
416 "http://my-relay-hostname:8080"
417 );
418 assert_eq!(endpoints.events_base_url(), "http://my-relay-hostname:8080");
419 }
420
421 #[test]
422 #[cfg(any(
423 feature = "hyper-rustls-native-roots",
424 feature = "hyper-rustls-webpki-roots",
425 feature = "native-tls"
426 ))]
427 fn unconfigured_config_builder_handles_application_tags_correctly() {
428 let builder = ConfigBuilder::new("sdk-key");
429 let config = builder.build().expect("config should build");
430
431 assert_eq!(None, config.application_tag);
432 }
433
434 #[test_case("id", "version", Some("application-id/id application-version/version".to_string()))]
435 #[test_case("Invalid id", "version", Some("application-version/version".to_string()))]
436 #[test_case("id", "Invalid version", Some("application-id/id".to_string()))]
437 #[test_case("Invalid id", "Invalid version", None)]
438 #[cfg(any(
439 feature = "hyper-rustls-native-roots",
440 feature = "hyper-rustls-webpki-roots",
441 feature = "native-tls"
442 ))]
443 fn config_builder_handles_application_tags_appropriately(
444 id: impl Into<String>,
445 version: impl Into<String>,
446 expected: Option<String>,
447 ) {
448 let mut application_info = ApplicationInfo::new();
449 application_info
450 .application_identifier(id)
451 .application_version(version);
452 let builder = ConfigBuilder::new("sdk-key");
453 let config = builder
454 .application_info(application_info)
455 .build()
456 .expect("config should build");
457
458 assert_eq!(expected, config.application_tag);
459 }
460
461 #[test_case("", "abc", Err("Key was empty or contained invalid characters"); "Empty key")]
462 #[test_case(" ", "abc", Err("Key was empty or contained invalid characters"); "Key with whitespace")]
463 #[test_case("/", "abc", Err("Key was empty or contained invalid characters"); "Key with slash")]
464 #[test_case(":", "abc", Err("Key was empty or contained invalid characters"); "Key with colon")]
465 #[test_case("🦀", "abc", Err("Key was empty or contained invalid characters"); "Key with emoji")]
466 #[test_case("abcABC123.-_", "abc", Ok(()); "Valid key")]
467 #[test_case("abc", "", Err("Value was empty or contained invalid characters"); "Empty value")]
468 #[test_case("abc", " ", Err("Value was empty or contained invalid characters"); "Value with whitespace")]
469 #[test_case("abc", "/", Err("Value was empty or contained invalid characters"); "Value with slash")]
470 #[test_case("abc", ":", Err("Value was empty or contained invalid characters"); "Value with colon")]
471 #[test_case("abc", "🦀", Err("Value was empty or contained invalid characters"); "Value with emoji")]
472 #[test_case("abc", "abcABC123.-_", Ok(()); "Valid value")]
473 #[test_case("abc", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl", Ok(()); "64 is the max length")]
474 #[test_case("abc", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm", Err("Value was longer than 64 characters and was discarded"); "65 is too far")]
475 fn tag_can_determine_valid_values(key: &str, value: &str, expected_result: Result<(), &str>) {
476 let tag = Tag {
477 key: key.to_string(),
478 value: value.to_string(),
479 };
480 assert_eq!(expected_result, tag.is_valid());
481 }
482
483 #[test_case(vec![], None; "No tags returns None")]
484 #[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")]
485 #[test_case(vec![("key".into(), "xyz".into()), ("key".into(), "abc".into())], Some("key/abc key/xyz".into()); "Keys are ordered correctly")]
486 #[test_case(vec![("key".into(), "abc".into()), ("key".into(), "abc".into())], Some("key/abc".into()); "Tags are deduped")]
487 #[test_case(vec![("XYZ".into(), "xyz".into()), ("abc".into(), "abc".into())], Some("XYZ/xyz abc/abc".into()); "Keys are ascii sorted correctly")]
488 #[test_case(vec![("abc".into(), "XYZ".into()), ("abc".into(), "abc".into())], Some("abc/XYZ abc/abc".into()); "Values are ascii sorted correctly")]
489 #[test_case(vec![("".into(), "XYZ".into()), ("abc".into(), "xyz".into())], Some("abc/xyz".into()); "Invalid tags are filtered")]
490 #[test_case(Vec::new(), None; "Empty tags returns None")]
491 fn application_tag_builder_can_create_tag_string_correctly(
492 tags: Vec<(String, String)>,
493 expected_value: Option<String>,
494 ) {
495 let mut application_info = ApplicationInfo::new();
496
497 tags.into_iter().for_each(|(key, value)| {
498 application_info.add_tag(key, value);
499 });
500
501 assert_eq!(expected_value, application_info.build());
502 }
503}