Skip to main content

launchdarkly_server_sdk/
config.rs

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
58/// ApplicationInfo allows configuration of application metadata.
59///
60/// If you want to set non-default values for any of these fields, create a new instance with
61/// [ApplicationInfo::new] and pass it to [ConfigBuilder::application_info].
62pub struct ApplicationInfo {
63    tags: Vec<Tag>,
64}
65
66impl ApplicationInfo {
67    /// Create a new default instance of [ApplicationInfo].
68    pub fn new() -> Self {
69        Self { tags: Vec::new() }
70    }
71
72    /// A unique identifier representing the application where the LaunchDarkly SDK is running.
73    ///
74    /// This can be specified as any string value as long as it only uses the following characters:
75    /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
76    /// characters will be ignored.
77    pub fn application_identifier(&mut self, application_id: impl Into<String>) -> &mut Self {
78        self.add_tag("application-id", application_id)
79    }
80
81    /// A unique identifier representing the version of the application where the LaunchDarkly SDK
82    /// is running.
83    ///
84    /// This can be specified as any string value as long as it only uses the following characters:
85    /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
86    /// characters will be ignored.
87    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
131/// Immutable configuration object for [crate::Client].
132///
133/// [Config] instances can be created using a [ConfigBuilder].
134pub 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    /// Returns the sdk key.
147    pub fn sdk_key(&self) -> &str {
148        &self.sdk_key
149    }
150
151    /// Returns the [ServiceEndpointsBuilder]
152    pub fn service_endpoints_builder(&self) -> &ServiceEndpointsBuilder {
153        &self.service_endpoints_builder
154    }
155
156    /// Returns the DataStoreFactory
157    pub fn data_store_builder(&self) -> &dyn DataStoreFactory {
158        self.data_store_builder.borrow()
159    }
160
161    /// Returns the DataSourceFactory
162    pub fn data_source_builder(&self) -> &dyn DataSourceFactory {
163        self.data_source_builder.borrow()
164    }
165
166    /// Returns the EventProcessorFactory
167    pub fn event_processor_builder(&self) -> &dyn EventProcessorFactory {
168        self.event_processor_builder.borrow()
169    }
170
171    /// Returns the offline status
172    pub fn offline(&self) -> bool {
173        self.offline
174    }
175
176    /// Returns the daemon mode status
177    pub fn daemon_mode(&self) -> bool {
178        self.daemon_mode
179    }
180
181    /// Returns the tag builder if provided
182    pub fn application_tag(&self) -> &Option<String> {
183        &self.application_tag
184    }
185}
186
187/// Error type used to represent failures when building a Config instance.
188#[non_exhaustive]
189#[derive(Debug, Error)]
190pub enum BuildError {
191    /// Error used when a configuration setting is invalid.
192    #[error("config failed to build: {0}")]
193    InvalidConfig(String),
194}
195
196/// Used to create a [Config] struct for creating [crate::Client] instances.
197///
198/// For usage examples see:
199/// - [Creating service endpoints](crate::ServiceEndpointsBuilder)
200/// - [Configuring a persistent data store](crate::PersistentDataStoreBuilder)
201/// - [Configuring the streaming data source](crate::StreamingDataSourceBuilder)
202/// - [Configuring events sent to LaunchDarkly](crate::EventProcessorBuilder)
203pub 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    /// Create a new instance of the [ConfigBuilder] with the provided `sdk_key`.
216    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    /// Set the URLs to use for this client. For usage see [ServiceEndpointsBuilder]
230    pub fn service_endpoints(mut self, builder: &ServiceEndpointsBuilder) -> Self {
231        self.service_endpoints_builder = Some(builder.clone());
232        self
233    }
234
235    /// Set the data store to use for this client.
236    ///
237    /// By default, the SDK uses an in-memory data store.
238    /// For a persistent store, see [PersistentDataStoreBuilder](crate::stores::persistent_store_builders::PersistentDataStoreBuilder).
239    pub fn data_store(mut self, builder: &dyn DataStoreFactory) -> Self {
240        self.data_store_builder = Some(builder.to_owned());
241        self
242    }
243
244    /// Set the data source to use for this client.
245    /// For the streaming data source, see [StreamingDataSourceBuilder](crate::data_source_builders::StreamingDataSourceBuilder).
246    ///
247    /// If offline mode is enabled, this data source will be ignored.
248    pub fn data_source(mut self, builder: &dyn DataSourceFactory) -> Self {
249        self.data_source_builder = Some(builder.to_owned());
250        self
251    }
252
253    /// Set the event processor to use for this client.
254    /// For usage see [EventProcessorBuilder](crate::EventProcessorBuilder).
255    ///
256    /// If offline mode is enabled, this event processor will be ignored.
257    pub fn event_processor(mut self, builder: &dyn EventProcessorFactory) -> Self {
258        self.event_processor_builder = Some(builder.to_owned());
259        self
260    }
261
262    /// Whether the client should be initialized in offline mode.
263    ///
264    /// In offline mode, default values are returned for all flags and no remote network requests
265    /// are made. By default, this is false.
266    pub fn offline(mut self, offline: bool) -> Self {
267        self.offline = offline;
268        self
269    }
270
271    /// Whether the client should operate in daemon mode.
272    ///
273    /// In daemon mode, the client will not receive updates directly from LaunchDarkly. Instead,
274    /// the client will rely on the data store to provide the latest feature flag values. By
275    /// default, this is false.
276    pub fn daemon_mode(mut self, enable: bool) -> Self {
277        self.daemon_mode = enable;
278        self
279    }
280
281    /// Provides configuration of application metadata.
282    ///
283    /// These properties are optional and informational. They may be used in LaunchDarkly analytics
284    /// or other product features, but they do not affect feature flag evaluations.
285    pub fn application_info(mut self, application_info: ApplicationInfo) -> Self {
286        self.application_info = Some(application_info);
287        self
288    }
289
290    /// Create a new instance of [Config] based on the [ConfigBuilder] configuration.
291    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}