launchdarkly_server_sdk/
config.rs

1use thiserror::Error;
2
3use crate::data_source_builders::{DataSourceFactory, NullDataSourceBuilder};
4use crate::events::processor_builders::{
5    EventProcessorBuilder, EventProcessorFactory, NullEventProcessorBuilder,
6};
7use crate::stores::store_builders::{DataStoreFactory, InMemoryDataStoreBuilder};
8use crate::{ServiceEndpointsBuilder, StreamingDataSourceBuilder};
9
10use std::borrow::Borrow;
11
12#[derive(Debug)]
13struct Tag {
14    key: String,
15    value: String,
16}
17
18impl Tag {
19    fn is_valid(&self) -> Result<(), &str> {
20        if self.value.chars().count() > 64 {
21            return Err("Value was longer than 64 characters and was discarded");
22        }
23
24        if self.key.is_empty() || !self.key.chars().all(Tag::valid_characters) {
25            return Err("Key was empty or contained invalid characters");
26        }
27
28        if self.value.is_empty() || !self.value.chars().all(Tag::valid_characters) {
29            return Err("Value was empty or contained invalid characters");
30        }
31
32        Ok(())
33    }
34
35    fn valid_characters(c: char) -> bool {
36        c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_')
37    }
38}
39
40impl std::fmt::Display for Tag {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        write!(f, "{}/{}", self.key, self.value)
43    }
44}
45
46/// ApplicationInfo allows configuration of application metadata.
47///
48/// If you want to set non-default values for any of these fields, create a new instance with
49/// [ApplicationInfo::new] and pass it to [ConfigBuilder::application_info].
50pub struct ApplicationInfo {
51    tags: Vec<Tag>,
52}
53
54impl ApplicationInfo {
55    /// Create a new default instance of [ApplicationInfo].
56    pub fn new() -> Self {
57        Self { tags: Vec::new() }
58    }
59
60    /// A unique identifier representing the application where the LaunchDarkly SDK is running.
61    ///
62    /// This can be specified as any string value as long as it only uses the following characters:
63    /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
64    /// characters will be ignored.
65    pub fn application_identifier(&mut self, application_id: impl Into<String>) -> &mut Self {
66        self.add_tag("application-id", application_id)
67    }
68
69    /// A unique identifier representing the version of the application where the LaunchDarkly SDK
70    /// is running.
71    ///
72    /// This can be specified as any string value as long as it only uses the following characters:
73    /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
74    /// characters will be ignored.
75    pub fn application_version(&mut self, application_version: impl Into<String>) -> &mut Self {
76        self.add_tag("application-version", application_version)
77    }
78
79    fn add_tag(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
80        let tag = Tag {
81            key: key.into(),
82            value: value.into(),
83        };
84
85        match tag.is_valid() {
86            Ok(_) => self.tags.push(tag),
87            Err(e) => {
88                warn!("{}", e)
89            }
90        }
91
92        self
93    }
94
95    pub(crate) fn build(&self) -> Option<String> {
96        if self.tags.is_empty() {
97            return None;
98        }
99
100        let mut tags = self
101            .tags
102            .iter()
103            .map(|tag| tag.to_string())
104            .collect::<Vec<String>>();
105
106        tags.sort();
107        tags.dedup();
108
109        Some(tags.join(" "))
110    }
111}
112
113impl Default for ApplicationInfo {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119/// Immutable configuration object for [crate::Client].
120///
121/// [Config] instances can be created using a [ConfigBuilder].
122pub struct Config {
123    sdk_key: String,
124    service_endpoints_builder: ServiceEndpointsBuilder,
125    data_store_builder: Box<dyn DataStoreFactory>,
126    data_source_builder: Box<dyn DataSourceFactory>,
127    event_processor_builder: Box<dyn EventProcessorFactory>,
128    application_tag: Option<String>,
129    offline: bool,
130    daemon_mode: bool,
131}
132
133impl Config {
134    /// Returns the sdk key.
135    pub fn sdk_key(&self) -> &str {
136        &self.sdk_key
137    }
138
139    /// Returns the [ServiceEndpointsBuilder]
140    pub fn service_endpoints_builder(&self) -> &ServiceEndpointsBuilder {
141        &self.service_endpoints_builder
142    }
143
144    /// Returns the DataStoreFactory
145    pub fn data_store_builder(&self) -> &(dyn DataStoreFactory) {
146        self.data_store_builder.borrow()
147    }
148
149    /// Returns the DataSourceFactory
150    pub fn data_source_builder(&self) -> &(dyn DataSourceFactory) {
151        self.data_source_builder.borrow()
152    }
153
154    /// Returns the EventProcessorFactory
155    pub fn event_processor_builder(&self) -> &(dyn EventProcessorFactory) {
156        self.event_processor_builder.borrow()
157    }
158
159    /// Returns the offline status
160    pub fn offline(&self) -> bool {
161        self.offline
162    }
163
164    /// Returns the daemon mode status
165    pub fn daemon_mode(&self) -> bool {
166        self.daemon_mode
167    }
168
169    /// Returns the tag builder if provided
170    pub fn application_tag(&self) -> &Option<String> {
171        &self.application_tag
172    }
173}
174
175/// Error type used to represent failures when building a Config instance.
176#[non_exhaustive]
177#[derive(Debug, Error)]
178pub enum BuildError {
179    /// Error used when a configuration setting is invalid.
180    #[error("config failed to build: {0}")]
181    InvalidConfig(String),
182}
183
184/// Used to create a [Config] struct for creating [crate::Client] instances.
185///
186/// For usage examples see:
187/// - [Creating service endpoints](crate::ServiceEndpointsBuilder)
188/// - [Configuring a persistent data store](crate::PersistentDataStoreBuilder)
189/// - [Configuring the streaming data source](crate::StreamingDataSourceBuilder)
190/// - [Configuring events sent to LaunchDarkly](crate::EventProcessorBuilder)
191pub struct ConfigBuilder {
192    service_endpoints_builder: Option<ServiceEndpointsBuilder>,
193    data_store_builder: Option<Box<dyn DataStoreFactory>>,
194    data_source_builder: Option<Box<dyn DataSourceFactory>>,
195    event_processor_builder: Option<Box<dyn EventProcessorFactory>>,
196    application_info: Option<ApplicationInfo>,
197    offline: bool,
198    daemon_mode: bool,
199    sdk_key: String,
200}
201
202impl ConfigBuilder {
203    /// Create a new instance of the [ConfigBuilder] with the provided `sdk_key`.
204    pub fn new(sdk_key: &str) -> Self {
205        Self {
206            service_endpoints_builder: None,
207            data_store_builder: None,
208            data_source_builder: None,
209            event_processor_builder: None,
210            offline: false,
211            daemon_mode: false,
212            application_info: None,
213            sdk_key: sdk_key.to_string(),
214        }
215    }
216
217    /// Set the URLs to use for this client. For usage see [ServiceEndpointsBuilder]
218    pub fn service_endpoints(mut self, builder: &ServiceEndpointsBuilder) -> Self {
219        self.service_endpoints_builder = Some(builder.clone());
220        self
221    }
222
223    /// Set the data store to use for this client.
224    ///
225    /// By default, the SDK uses an in-memory data store.
226    /// For a persistent store, see [PersistentDataStoreBuilder](crate::stores::persistent_store_builders::PersistentDataStoreBuilder).
227    pub fn data_store(mut self, builder: &dyn DataStoreFactory) -> Self {
228        self.data_store_builder = Some(builder.to_owned());
229        self
230    }
231
232    /// Set the data source to use for this client.
233    /// For the streaming data source, see [StreamingDataSourceBuilder](crate::data_source_builders::StreamingDataSourceBuilder).
234    ///
235    /// If offline mode is enabled, this data source will be ignored.
236    pub fn data_source(mut self, builder: &dyn DataSourceFactory) -> Self {
237        self.data_source_builder = Some(builder.to_owned());
238        self
239    }
240
241    /// Set the event processor to use for this client.
242    /// For usage see [EventProcessorBuilder](crate::EventProcessorBuilder).
243    ///
244    /// If offline mode is enabled, this event processor will be ignored.
245    pub fn event_processor(mut self, builder: &dyn EventProcessorFactory) -> Self {
246        self.event_processor_builder = Some(builder.to_owned());
247        self
248    }
249
250    /// Whether the client should be initialized in offline mode.
251    ///
252    /// In offline mode, default values are returned for all flags and no remote network requests
253    /// are made. By default, this is false.
254    pub fn offline(mut self, offline: bool) -> Self {
255        self.offline = offline;
256        self
257    }
258
259    /// Whether the client should operate in daemon mode.
260    ///
261    /// In daemon mode, the client will not receive updates directly from LaunchDarkly. Instead,
262    /// the client will rely on the data store to provide the latest feature flag values. By
263    /// default, this is false.
264    pub fn daemon_mode(mut self, enable: bool) -> Self {
265        self.daemon_mode = enable;
266        self
267    }
268
269    /// Provides configuration of application metadata.
270    ///
271    /// These properties are optional and informational. They may be used in LaunchDarkly analytics
272    /// or other product features, but they do not affect feature flag evaluations.
273    pub fn application_info(mut self, application_info: ApplicationInfo) -> Self {
274        self.application_info = Some(application_info);
275        self
276    }
277
278    /// Create a new instance of [Config] based on the [ConfigBuilder] configuration.
279    pub fn build(self) -> Result<Config, BuildError> {
280        let service_endpoints_builder = match &self.service_endpoints_builder {
281            None => ServiceEndpointsBuilder::new(),
282            Some(service_endpoints_builder) => service_endpoints_builder.clone(),
283        };
284
285        let data_store_builder = match &self.data_store_builder {
286            None => Box::new(InMemoryDataStoreBuilder::new()),
287            Some(_data_store_builder) => self.data_store_builder.unwrap(),
288        };
289
290        let data_source_builder_result: Result<Box<dyn DataSourceFactory>, BuildError> =
291            match self.data_source_builder {
292                None if self.offline => Ok(Box::new(NullDataSourceBuilder::new())),
293                Some(_) if self.offline => {
294                    warn!("Custom data source builders will be ignored when in offline mode");
295                    Ok(Box::new(NullDataSourceBuilder::new()))
296                }
297                None if self.daemon_mode => Ok(Box::new(NullDataSourceBuilder::new())),
298                Some(_) if self.daemon_mode => {
299                    warn!("Custom data source builders will be ignored when in daemon mode");
300                    Ok(Box::new(NullDataSourceBuilder::new()))
301                }
302                Some(builder) => Ok(builder),
303                #[cfg(feature = "rustls")]
304                None => Ok(Box::new(StreamingDataSourceBuilder::<
305                    hyper_rustls::HttpsConnector<hyper::client::HttpConnector>,
306                >::new())),
307                #[cfg(not(feature = "rustls"))]
308                None => Err(BuildError::InvalidConfig(
309                    "data source builder required when rustls is disabled".into(),
310                )),
311            };
312        let data_source_builder = data_source_builder_result?;
313
314        let event_processor_builder_result: Result<Box<dyn EventProcessorFactory>, BuildError> =
315            match self.event_processor_builder {
316                None if self.offline => Ok(Box::new(NullEventProcessorBuilder::new())),
317                Some(_) if self.offline => {
318                    warn!("Custom event processor builders will be ignored when in offline mode");
319                    Ok(Box::new(NullEventProcessorBuilder::new()))
320                }
321                Some(builder) => Ok(builder),
322                #[cfg(feature = "rustls")]
323                None => Ok(Box::new(EventProcessorBuilder::<
324                    hyper_rustls::HttpsConnector<hyper::client::HttpConnector>,
325                >::new())),
326                #[cfg(not(feature = "rustls"))]
327                None => Err(BuildError::InvalidConfig(
328                    "event processor factory required when rustls is disabled".into(),
329                )),
330            };
331        let event_processor_builder = event_processor_builder_result?;
332
333        let application_tag = match self.application_info {
334            Some(tb) => tb.build(),
335            _ => None,
336        };
337
338        Ok(Config {
339            sdk_key: self.sdk_key,
340            service_endpoints_builder,
341            data_store_builder,
342            data_source_builder,
343            event_processor_builder,
344            application_tag,
345            offline: self.offline,
346            daemon_mode: self.daemon_mode,
347        })
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use test_case::test_case;
354
355    use super::*;
356
357    #[test]
358    fn client_configured_with_custom_endpoints() {
359        let builder = ConfigBuilder::new("sdk-key").service_endpoints(
360            ServiceEndpointsBuilder::new().relay_proxy("http://my-relay-hostname:8080"),
361        );
362
363        let endpoints = builder.service_endpoints_builder.unwrap().build().unwrap();
364        assert_eq!(
365            endpoints.streaming_base_url(),
366            "http://my-relay-hostname:8080"
367        );
368        assert_eq!(
369            endpoints.polling_base_url(),
370            "http://my-relay-hostname:8080"
371        );
372        assert_eq!(endpoints.events_base_url(), "http://my-relay-hostname:8080");
373    }
374
375    #[test]
376    fn unconfigured_config_builder_handles_application_tags_correctly() {
377        let builder = ConfigBuilder::new("sdk-key");
378        let config = builder.build().expect("config should build");
379
380        assert_eq!(None, config.application_tag);
381    }
382
383    #[test_case("id", "version", Some("application-id/id application-version/version".to_string()))]
384    #[test_case("Invalid id", "version", Some("application-version/version".to_string()))]
385    #[test_case("id", "Invalid version", Some("application-id/id".to_string()))]
386    #[test_case("Invalid id", "Invalid version", None)]
387    fn config_builder_handles_application_tags_appropriately(
388        id: impl Into<String>,
389        version: impl Into<String>,
390        expected: Option<String>,
391    ) {
392        let mut application_info = ApplicationInfo::new();
393        application_info
394            .application_identifier(id)
395            .application_version(version);
396        let builder = ConfigBuilder::new("sdk-key");
397        let config = builder
398            .application_info(application_info)
399            .build()
400            .expect("config should build");
401
402        assert_eq!(expected, config.application_tag);
403    }
404
405    #[test_case("", "abc", Err("Key was empty or contained invalid characters"); "Empty key")]
406    #[test_case(" ", "abc", Err("Key was empty or contained invalid characters"); "Key with whitespace")]
407    #[test_case("/", "abc", Err("Key was empty or contained invalid characters"); "Key with slash")]
408    #[test_case(":", "abc", Err("Key was empty or contained invalid characters"); "Key with colon")]
409    #[test_case("🦀", "abc", Err("Key was empty or contained invalid characters"); "Key with emoji")]
410    #[test_case("abcABC123.-_", "abc", Ok(()); "Valid key")]
411    #[test_case("abc", "", Err("Value was empty or contained invalid characters"); "Empty value")]
412    #[test_case("abc", " ", Err("Value was empty or contained invalid characters"); "Value with whitespace")]
413    #[test_case("abc", "/", Err("Value was empty or contained invalid characters"); "Value with slash")]
414    #[test_case("abc", ":", Err("Value was empty or contained invalid characters"); "Value with colon")]
415    #[test_case("abc", "🦀", Err("Value was empty or contained invalid characters"); "Value with emoji")]
416    #[test_case("abc", "abcABC123.-_", Ok(()); "Valid value")]
417    #[test_case("abc", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl", Ok(()); "64 is the max length")]
418    #[test_case("abc", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm", Err("Value was longer than 64 characters and was discarded"); "65 is too far")]
419    fn tag_can_determine_valid_values(key: &str, value: &str, expected_result: Result<(), &str>) {
420        let tag = Tag {
421            key: key.to_string(),
422            value: value.to_string(),
423        };
424        assert_eq!(expected_result, tag.is_valid());
425    }
426
427    #[test_case(vec![], None; "No tags returns None")]
428    #[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")]
429    #[test_case(vec![("key".into(), "xyz".into()), ("key".into(), "abc".into())], Some("key/abc key/xyz".into()); "Keys are ordered correctly")]
430    #[test_case(vec![("key".into(), "abc".into()), ("key".into(), "abc".into())], Some("key/abc".into()); "Tags are deduped")]
431    #[test_case(vec![("XYZ".into(), "xyz".into()), ("abc".into(), "abc".into())], Some("XYZ/xyz abc/abc".into()); "Keys are ascii sorted correctly")]
432    #[test_case(vec![("abc".into(), "XYZ".into()), ("abc".into(), "abc".into())], Some("abc/XYZ abc/abc".into()); "Values are ascii sorted correctly")]
433    #[test_case(vec![("".into(), "XYZ".into()), ("abc".into(), "xyz".into())], Some("abc/xyz".into()); "Invalid tags are filtered")]
434    #[test_case(Vec::new(), None; "Empty tags returns None")]
435    fn application_tag_builder_can_create_tag_string_correctly(
436        tags: Vec<(String, String)>,
437        expected_value: Option<String>,
438    ) {
439        let mut application_info = ApplicationInfo::new();
440
441        tags.into_iter().for_each(|(key, value)| {
442            application_info.add_tag(key, value);
443        });
444
445        assert_eq!(expected_value, application_info.build());
446    }
447}