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
46pub struct ApplicationInfo {
51 tags: Vec<Tag>,
52}
53
54impl ApplicationInfo {
55 pub fn new() -> Self {
57 Self { tags: Vec::new() }
58 }
59
60 pub fn application_identifier(&mut self, application_id: impl Into<String>) -> &mut Self {
66 self.add_tag("application-id", application_id)
67 }
68
69 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
119pub 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 pub fn sdk_key(&self) -> &str {
136 &self.sdk_key
137 }
138
139 pub fn service_endpoints_builder(&self) -> &ServiceEndpointsBuilder {
141 &self.service_endpoints_builder
142 }
143
144 pub fn data_store_builder(&self) -> &(dyn DataStoreFactory) {
146 self.data_store_builder.borrow()
147 }
148
149 pub fn data_source_builder(&self) -> &(dyn DataSourceFactory) {
151 self.data_source_builder.borrow()
152 }
153
154 pub fn event_processor_builder(&self) -> &(dyn EventProcessorFactory) {
156 self.event_processor_builder.borrow()
157 }
158
159 pub fn offline(&self) -> bool {
161 self.offline
162 }
163
164 pub fn daemon_mode(&self) -> bool {
166 self.daemon_mode
167 }
168
169 pub fn application_tag(&self) -> &Option<String> {
171 &self.application_tag
172 }
173}
174
175#[non_exhaustive]
177#[derive(Debug, Error)]
178pub enum BuildError {
179 #[error("config failed to build: {0}")]
181 InvalidConfig(String),
182}
183
184pub 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 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 pub fn service_endpoints(mut self, builder: &ServiceEndpointsBuilder) -> Self {
219 self.service_endpoints_builder = Some(builder.clone());
220 self
221 }
222
223 pub fn data_store(mut self, builder: &dyn DataStoreFactory) -> Self {
228 self.data_store_builder = Some(builder.to_owned());
229 self
230 }
231
232 pub fn data_source(mut self, builder: &dyn DataSourceFactory) -> Self {
237 self.data_source_builder = Some(builder.to_owned());
238 self
239 }
240
241 pub fn event_processor(mut self, builder: &dyn EventProcessorFactory) -> Self {
246 self.event_processor_builder = Some(builder.to_owned());
247 self
248 }
249
250 pub fn offline(mut self, offline: bool) -> Self {
255 self.offline = offline;
256 self
257 }
258
259 pub fn daemon_mode(mut self, enable: bool) -> Self {
265 self.daemon_mode = enable;
266 self
267 }
268
269 pub fn application_info(mut self, application_info: ApplicationInfo) -> Self {
274 self.application_info = Some(application_info);
275 self
276 }
277
278 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}