Skip to main content

launchdarkly_server_sdk/
config.rs

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
59/// ApplicationInfo allows configuration of application metadata.
60///
61/// If you want to set non-default values for any of these fields, create a new instance with
62/// [ApplicationInfo::new] and pass it to [ConfigBuilder::application_info].
63pub struct ApplicationInfo {
64    tags: Vec<Tag>,
65}
66
67impl ApplicationInfo {
68    /// Create a new default instance of [ApplicationInfo].
69    pub fn new() -> Self {
70        Self { tags: Vec::new() }
71    }
72
73    /// A unique identifier representing the application where the LaunchDarkly SDK is running.
74    ///
75    /// This can be specified as any string value as long as it only uses the following characters:
76    /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
77    /// characters will be ignored.
78    pub fn application_identifier(&mut self, application_id: impl Into<String>) -> &mut Self {
79        self.add_tag("application-id", application_id)
80    }
81
82    /// A unique identifier representing the version of the application where the LaunchDarkly SDK
83    /// is running.
84    ///
85    /// This can be specified as any string value as long as it only uses the following characters:
86    /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
87    /// characters will be ignored.
88    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
132/// Immutable configuration object for [crate::Client].
133///
134/// [Config] instances can be created using a [ConfigBuilder].
135pub 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    /// Returns the sdk key.
150    pub fn sdk_key(&self) -> &str {
151        &self.sdk_key
152    }
153
154    /// Returns the [ServiceEndpointsBuilder]
155    pub fn service_endpoints_builder(&self) -> &ServiceEndpointsBuilder {
156        &self.service_endpoints_builder
157    }
158
159    /// Returns the DataStoreFactory
160    pub fn data_store_builder(&self) -> &dyn DataStoreFactory {
161        self.data_store_builder.borrow()
162    }
163
164    /// Returns the DataSourceFactory
165    pub fn data_source_builder(&self) -> &dyn DataSourceFactory {
166        self.data_source_builder.borrow()
167    }
168
169    /// Returns the DataSystemFactory, if an FDv2 data system was configured.
170    pub(crate) fn data_system_builder(&self) -> Option<&dyn DataSystemFactory> {
171        self.data_system_builder.as_deref()
172    }
173
174    /// Returns the EventProcessorFactory
175    pub fn event_processor_builder(&self) -> &dyn EventProcessorFactory {
176        self.event_processor_builder.borrow()
177    }
178
179    /// Returns the offline status
180    pub fn offline(&self) -> bool {
181        self.offline
182    }
183
184    /// Returns the daemon mode status
185    pub fn daemon_mode(&self) -> bool {
186        self.daemon_mode
187    }
188
189    /// Returns the tag builder if provided
190    pub fn application_tag(&self) -> &Option<String> {
191        &self.application_tag
192    }
193
194    /// Returns the per-SDK-instance identifier. This is a v4 UUID, generated once when the
195    /// [Config] is built, that is included in the `X-LaunchDarkly-Instance-Id` HTTP header
196    /// on outbound requests for the lifetime of the SDK instance.
197    pub fn instance_id(&self) -> &str {
198        &self.instance_id
199    }
200}
201
202/// Error type used to represent failures when building a Config instance.
203#[non_exhaustive]
204#[derive(Debug, Error)]
205pub enum BuildError {
206    /// Error used when a configuration setting is invalid.
207    #[error("config failed to build: {0}")]
208    InvalidConfig(String),
209}
210
211/// Used to create a [Config] struct for creating [crate::Client] instances.
212///
213/// For usage examples see:
214/// - [Creating service endpoints](crate::ServiceEndpointsBuilder)
215/// - [Configuring a persistent data store](crate::PersistentDataStoreBuilder)
216/// - [Configuring the streaming data source](crate::StreamingDataSourceBuilder)
217/// - [Configuring events sent to LaunchDarkly](crate::EventProcessorBuilder)
218pub 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    /// Create a new instance of the [ConfigBuilder] with the provided `sdk_key`.
232    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    /// Set the URLs to use for this client. For usage see [ServiceEndpointsBuilder]
247    pub fn service_endpoints(mut self, builder: &ServiceEndpointsBuilder) -> Self {
248        self.service_endpoints_builder = Some(builder.clone());
249        self
250    }
251
252    /// Set the data store to use for this client.
253    ///
254    /// By default, the SDK uses an in-memory data store.
255    /// For a persistent store, see [PersistentDataStoreBuilder](crate::stores::persistent_store_builders::PersistentDataStoreBuilder).
256    pub fn data_store(mut self, builder: &dyn DataStoreFactory) -> Self {
257        self.data_store_builder = Some(builder.to_owned());
258        self
259    }
260
261    /// Set the data source to use for this client.
262    /// For the streaming data source, see [StreamingDataSourceBuilder](crate::data_source_builders::StreamingDataSourceBuilder).
263    ///
264    /// If offline mode is enabled, this data source will be ignored.
265    pub fn data_source(mut self, builder: &dyn DataSourceFactory) -> Self {
266        self.data_source_builder = Some(builder.to_owned());
267        self
268    }
269
270    /// Set the data system to use for this client.
271    ///
272    /// When set, the data system supersedes the [data_source](ConfigBuilder::data_source).
273    /// If offline mode is enabled, it will be ignored.
274    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    /// Set the event processor to use for this client.
281    /// For usage see [EventProcessorBuilder](crate::EventProcessorBuilder).
282    ///
283    /// If offline mode is enabled, this event processor will be ignored.
284    pub fn event_processor(mut self, builder: &dyn EventProcessorFactory) -> Self {
285        self.event_processor_builder = Some(builder.to_owned());
286        self
287    }
288
289    /// Whether the client should be initialized in offline mode.
290    ///
291    /// In offline mode, default values are returned for all flags and no remote network requests
292    /// are made. By default, this is false.
293    pub fn offline(mut self, offline: bool) -> Self {
294        self.offline = offline;
295        self
296    }
297
298    /// Whether the client should operate in daemon mode.
299    ///
300    /// In daemon mode, the client will not receive updates directly from LaunchDarkly. Instead,
301    /// the client will rely on the data store to provide the latest feature flag values. By
302    /// default, this is false.
303    pub fn daemon_mode(mut self, enable: bool) -> Self {
304        self.daemon_mode = enable;
305        self
306    }
307
308    /// Provides configuration of application metadata.
309    ///
310    /// These properties are optional and informational. They may be used in LaunchDarkly analytics
311    /// or other product features, but they do not affect feature flag evaluations.
312    pub fn application_info(mut self, application_info: ApplicationInfo) -> Self {
313        self.application_info = Some(application_info);
314        self
315    }
316
317    /// Create a new instance of [Config] based on the [ConfigBuilder] configuration.
318    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        // The data system is optional; when set it supersedes the data source.
330        // Like the data source, it is ignored in offline or daemon mode.
331        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        // Per SCMP-server-connection-minutes-polling, every polling request must carry a
431        // per-SDK-instance v4 UUID. We generate it once here, store it on Config, and pass it
432        // into the data source, feature requester, and event processor so that streaming,
433        // polling, and event requests all carry the same stable identifier for the lifetime
434        // of this client.
435        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        // Each call to ConfigBuilder::build represents a new SDK instance; each must get its own
517        // GUID so connection-minutes accounting on the server side can distinguish them.
518        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}