1#![deny(warnings)]
3#![deny(missing_docs)]
4
5use std::sync::{Arc, RwLock};
6use std::{borrow::Cow, collections::HashSet};
7
8pub use figment::value;
9use figment::{
10 error::Kind,
11 providers::{Env, Serialized},
12 Figment, Provider,
13};
14use saluki_error::GenericError;
15use serde::Deserialize;
16use snafu::Snafu;
17use tokio::sync::broadcast;
18use tracing::{debug, error};
19
20pub mod duration_string;
21pub mod dynamic;
22mod provider;
23pub mod space_separated;
24pub mod yaml;
25
26pub use self::duration_string::{parse_duration, DurationString, ParseDurationError};
27pub use self::dynamic::FieldUpdateWatcher;
28use self::dynamic::{settings_to_state, ConfigChangeEvent, ConfigUpdate};
29use self::provider::ResolvedProvider;
30pub use self::space_separated::{deserialize_opt_space_separated_or_seq, deserialize_space_separated_or_seq};
31
32#[derive(Clone)]
33struct ArcProvider(Arc<dyn Provider + Send + Sync>);
34
35impl Provider for ArcProvider {
36 fn metadata(&self) -> figment::Metadata {
37 self.0.metadata()
38 }
39
40 fn data(&self) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
41 self.0.data()
42 }
43}
44
45enum ProviderSource {
46 Static(ArcProvider),
47 Dynamic(Option<ConfigUpdate>),
48}
49
50impl Clone for ProviderSource {
51 fn clone(&self) -> Self {
52 match self {
53 Self::Static(p) => Self::Static(p.clone()),
54 Self::Dynamic(_) => Self::Dynamic(None),
55 }
56 }
57}
58
59#[derive(Debug, Snafu)]
61#[snafu(context(suffix(false)))]
62pub enum ConfigurationError {
63 #[snafu(display("Environment variable prefix must not be empty."))]
65 EmptyPrefix,
66
67 #[snafu(display("Missing field '{}' in configuration. {}", field, help_text))]
69 MissingField {
70 help_text: String,
75
76 field: Cow<'static, str>,
78 },
79
80 #[snafu(display(
82 "Expected value for field '{}' to be '{}', got '{}' instead.",
83 field,
84 expected_ty,
85 actual_ty
86 ))]
87 InvalidFieldType {
88 field: String,
92
93 expected_ty: String,
95
96 actual_ty: String,
98 },
99
100 #[snafu(transparent)]
102 Generic {
103 source: GenericError,
105 },
106}
107
108impl From<figment::Error> for ConfigurationError {
109 fn from(e: figment::Error) -> Self {
110 match e.kind {
111 Kind::InvalidType(actual_ty, expected_ty) => Self::InvalidFieldType {
112 field: e.path.join("."),
113 expected_ty,
114 actual_ty: actual_ty.to_string(),
115 },
116 _ => Self::Generic { source: e.into() },
117 }
118 }
119}
120
121#[derive(Clone, Debug, Eq, Hash, PartialEq)]
122enum LookupSource {
123 Environment { prefix: String },
125}
126
127impl LookupSource {
128 fn transform_key(&self, key: &str) -> String {
129 match self {
130 LookupSource::Environment { prefix } => format!("{}{}", prefix, key.replace('.', "_").to_uppercase()),
133 }
134 }
135}
136
137#[derive(Clone, Default)]
153pub struct ConfigurationLoader {
154 lookup_sources: HashSet<LookupSource>,
155 provider_sources: Vec<ProviderSource>,
156}
157
158impl ConfigurationLoader {
159 pub fn add_providers<P, I>(mut self, providers: I) -> Self
169 where
170 P: Provider + Send + Sync + 'static,
171 I: IntoIterator<Item = P>,
172 {
173 for p in providers {
174 self.provider_sources
175 .push(ProviderSource::Static(ArcProvider(Arc::new(p))));
176 }
177 self
178 }
179
180 pub fn from_yaml<P>(mut self, path: P) -> Result<Self, ConfigurationError>
186 where
187 P: AsRef<std::path::Path>,
188 {
189 let resolved_provider = ResolvedProvider::from_yaml(&path)?;
190 self.provider_sources
191 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
192 Ok(self)
193 }
194
195 pub fn try_from_yaml<P>(mut self, path: P) -> Self
199 where
200 P: AsRef<std::path::Path>,
201 {
202 match ResolvedProvider::from_yaml(&path) {
203 Ok(resolved_provider) => {
204 self.provider_sources
205 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
206 }
207 Err(e) => {
208 println!(
209 "Unable to read YAML configuration file '{}': {}. Ignoring.",
210 path.as_ref().to_string_lossy(),
211 e
212 );
213 }
214 }
215 self
216 }
217
218 pub fn from_json<P>(mut self, path: P) -> Result<Self, ConfigurationError>
224 where
225 P: AsRef<std::path::Path>,
226 {
227 let resolved_provider = ResolvedProvider::from_json(&path)?;
228 self.provider_sources
229 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
230 Ok(self)
231 }
232
233 pub fn try_from_json<P>(mut self, path: P) -> Self
237 where
238 P: AsRef<std::path::Path>,
239 {
240 match ResolvedProvider::from_json(&path) {
241 Ok(resolved_provider) => {
242 self.provider_sources
243 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
244 }
245 Err(e) => {
246 println!(
247 "Unable to read JSON configuration file '{}': {}. Ignoring.",
248 path.as_ref().to_string_lossy(),
249 e
250 );
251 }
252 }
253 self
254 }
255
256 pub fn from_environment(mut self, prefix: &'static str) -> Result<Self, ConfigurationError> {
269 if prefix.is_empty() {
270 return Err(ConfigurationError::EmptyPrefix);
271 }
272
273 let prefix = if prefix.ends_with('_') {
274 prefix.to_string()
275 } else {
276 format!("{}_", prefix)
277 };
278
279 let env = Env::prefixed(&prefix).split("__");
281 let values = env.data().unwrap();
282 if let Some(default_dict) = values.get(&figment::Profile::Default) {
283 self.provider_sources
284 .push(ProviderSource::Static(ArcProvider(Arc::new(Serialized::defaults(
285 default_dict.clone(),
286 )))));
287 self.lookup_sources.insert(LookupSource::Environment { prefix });
288 }
289 Ok(self)
290 }
291
292 pub fn with_dynamic_configuration(mut self, initial: ConfigUpdate) -> Self {
298 self.provider_sources.push(ProviderSource::Dynamic(Some(initial)));
299 self
300 }
301
302 pub fn into_typed<'a, T>(self) -> Result<T, ConfigurationError>
308 where
309 T: Deserialize<'a>,
310 {
311 let figment = build_figment_from_sources(&self.provider_sources);
312 figment.extract().map_err(Into::into)
313 }
314
315 pub async fn into_generic(mut self) -> Result<(GenericConfiguration, Option<ConfigUpdater>), ConfigurationError> {
325 let has_dynamic_provider = self
326 .provider_sources
327 .iter()
328 .any(|s| matches!(s, ProviderSource::Dynamic(_)));
329
330 if has_dynamic_provider {
331 let mut initial_opt = None;
332 for source in self.provider_sources.iter_mut() {
333 if let ProviderSource::Dynamic(ref mut initial) = source {
334 initial_opt = initial.take();
335 break;
336 }
337 }
338 let initial = initial_opt.expect("Dynamic initial update should exist but was not found");
339
340 let mut dynamic_state = serde_json::Value::Null;
343 apply_to_dynamic_state(&mut dynamic_state, initial);
344 let figment = build_figment_with_dynamic_state(&self.provider_sources, &dynamic_state);
345 let current_config = figment.extract()?;
346
347 let (event_sender, _) = broadcast::channel(100);
348
349 let generic_config = GenericConfiguration {
350 inner: Arc::new(Inner {
351 figment: RwLock::new(figment),
352 lookup_sources: self.lookup_sources,
353 event_sender: Some(event_sender.clone()),
354 }),
355 };
356
357 let updater = ConfigUpdater {
358 inner: generic_config.inner.clone(),
359 provider_sources: self.provider_sources,
360 sender: event_sender,
361 dynamic_state,
362 current_config,
363 };
364
365 Ok((generic_config, Some(updater)))
366 } else {
367 let figment = build_figment_from_sources(&self.provider_sources);
369
370 let generic_config = GenericConfiguration {
371 inner: Arc::new(Inner {
372 figment: RwLock::new(figment),
373 lookup_sources: self.lookup_sources,
374 event_sender: None,
375 }),
376 };
377
378 Ok((generic_config, None))
379 }
380 }
381
382 #[cfg(any(test, feature = "test-util"))]
396 pub async fn for_tests(
397 file_values: Option<serde_json::Value>, env_vars: Option<&[(String, String)]>,
398 initial_snapshot: Option<ConfigUpdate>,
399 ) -> (GenericConfiguration, Option<tokio::sync::mpsc::Sender<ConfigUpdate>>) {
400 Self::for_tests_with_provider_factory(file_values, env_vars, initial_snapshot, |_| {
401 Serialized::defaults(serde_json::json!({}))
402 })
403 .await
404 }
405
406 #[cfg(any(test, feature = "test-util"))]
415 pub async fn for_tests_with_provider_factory<P, F>(
416 file_values: Option<serde_json::Value>, env_vars: Option<&[(String, String)]>,
417 initial_snapshot: Option<ConfigUpdate>, provider_factory: F,
418 ) -> (GenericConfiguration, Option<tokio::sync::mpsc::Sender<ConfigUpdate>>)
419 where
420 P: Provider + Send + Sync + 'static,
421 F: FnOnce(Vec<(String, String)>) -> P,
422 {
423 let json_file = tempfile::NamedTempFile::new().expect("should not fail to create temp file.");
424 let path = &json_file.path();
425 let json_to_write = file_values.unwrap_or(serde_json::json!({}));
426 serde_json::to_writer(&json_file, &json_to_write).expect("should not fail to write to temp file.");
427
428 let mut loader = ConfigurationLoader::default().try_from_json(path);
429 if let Some(initial_snapshot) = initial_snapshot {
430 loader = loader.with_dynamic_configuration(initial_snapshot);
431 }
432
433 let guard = test_env_lock();
437
438 if let Some(pairs) = env_vars.as_ref() {
439 for (k, v) in pairs.iter() {
440 std::env::set_var(k, v);
444 std::env::set_var(format!("TEST_{}", k), v);
445 }
446 }
447
448 let provider_env_vars = env_vars.unwrap_or_default().to_vec();
450 let loader = loader.add_providers([provider_factory(provider_env_vars)]);
451
452 let loader = loader
454 .from_environment("TEST")
455 .expect("should not fail to add environment provider");
456
457 if let Some(pairs) = env_vars.as_ref() {
459 for (k, _) in pairs.iter() {
460 std::env::remove_var(k);
461 std::env::remove_var(format!("TEST_{}", k));
462 }
463 }
464
465 drop(guard);
466
467 let (cfg, updater) = loader
468 .into_generic()
469 .await
470 .expect("should not fail to build generic configuration");
471 let maybe_sender = updater.map(|mut updater| {
472 let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
473 tokio::spawn(async move {
474 while let Some(update) = receiver.recv().await {
475 updater
476 .apply(update)
477 .expect("should not fail to apply dynamic configuration update");
478 }
479 });
480 sender
481 });
482
483 (cfg, maybe_sender)
484 }
485}
486
487#[cfg(any(test, feature = "test-util"))]
503pub fn test_env_lock() -> std::sync::MutexGuard<'static, ()> {
504 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
505 ENV_MUTEX.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
506}
507
508#[cfg(any(test, feature = "test-util"))]
520pub async fn config_from(file_values: serde_json::Value) -> GenericConfiguration {
521 let (config, _) = ConfigurationLoader::for_tests(Some(file_values), None, None).await;
522 config
523}
524
525fn build_figment_from_sources(sources: &[ProviderSource]) -> Figment {
526 sources.iter().fold(Figment::new(), |figment, source| match source {
527 ProviderSource::Static(p) => figment.admerge(p.clone()),
528 ProviderSource::Dynamic(_) => figment,
530 })
531}
532
533pub fn upsert(root: &mut serde_json::Value, key: &str, value: serde_json::Value) {
537 if !root.is_object() {
538 *root = serde_json::Value::Object(serde_json::Map::new());
539 }
540
541 let mut current = root;
542 let mut segments = key.split('.').peekable();
544
545 while let Some(seg) = segments.next() {
546 let is_leaf = segments.peek().is_none();
547
548 if !current.is_object() {
550 *current = serde_json::Value::Object(serde_json::Map::new());
551 }
552 let node = current.as_object_mut().expect("current node should be an object");
553
554 if is_leaf {
555 node.insert(seg.to_string(), value);
556 break;
557 } else {
558 let should_create_node = match node.get(seg) {
560 Some(v) => !v.is_object(),
561 None => true,
562 };
563 if should_create_node {
565 node.insert(seg.to_string(), serde_json::Value::Object(serde_json::Map::new()));
566 }
567
568 current = node.get_mut(seg).expect("should not fail to get nested object");
570 }
571 }
572}
573
574pub struct ConfigUpdater {
580 inner: Arc<Inner>,
581 provider_sources: Vec<ProviderSource>,
582 sender: broadcast::Sender<ConfigChangeEvent>,
583 dynamic_state: serde_json::Value,
584 current_config: figment::value::Value,
585}
586
587impl ConfigUpdater {
588 pub fn apply(&mut self, update: ConfigUpdate) -> Result<(), ConfigurationError> {
599 apply_to_dynamic_state(&mut self.dynamic_state, update);
600
601 let new_figment = build_figment_with_dynamic_state(&self.provider_sources, &self.dynamic_state);
603 let new_config: figment::value::Value = new_figment.extract()?;
604
605 if self.current_config != new_config {
606 let changes = dynamic::diff_config(&self.current_config, &new_config);
607
608 {
609 let mut figment_guard = self.inner.figment.write().unwrap_or_else(|e| {
610 error!("Failed to acquire write lock for dynamic configuration: {}", e);
611 e.into_inner()
612 });
613 *figment_guard = new_figment;
614 }
615
616 for change in changes {
617 let _ = self.sender.send(change);
621 }
622
623 self.current_config = new_config;
625 }
626
627 Ok(())
628 }
629}
630
631fn apply_to_dynamic_state(dynamic_state: &mut serde_json::Value, update: ConfigUpdate) {
633 match update {
636 ConfigUpdate::Snapshot(settings) => {
637 debug!("Received configuration snapshot update.");
638 *dynamic_state = settings_to_state(&settings);
639 }
640 ConfigUpdate::Partial(setting) => {
641 debug!(key = %setting.key, "Received partial configuration update.");
642 if dynamic_state.is_null() {
643 *dynamic_state = serde_json::Value::Object(serde_json::Map::new());
644 }
645 if dynamic_state.is_object() {
646 upsert(dynamic_state, &setting.key, setting.value);
647 } else {
648 error!("Received partial update but current dynamic state is not an object. This should not happen.");
649 }
650 }
651 }
652}
653
654fn build_figment_with_dynamic_state(sources: &[ProviderSource], dynamic_state: &serde_json::Value) -> Figment {
656 sources.iter().fold(Figment::new(), |figment, source| match source {
657 ProviderSource::Static(p) => figment.admerge(p.clone()),
658 ProviderSource::Dynamic(_) => figment.admerge(Serialized::defaults(dynamic_state.clone())),
659 })
660}
661
662#[derive(Debug)]
663struct Inner {
664 figment: RwLock<Figment>,
665 lookup_sources: HashSet<LookupSource>,
666 event_sender: Option<broadcast::Sender<ConfigChangeEvent>>,
667}
668
669#[derive(Clone, Debug)]
691pub struct GenericConfiguration {
692 inner: Arc<Inner>,
693}
694
695impl GenericConfiguration {
696 fn get<'a, T>(&self, key: &str) -> Result<T, ConfigurationError>
697 where
698 T: Deserialize<'a>,
699 {
700 let figment_guard = self.inner.figment.read().unwrap();
701 match figment_guard.extract_inner(key) {
702 Ok(value) => Ok(value),
703 Err(e) => {
704 if matches!(e.kind, figment::error::Kind::MissingField(_)) {
705 let fallback_key = key.replace('.', "_");
710 figment_guard
711 .extract_inner(&fallback_key)
712 .map_err(|fallback_e| from_figment_error(&self.inner.lookup_sources, fallback_e))
713 } else {
714 Err(e.into())
715 }
716 }
717 }
718 }
719
720 pub fn get_typed<'a, T>(&self, key: &str) -> Result<T, ConfigurationError>
729 where
730 T: Deserialize<'a>,
731 {
732 self.get(key)
733 }
734
735 pub fn get_typed_or_default<'a, T>(&self, key: &str) -> T
742 where
743 T: Default + Deserialize<'a>,
744 {
745 self.get(key).unwrap_or_default()
746 }
747
748 pub fn try_get_typed<'a, T>(&self, key: &str) -> Result<Option<T>, ConfigurationError>
759 where
760 T: Deserialize<'a>,
761 {
762 match self.get(key) {
763 Ok(value) => Ok(Some(value)),
764 Err(ConfigurationError::MissingField { .. }) => Ok(None),
765 Err(e) => Err(e),
766 }
767 }
768
769 pub fn as_typed<'a, T>(&self) -> Result<T, ConfigurationError>
775 where
776 T: Deserialize<'a>,
777 {
778 self.inner
779 .figment
780 .read()
781 .unwrap()
782 .extract()
783 .map_err(|e| from_figment_error(&self.inner.lookup_sources, e))
784 }
785
786 pub fn subscribe_for_updates(&self) -> Option<broadcast::Receiver<dynamic::ConfigChangeEvent>> {
788 self.inner.event_sender.as_ref().map(|s| s.subscribe())
789 }
790
791 pub fn flattened_keys(&self) -> Result<Vec<(String, serde_json::Value)>, ConfigurationError> {
801 let root: serde_json::Value = self.as_typed()?;
802 let mut out = Vec::new();
803 flatten_value(&root, &mut String::new(), &mut out);
804 Ok(out)
805 }
806
807 pub fn watch_for_updates(&self, key: &str) -> FieldUpdateWatcher {
812 FieldUpdateWatcher {
813 key: key.to_string(),
814 rx: self.subscribe_for_updates(),
815 }
816 }
817}
818
819fn flatten_value(value: &serde_json::Value, prefix: &mut String, out: &mut Vec<(String, serde_json::Value)>) {
821 if let serde_json::Value::Object(map) = value {
822 for (key, child) in map {
823 let prev_len = prefix.len();
824 if !prefix.is_empty() {
825 prefix.push('.');
826 }
827 prefix.push_str(key);
828 flatten_value(child, prefix, out);
829 prefix.truncate(prev_len);
830 }
831 } else {
832 out.push((prefix.clone(), value.clone()));
833 }
834}
835
836fn from_figment_error(lookup_sources: &HashSet<LookupSource>, e: figment::Error) -> ConfigurationError {
837 match e.kind {
838 Kind::MissingField(field) => {
839 let mut valid_keys = lookup_sources
840 .iter()
841 .map(|source| source.transform_key(&field))
842 .collect::<Vec<_>>();
843
844 valid_keys.insert(0, field.to_string());
846
847 let help_text = format!("Try setting `{}`.", valid_keys.join("` or `"));
848
849 ConfigurationError::MissingField { help_text, field }
850 }
851 Kind::InvalidType(actual_ty, expected_ty) => ConfigurationError::InvalidFieldType {
852 field: e.path.join("."),
853 expected_ty,
854 actual_ty: actual_ty.to_string(),
855 },
856 _ => ConfigurationError::Generic { source: e.into() },
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::dynamic::ConfigSetting;
863 use super::*;
864
865 #[tokio::test]
866 async fn static_configuration() {
867 let (cfg, _) = ConfigurationLoader::for_tests(
868 Some(serde_json::json!({
869 "foo": "bar",
870 "baz": 5,
871 "foobar": { "a": false, "b": "c" }
872 })),
873 Some(&[("ENV_VAR".to_string(), "from_env".to_string())]),
874 None,
875 )
876 .await;
877
878 assert_eq!(cfg.get_typed::<String>("foo").unwrap(), "bar");
879 assert_eq!(cfg.get_typed::<i32>("baz").unwrap(), 5);
880 assert!(!cfg.get_typed::<bool>("foobar.a").unwrap());
881 assert_eq!(cfg.get_typed::<String>("env_var").unwrap(), "from_env");
882 assert!(matches!(
883 cfg.get::<String>("nonexistentKey"),
884 Err(ConfigurationError::MissingField { .. })
885 ));
886 }
887
888 #[tokio::test]
889 async fn dynamic_configuration() {
890 let (cfg, sender) = ConfigurationLoader::for_tests(
891 Some(serde_json::json!({
892 "foo": "bar",
893 "baz": 5,
894 "foobar": { "a": false, "b": "c" }
895 })),
896 Some(&[("ENV_VAR".to_string(), "from_env".to_string())]),
897 Some(ConfigUpdate::snapshot([ConfigSetting::explicit(
898 "new",
899 serde_json::json!("from_snapshot"),
900 )])),
901 )
902 .await;
903 let sender = sender.expect("sender should exist");
904
905 assert_eq!(cfg.get_typed::<String>("foo").unwrap(), "bar");
907
908 assert_eq!(cfg.get_typed::<String>("new").unwrap(), "from_snapshot");
910
911 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
912
913 sender
914 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
915 "new_key",
916 "from dynamic update".to_string().into(),
917 )))
918 .await
919 .unwrap();
920
921 tokio::time::timeout(std::time::Duration::from_secs(2), async {
922 loop {
923 match rx.recv().await {
924 Ok(ev) if ev.key == "new_key" => break ev,
925 Err(e) => panic!("updates channel closed: {e}"),
926 Ok(_) => continue,
927 }
928 }
929 })
930 .await
931 .expect("timed out waiting for new_key update");
932
933 assert_eq!(cfg.get_typed::<String>("new_key").unwrap(), "from dynamic update");
934
935 sender
937 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
938 "foobar.a",
939 serde_json::json!(true),
940 )))
941 .await
942 .unwrap();
943
944 tokio::time::timeout(std::time::Duration::from_secs(2), async {
945 loop {
946 match rx.recv().await {
947 Ok(ev) if ev.key == "foobar.a" => break ev,
948 Err(e) => panic!("updates channel closed: {e}"),
949 Ok(_) => continue,
950 }
951 }
952 })
953 .await
954 .expect("timed out waiting for foobar.a update");
955
956 assert!(cfg.get_typed::<bool>("foobar.a").unwrap());
957 assert_eq!(cfg.get_typed::<String>("foobar.b").unwrap(), "c");
958 }
959
960 #[test]
961 fn update_events_are_sent_after_the_figment_map_is_updated() {
962 fn recv_with_timeout(
963 rx: &mut tokio::sync::broadcast::Receiver<ConfigChangeEvent>, timeout: std::time::Duration,
964 ) -> Option<ConfigChangeEvent> {
965 let deadline = std::time::Instant::now() + timeout;
966 loop {
967 match rx.try_recv() {
968 Ok(event) => return Some(event),
969 Err(tokio::sync::broadcast::error::TryRecvError::Empty) if std::time::Instant::now() < deadline => {
970 std::thread::sleep(std::time::Duration::from_millis(1));
971 }
972 Err(tokio::sync::broadcast::error::TryRecvError::Empty) => return None,
973 Err(e) => panic!("updates channel failed: {e}"),
974 }
975 }
976 }
977
978 let runtime = tokio::runtime::Builder::new_current_thread().build().unwrap();
979 let (cfg, sender) = runtime.block_on(async {
980 let (cfg, sender) = ConfigurationLoader::for_tests(
981 None,
982 None,
983 Some(ConfigUpdate::snapshot([ConfigSetting::explicit(
984 "observed",
985 serde_json::json!("old"),
986 )])),
987 )
988 .await;
989 (cfg, sender.expect("sender should exist"))
990 });
991
992 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
993 let (started_tx, started_rx) = std::sync::mpsc::channel();
994 let runtime_thread = std::thread::spawn(move || {
995 runtime.block_on(async {
996 started_tx.send(()).unwrap();
997 let _ = stop_rx.await;
998 });
999 });
1000 started_rx.recv().unwrap();
1001
1002 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
1003 let figment_guard = cfg.inner.figment.read().unwrap();
1004
1005 sender
1006 .blocking_send(ConfigUpdate::Partial(ConfigSetting::explicit(
1007 "observed",
1008 serde_json::json!("new"),
1009 )))
1010 .unwrap();
1011
1012 let early_event = recv_with_timeout(&mut rx, std::time::Duration::from_millis(100));
1013 assert!(
1014 early_event.is_none(),
1015 "update event arrived before the figment map changed"
1016 );
1017
1018 drop(figment_guard);
1019
1020 let event = recv_with_timeout(&mut rx, std::time::Duration::from_secs(2))
1021 .expect("timed out waiting for observed update");
1022 assert_eq!(event.key, "observed");
1023 assert_eq!(cfg.get_typed::<String>("observed").unwrap(), "new");
1024
1025 let _ = stop_tx.send(());
1026 runtime_thread.join().unwrap();
1027 }
1028
1029 #[tokio::test]
1030 async fn environment_precedence_over_dynamic() {
1031 let (cfg, sender) = ConfigurationLoader::for_tests(
1032 Some(serde_json::json!({
1033 "foo": "bar",
1034 "baz": 5,
1035 "foobar": { "a": false, "b": "c" }
1036 })),
1037 Some(&[("ENV_VAR".to_string(), "from_env".to_string())]),
1038 Some(ConfigUpdate::snapshot([ConfigSetting::explicit(
1039 "env_var",
1040 serde_json::json!("from_snapshot_env_var"),
1041 )])),
1042 )
1043 .await;
1044 let sender = sender.expect("sender should exist");
1045
1046 assert_eq!(cfg.get_typed::<String>("env_var").unwrap(), "from_env");
1048
1049 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
1050
1051 sender
1053 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1054 "env_var",
1055 serde_json::json!("from_partial"),
1056 )))
1057 .await
1058 .unwrap();
1059
1060 sender
1062 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1063 "foobar.a",
1064 serde_json::json!(false),
1065 )))
1066 .await
1067 .unwrap();
1068
1069 sender
1071 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1072 "dummy",
1073 serde_json::json!(1),
1074 )))
1075 .await
1076 .unwrap();
1077
1078 tokio::time::timeout(std::time::Duration::from_secs(2), async {
1079 loop {
1080 match rx.recv().await {
1081 Ok(ev) if ev.key == "dummy" => break,
1082 Err(e) => panic!("updates channel closed: {e}"),
1083 Ok(_) => continue,
1084 }
1085 }
1086 })
1087 .await
1088 .expect("timed out waiting for sync marker");
1089
1090 assert_eq!(cfg.get_typed::<String>("env_var").unwrap(), "from_env");
1091 }
1092
1093 #[tokio::test]
1094 async fn dynamic_configuration_add_new_nested_key() {
1095 let (cfg, sender) = ConfigurationLoader::for_tests(
1096 Some(serde_json::json!({
1097 "foo": "bar",
1098 "baz": 5,
1099 "foobar": { "a": false, "b": "c" }
1100 })),
1101 None,
1102 Some(ConfigUpdate::snapshot([])),
1103 )
1104 .await;
1105 let sender = sender.expect("sender should exist");
1106
1107 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
1108
1109 sender
1110 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1111 "new_parent.new_child",
1112 serde_json::json!(42),
1113 )))
1114 .await
1115 .unwrap();
1116
1117 tokio::time::timeout(std::time::Duration::from_secs(2), async {
1119 loop {
1120 match rx.recv().await {
1121 Ok(ev) if ev.key == "new_parent" => break ev,
1122 Err(e) => panic!("updates channel closed: {e}"),
1123 Ok(_) => continue,
1124 }
1125 }
1126 })
1127 .await
1128 .expect("timed out waiting for new_parent.new_child update");
1129
1130 assert_eq!(cfg.get_typed::<i32>("new_parent.new_child").unwrap(), 42);
1131 }
1132
1133 #[tokio::test]
1134 async fn underscore_fallback_on_get() {
1135 let (cfg, _) = ConfigurationLoader::for_tests(
1136 Some(serde_json::json!({})),
1137 Some(&[("RANDOM_KEY".to_string(), "from_env_only".to_string())]),
1138 None,
1139 )
1140 .await;
1141
1142 assert_eq!(cfg.get_typed::<String>("random.key").unwrap(), "from_env_only");
1143 }
1144
1145 #[tokio::test]
1146 async fn underscore_fallback_on_get_multi_segment_key() {
1147 let (cfg, _) = ConfigurationLoader::for_tests(
1152 Some(serde_json::json!({})),
1153 Some(&[(
1154 "DATA_PLANE_API_LISTEN_ADDRESS".to_string(),
1155 "tcp://0.0.0.0:55100".to_string(),
1156 )]),
1157 None,
1158 )
1159 .await;
1160
1161 assert_eq!(
1162 cfg.try_get_typed::<String>("data_plane.api_listen_address").unwrap(),
1163 Some("tcp://0.0.0.0:55100".to_string()),
1164 );
1165 }
1166
1167 #[tokio::test]
1168 async fn static_configuration_has_no_updates() {
1169 let (cfg, maybe_sender) = ConfigurationLoader::for_tests(Some(serde_json::json!({})), None, None).await;
1170 assert!(maybe_sender.is_none());
1171 assert!(cfg.subscribe_for_updates().is_none());
1172 }
1173
1174 #[tokio::test]
1175 async fn updater_applies_each_update_before_returning() {
1176 let (cfg, updater) = ConfigurationLoader::default()
1177 .with_dynamic_configuration(ConfigUpdate::snapshot([ConfigSetting::explicit(
1178 "key",
1179 serde_json::json!("from_snapshot"),
1180 )]))
1181 .into_generic()
1182 .await
1183 .expect("should not fail to build generic configuration");
1184 let mut updater = updater.expect("dynamic configuration should return an updater");
1185 assert_eq!(cfg.get_typed::<String>("key").unwrap(), "from_snapshot");
1186
1187 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
1188 updater
1189 .apply(ConfigUpdate::Partial(ConfigSetting::explicit(
1190 "key",
1191 serde_json::json!("from_update"),
1192 )))
1193 .expect("should not fail to apply the update");
1194
1195 assert_eq!(cfg.get_typed::<String>("key").unwrap(), "from_update");
1197 let event = rx.try_recv().expect("the change event should already be sent");
1198 assert_eq!(event.key, "key");
1199 }
1200
1201 #[tokio::test]
1202 async fn flattened_keys_flat_and_nested() {
1203 let (cfg, _) = ConfigurationLoader::for_tests(
1204 Some(serde_json::json!({
1205 "top": "value",
1206 "nested": { "a": 1, "b": { "c": true } }
1207 })),
1208 None,
1209 None,
1210 )
1211 .await;
1212
1213 let pairs = cfg.flattened_keys().unwrap();
1214 let map: std::collections::HashMap<&str, &serde_json::Value> =
1215 pairs.iter().map(|(k, v)| (k.as_str(), v)).collect();
1216
1217 assert_eq!(map.get("top"), Some(&&serde_json::json!("value")));
1218 assert_eq!(map.get("nested.a"), Some(&&serde_json::json!(1)));
1219 assert_eq!(map.get("nested.b.c"), Some(&&serde_json::json!(true)));
1220 assert!(!map.contains_key("nested"));
1221 assert!(!map.contains_key("nested.b"));
1222 }
1223
1224 #[tokio::test]
1225 async fn flattened_keys_arrays_are_leaves() {
1226 let (cfg, _) = ConfigurationLoader::for_tests(
1227 Some(serde_json::json!({
1228 "tags": ["a", "b"],
1229 "matrix": [[1, 2], [3, 4]]
1230 })),
1231 None,
1232 None,
1233 )
1234 .await;
1235
1236 let pairs = cfg.flattened_keys().unwrap();
1237 let map: std::collections::HashMap<&str, &serde_json::Value> =
1238 pairs.iter().map(|(k, v)| (k.as_str(), v)).collect();
1239
1240 assert_eq!(map.get("tags"), Some(&&serde_json::json!(["a", "b"])));
1241 assert_eq!(map.get("matrix"), Some(&&serde_json::json!([[1, 2], [3, 4]])));
1242 }
1243
1244 #[tokio::test]
1245 async fn flattened_keys_null_values_absent() {
1246 let (cfg, _) = ConfigurationLoader::for_tests(
1247 Some(serde_json::json!({
1248 "present": "yes",
1249 "absent": null
1250 })),
1251 None,
1252 None,
1253 )
1254 .await;
1255
1256 let pairs = cfg.flattened_keys().unwrap();
1257 let map: std::collections::HashMap<&str, &serde_json::Value> =
1258 pairs.iter().map(|(k, v)| (k.as_str(), v)).collect();
1259
1260 assert_eq!(map.get("present"), Some(&&serde_json::json!("yes")));
1261 assert!(!map.contains_key("absent"));
1263 }
1264
1265 #[tokio::test]
1266 async fn from_yaml_loads_configuration_file() {
1267 use std::io::Write as _;
1268
1269 let mut file = tempfile::NamedTempFile::new().expect("should create temp file");
1270 file.write_all(b"top: value\nnested:\n inner: 7\n")
1271 .expect("should write temp file");
1272 file.flush().expect("should flush temp file");
1273
1274 let (cfg, _) = ConfigurationLoader::default()
1275 .from_yaml(file.path())
1276 .expect("YAML file should load")
1277 .into_generic()
1278 .await
1279 .expect("should build generic configuration");
1280
1281 assert_eq!(cfg.get_typed::<String>("top").unwrap(), "value");
1282 assert_eq!(cfg.get_typed::<i64>("nested.inner").unwrap(), 7);
1283 }
1284
1285 #[tokio::test]
1286 async fn try_from_yaml_ignores_unreadable_file() {
1287 let (cfg, _) = ConfigurationLoader::default()
1290 .try_from_yaml("/nonexistent/definitely/not/here.yaml")
1291 .into_generic()
1292 .await
1293 .expect("should build generic configuration even when the file is missing");
1294
1295 assert!(matches!(
1296 cfg.get::<String>("anything"),
1297 Err(ConfigurationError::MissingField { .. })
1298 ));
1299 }
1300
1301 #[tokio::test]
1302 async fn from_json_returns_error_for_invalid_file() {
1303 use std::io::Write as _;
1304
1305 let mut file = tempfile::NamedTempFile::new().expect("should create temp file");
1306 file.write_all(b"{ not valid json ").expect("should write temp file");
1307 file.flush().expect("should flush temp file");
1308
1309 let result = ConfigurationLoader::default().from_json(file.path());
1310 assert!(result.is_err(), "invalid JSON should fail to load at the loader level");
1311 }
1312}