1#![deny(warnings)]
3#![deny(missing_docs)]
4
5use std::sync::{Arc, OnceLock, 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, mpsc, oneshot, Mutex};
18use tracing::{debug, error};
19
20pub mod duration_string;
21pub mod dynamic;
22mod provider;
23pub mod space_separated;
24
25pub use self::duration_string::{parse_duration, DurationString, ParseDurationError};
26pub use self::dynamic::FieldUpdateWatcher;
27use self::dynamic::{ConfigChangeEvent, ConfigUpdate};
28use self::provider::ResolvedProvider;
29pub use self::space_separated::{deserialize_opt_space_separated_or_seq, deserialize_space_separated_or_seq};
30
31#[derive(Clone)]
32struct ArcProvider(Arc<dyn Provider + Send + Sync>);
33
34impl Provider for ArcProvider {
35 fn metadata(&self) -> figment::Metadata {
36 self.0.metadata()
37 }
38
39 fn data(&self) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
40 self.0.data()
41 }
42}
43
44enum ProviderSource {
45 Static(ArcProvider),
46 Dynamic(Option<mpsc::Receiver<ConfigUpdate>>),
47}
48
49impl Clone for ProviderSource {
50 fn clone(&self) -> Self {
51 match self {
52 Self::Static(p) => Self::Static(p.clone()),
53 Self::Dynamic(_) => Self::Dynamic(None),
54 }
55 }
56}
57
58#[derive(Debug, Snafu)]
60#[snafu(context(suffix(false)))]
61pub enum ConfigurationError {
62 #[snafu(display("Environment variable prefix must not be empty."))]
64 EmptyPrefix,
65
66 #[snafu(display("Missing field '{}' in configuration. {}", field, help_text))]
68 MissingField {
69 help_text: String,
74
75 field: Cow<'static, str>,
77 },
78
79 #[snafu(display(
81 "Expected value for field '{}' to be '{}', got '{}' instead.",
82 field,
83 expected_ty,
84 actual_ty
85 ))]
86 InvalidFieldType {
87 field: String,
91
92 expected_ty: String,
94
95 actual_ty: String,
97 },
98
99 #[snafu(transparent)]
101 Generic {
102 source: GenericError,
104 },
105}
106
107impl From<figment::Error> for ConfigurationError {
108 fn from(e: figment::Error) -> Self {
109 match e.kind {
110 Kind::InvalidType(actual_ty, expected_ty) => Self::InvalidFieldType {
111 field: e.path.join("."),
112 expected_ty,
113 actual_ty: actual_ty.to_string(),
114 },
115 _ => Self::Generic { source: e.into() },
116 }
117 }
118}
119
120#[derive(Clone, Debug, Eq, Hash, PartialEq)]
121enum LookupSource {
122 Environment { prefix: String },
124}
125
126impl LookupSource {
127 fn transform_key(&self, key: &str) -> String {
128 match self {
129 LookupSource::Environment { prefix } => format!("{}{}", prefix, key.replace('.', "_").to_uppercase()),
132 }
133 }
134}
135
136#[derive(Clone, Default)]
152pub struct ConfigurationLoader {
153 key_aliases: &'static [(&'static str, &'static str)],
154 lookup_sources: HashSet<LookupSource>,
155 provider_sources: Vec<ProviderSource>,
156}
157
158impl ConfigurationLoader {
159 pub fn with_key_aliases(mut self, aliases: &'static [(&'static str, &'static str)]) -> Self {
168 self.key_aliases = aliases;
169 self
170 }
171
172 pub fn add_providers<P, I>(mut self, providers: I) -> Self
182 where
183 P: Provider + Send + Sync + 'static,
184 I: IntoIterator<Item = P>,
185 {
186 for p in providers {
187 self.provider_sources
188 .push(ProviderSource::Static(ArcProvider(Arc::new(p))));
189 }
190 self
191 }
192
193 pub fn from_yaml<P>(mut self, path: P) -> Result<Self, ConfigurationError>
199 where
200 P: AsRef<std::path::Path>,
201 {
202 let resolved_provider = ResolvedProvider::from_yaml(&path, self.key_aliases)?;
203 self.provider_sources
204 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
205 Ok(self)
206 }
207
208 pub fn try_from_yaml<P>(mut self, path: P) -> Self
212 where
213 P: AsRef<std::path::Path>,
214 {
215 match ResolvedProvider::from_yaml(&path, self.key_aliases) {
216 Ok(resolved_provider) => {
217 self.provider_sources
218 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
219 }
220 Err(e) => {
221 println!(
222 "Unable to read YAML configuration file '{}': {}. Ignoring.",
223 path.as_ref().to_string_lossy(),
224 e
225 );
226 }
227 }
228 self
229 }
230
231 pub fn from_json<P>(mut self, path: P) -> Result<Self, ConfigurationError>
237 where
238 P: AsRef<std::path::Path>,
239 {
240 let resolved_provider = ResolvedProvider::from_json(&path, self.key_aliases)?;
241 self.provider_sources
242 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
243 Ok(self)
244 }
245
246 pub fn try_from_json<P>(mut self, path: P) -> Self
250 where
251 P: AsRef<std::path::Path>,
252 {
253 match ResolvedProvider::from_json(&path, self.key_aliases) {
254 Ok(resolved_provider) => {
255 self.provider_sources
256 .push(ProviderSource::Static(ArcProvider(Arc::new(resolved_provider))));
257 }
258 Err(e) => {
259 println!(
260 "Unable to read JSON configuration file '{}': {}. Ignoring.",
261 path.as_ref().to_string_lossy(),
262 e
263 );
264 }
265 }
266 self
267 }
268
269 pub fn from_environment(mut self, prefix: &'static str) -> Result<Self, ConfigurationError> {
282 if prefix.is_empty() {
283 return Err(ConfigurationError::EmptyPrefix);
284 }
285
286 let prefix = if prefix.ends_with('_') {
287 prefix.to_string()
288 } else {
289 format!("{}_", prefix)
290 };
291
292 let env = Env::prefixed(&prefix).split("__");
294 let values = env.data().unwrap();
295 if let Some(default_dict) = values.get(&figment::Profile::Default) {
296 self.provider_sources
297 .push(ProviderSource::Static(ArcProvider(Arc::new(Serialized::defaults(
298 default_dict.clone(),
299 )))));
300 self.lookup_sources.insert(LookupSource::Environment { prefix });
301 }
302 Ok(self)
303 }
304
305 pub fn with_dynamic_configuration(mut self, receiver: mpsc::Receiver<ConfigUpdate>) -> Self {
309 self.provider_sources.push(ProviderSource::Dynamic(Some(receiver)));
310 self
311 }
312
313 pub fn into_typed<'a, T>(self) -> Result<T, ConfigurationError>
319 where
320 T: Deserialize<'a>,
321 {
322 let figment = build_figment_from_sources(&self.provider_sources);
323 figment.extract().map_err(Into::into)
324 }
325
326 pub fn bootstrap_generic(&self) -> GenericConfiguration {
331 let figment = build_figment_from_sources(&self.provider_sources);
332
333 GenericConfiguration {
334 inner: Arc::new(Inner {
335 figment: RwLock::new(figment),
336 lookup_sources: self.lookup_sources.clone(),
337 event_sender: None,
338 ready_signal: Mutex::new(None),
339 }),
340 }
341 }
342
343 pub async fn into_generic(mut self) -> Result<GenericConfiguration, ConfigurationError> {
345 let has_dynamic_provider = self
346 .provider_sources
347 .iter()
348 .any(|s| matches!(s, ProviderSource::Dynamic(_)));
349
350 if has_dynamic_provider {
351 let mut receiver_opt = None;
352 for source in self.provider_sources.iter_mut() {
353 if let ProviderSource::Dynamic(ref mut receiver) = source {
354 receiver_opt = receiver.take();
355 break;
356 }
357 }
358 let receiver = receiver_opt.expect("Dynamic receiver should exist but was not found");
359
360 let figment = build_figment_from_sources(&self.provider_sources);
362
363 let (event_sender, _) = broadcast::channel(100);
364 let (ready_sender, ready_receiver) = oneshot::channel();
365
366 let generic_config = GenericConfiguration {
367 inner: Arc::new(Inner {
368 figment: RwLock::new(figment),
369 lookup_sources: self.lookup_sources,
370 event_sender: Some(event_sender.clone()),
371 ready_signal: Mutex::new(Some(ready_receiver)),
372 }),
373 };
374
375 tokio::spawn(run_dynamic_config_updater(
377 generic_config.inner.clone(),
378 receiver,
379 self.provider_sources,
380 event_sender,
381 ready_sender,
382 ));
383
384 Ok(generic_config)
385 } else {
386 let figment = build_figment_from_sources(&self.provider_sources);
388
389 Ok(GenericConfiguration {
390 inner: Arc::new(Inner {
391 figment: RwLock::new(figment),
392 lookup_sources: self.lookup_sources,
393 event_sender: None,
394 ready_signal: Mutex::new(None),
395 }),
396 })
397 }
398 }
399
400 pub async fn for_tests(
411 file_values: Option<serde_json::Value>, env_vars: Option<&[(String, String)]>,
412 enable_dynamic_configuration: bool,
413 ) -> (GenericConfiguration, Option<tokio::sync::mpsc::Sender<ConfigUpdate>>) {
414 Self::for_tests_with_provider_factory(file_values, env_vars, enable_dynamic_configuration, &[], || {
415 Serialized::defaults(serde_json::json!({}))
416 })
417 .await
418 }
419
420 pub async fn for_tests_with_provider_factory<P, F>(
429 file_values: Option<serde_json::Value>, env_vars: Option<&[(String, String)]>,
430 enable_dynamic_configuration: bool, key_aliases: &'static [(&'static str, &'static str)], provider_factory: F,
431 ) -> (GenericConfiguration, Option<tokio::sync::mpsc::Sender<ConfigUpdate>>)
432 where
433 P: Provider + Send + Sync + 'static,
434 F: FnOnce() -> P,
435 {
436 let json_file = tempfile::NamedTempFile::new().expect("should not fail to create temp file.");
437 let path = &json_file.path();
438 let json_to_write = file_values.unwrap_or(serde_json::json!({}));
439 serde_json::to_writer(&json_file, &json_to_write).expect("should not fail to write to temp file.");
440
441 let mut loader = ConfigurationLoader::default()
442 .with_key_aliases(key_aliases)
443 .try_from_json(path);
444 let mut maybe_sender = None;
445 if enable_dynamic_configuration {
446 let (sender, receiver) = tokio::sync::mpsc::channel(1);
447 loader = loader.with_dynamic_configuration(receiver);
448 maybe_sender = Some(sender);
449 }
450
451 static ENV_MUTEX: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
452
453 let guard = ENV_MUTEX.get_or_init(|| std::sync::Mutex::new(())).lock().unwrap();
454
455 if let Some(pairs) = env_vars.as_ref() {
456 for (k, v) in pairs.iter() {
457 std::env::set_var(k, v);
461 std::env::set_var(format!("TEST_{}", k), v);
462 }
463 }
464
465 let loader = loader.add_providers([provider_factory()]);
467
468 let loader = loader
470 .from_environment("TEST")
471 .expect("should not fail to add environment provider");
472
473 if let Some(pairs) = env_vars.as_ref() {
475 for (k, _) in pairs.iter() {
476 std::env::remove_var(k);
477 std::env::remove_var(format!("TEST_{}", k));
478 }
479 }
480
481 drop(guard);
482
483 let cfg = loader
484 .into_generic()
485 .await
486 .expect("should not fail to build generic configuration");
487
488 (cfg, maybe_sender)
489 }
490}
491
492fn build_figment_from_sources(sources: &[ProviderSource]) -> Figment {
493 sources.iter().fold(Figment::new(), |figment, source| match source {
494 ProviderSource::Static(p) => figment.admerge(p.clone()),
495 ProviderSource::Dynamic(_) => figment,
497 })
498}
499
500pub fn upsert(root: &mut serde_json::Value, key: &str, value: serde_json::Value) {
504 if !root.is_object() {
505 *root = serde_json::Value::Object(serde_json::Map::new());
506 }
507
508 let mut current = root;
509 let mut segments = key.split('.').peekable();
511
512 while let Some(seg) = segments.next() {
513 let is_leaf = segments.peek().is_none();
514
515 if !current.is_object() {
517 *current = serde_json::Value::Object(serde_json::Map::new());
518 }
519 let node = current.as_object_mut().expect("current node should be an object");
520
521 if is_leaf {
522 node.insert(seg.to_string(), value);
523 break;
524 } else {
525 let should_create_node = match node.get(seg) {
527 Some(v) => !v.is_object(),
528 None => true,
529 };
530 if should_create_node {
532 node.insert(seg.to_string(), serde_json::Value::Object(serde_json::Map::new()));
533 }
534
535 current = node.get_mut(seg).expect("should not fail to get nested object");
537 }
538 }
539}
540
541async fn run_dynamic_config_updater(
542 inner: Arc<Inner>, mut receiver: mpsc::Receiver<ConfigUpdate>, provider_sources: Vec<ProviderSource>,
543 sender: broadcast::Sender<ConfigChangeEvent>, ready_sender: oneshot::Sender<()>,
544) {
545 let initial_update = match receiver.recv().await {
547 Some(update) => update,
548 None => {
549 debug!("Dynamic configuration channel closed before initial snapshot.");
551 return;
552 }
553 };
554
555 let mut dynamic_state = match initial_update {
556 ConfigUpdate::Snapshot(state) => state,
557 ConfigUpdate::Partial { .. } => {
558 error!("First dynamic config message was not a snapshot. Updater may be in an inconsistent state.");
560 serde_json::Value::Null
561 }
562 };
563
564 let new_figment = provider_sources
566 .iter()
567 .fold(Figment::new(), |figment, source| match source {
568 ProviderSource::Static(p) => figment.admerge(p.clone()),
569 ProviderSource::Dynamic(_) => {
570 figment.admerge(figment::providers::Serialized::defaults(dynamic_state.clone()))
571 }
572 });
573
574 {
576 let mut figment_guard = inner.figment.write().unwrap();
577 *figment_guard = new_figment.clone();
578 }
579
580 if ready_sender.send(()).is_err() {
582 debug!("Configuration readiness receiver dropped. Updater task shutting down.");
583 return;
584 }
585
586 let mut current_config: figment::value::Value = new_figment.extract().unwrap();
588
589 loop {
591 let update = match receiver.recv().await {
592 Some(update) => update,
593 None => {
594 debug!("Dynamic configuration update channel closed. Updater task shutting down.");
596 return;
597 }
598 };
599
600 match update {
602 ConfigUpdate::Snapshot(new_state) => {
603 debug!("Received configuration snapshot update.");
604 dynamic_state = new_state;
605 }
606 ConfigUpdate::Partial { key, value } => {
607 debug!(%key, "Received partial configuration update.");
608 if dynamic_state.is_null() {
609 dynamic_state = serde_json::Value::Object(serde_json::Map::new());
610 }
611 if dynamic_state.is_object() {
612 upsert(&mut dynamic_state, &key, value);
613 } else {
614 error!(
615 "Received partial update but current dynamic state is not an object. This should not happen."
616 );
617 }
618 }
619 }
620
621 let new_figment = provider_sources
623 .iter()
624 .fold(Figment::new(), |figment, source| match source {
625 ProviderSource::Static(p) => figment.admerge(p.clone()),
626 ProviderSource::Dynamic(_) => {
627 figment.admerge(figment::providers::Serialized::defaults(dynamic_state.clone()))
628 }
629 });
630
631 let new_config: figment::value::Value = new_figment.clone().extract().unwrap();
632
633 if current_config != new_config {
634 let changes = dynamic::diff_config(¤t_config, &new_config);
635
636 {
637 let mut figment_guard = inner.figment.write().unwrap_or_else(|e| {
638 error!("Failed to acquire write lock for dynamic configuration: {}", e);
639 e.into_inner()
640 });
641 *figment_guard = new_figment;
642 }
643
644 for change in changes {
645 let _ = sender.send(change);
649 }
650
651 current_config = new_config;
653 }
654 }
655}
656
657#[derive(Debug)]
658struct Inner {
659 figment: RwLock<Figment>,
660 lookup_sources: HashSet<LookupSource>,
661 event_sender: Option<broadcast::Sender<ConfigChangeEvent>>,
662 ready_signal: Mutex<Option<oneshot::Receiver<()>>>,
663}
664
665#[derive(Clone, Debug)]
687pub struct GenericConfiguration {
688 inner: Arc<Inner>,
689}
690
691impl GenericConfiguration {
692 pub async fn ready(&self) {
699 let mut maybe_ready_rx = self.inner.ready_signal.lock().await;
702 if let Some(ready_rx) = maybe_ready_rx.take() {
703 saluki_antithesis::reachable!("config readiness wait entered");
708
709 let ready_result = ready_rx.await;
710
711 if ready_result.is_err() {
712 saluki_antithesis::unreachable!(
713 "config readiness sender dropped before signalling — updater task may have panicked"
714 );
715 error!("Failed to receive configuration readiness signal; updater task may have panicked.");
716 } else {
717 saluki_antithesis::sometimes!(true, "config readiness signal received");
718 }
719 }
720 }
721
722 fn get<'a, T>(&self, key: &str) -> Result<T, ConfigurationError>
723 where
724 T: Deserialize<'a>,
725 {
726 let figment_guard = self.inner.figment.read().unwrap();
727 match figment_guard.extract_inner(key) {
728 Ok(value) => Ok(value),
729 Err(e) => {
730 if matches!(e.kind, figment::error::Kind::MissingField(_)) {
731 let fallback_key = key.replace('.', "_");
736 figment_guard
737 .extract_inner(&fallback_key)
738 .map_err(|fallback_e| from_figment_error(&self.inner.lookup_sources, fallback_e))
739 } else {
740 Err(e.into())
741 }
742 }
743 }
744 }
745
746 pub fn get_typed<'a, T>(&self, key: &str) -> Result<T, ConfigurationError>
755 where
756 T: Deserialize<'a>,
757 {
758 self.get(key)
759 }
760
761 pub fn get_typed_or_default<'a, T>(&self, key: &str) -> T
768 where
769 T: Default + Deserialize<'a>,
770 {
771 self.get(key).unwrap_or_default()
772 }
773
774 pub fn try_get_typed<'a, T>(&self, key: &str) -> Result<Option<T>, ConfigurationError>
785 where
786 T: Deserialize<'a>,
787 {
788 match self.get(key) {
789 Ok(value) => Ok(Some(value)),
790 Err(ConfigurationError::MissingField { .. }) => Ok(None),
791 Err(e) => Err(e),
792 }
793 }
794
795 pub fn as_typed<'a, T>(&self) -> Result<T, ConfigurationError>
801 where
802 T: Deserialize<'a>,
803 {
804 self.inner
805 .figment
806 .read()
807 .unwrap()
808 .extract()
809 .map_err(|e| from_figment_error(&self.inner.lookup_sources, e))
810 }
811
812 pub fn subscribe_for_updates(&self) -> Option<broadcast::Receiver<dynamic::ConfigChangeEvent>> {
814 self.inner.event_sender.as_ref().map(|s| s.subscribe())
815 }
816
817 pub fn flattened_keys(&self) -> Result<Vec<(String, serde_json::Value)>, ConfigurationError> {
827 let root: serde_json::Value = self.as_typed()?;
828 let mut out = Vec::new();
829 flatten_value(&root, &mut String::new(), &mut out);
830 Ok(out)
831 }
832
833 pub fn watch_for_updates(&self, key: &str) -> FieldUpdateWatcher {
838 FieldUpdateWatcher {
839 key: key.to_string(),
840 rx: self.subscribe_for_updates(),
841 }
842 }
843}
844
845fn flatten_value(value: &serde_json::Value, prefix: &mut String, out: &mut Vec<(String, serde_json::Value)>) {
847 if let serde_json::Value::Object(map) = value {
848 for (key, child) in map {
849 let prev_len = prefix.len();
850 if !prefix.is_empty() {
851 prefix.push('.');
852 }
853 prefix.push_str(key);
854 flatten_value(child, prefix, out);
855 prefix.truncate(prev_len);
856 }
857 } else {
858 out.push((prefix.clone(), value.clone()));
859 }
860}
861
862fn from_figment_error(lookup_sources: &HashSet<LookupSource>, e: figment::Error) -> ConfigurationError {
863 match e.kind {
864 Kind::MissingField(field) => {
865 let mut valid_keys = lookup_sources
866 .iter()
867 .map(|source| source.transform_key(&field))
868 .collect::<Vec<_>>();
869
870 valid_keys.insert(0, field.to_string());
872
873 let help_text = format!("Try setting `{}`.", valid_keys.join("` or `"));
874
875 ConfigurationError::MissingField { help_text, field }
876 }
877 Kind::InvalidType(actual_ty, expected_ty) => ConfigurationError::InvalidFieldType {
878 field: e.path.join("."),
879 expected_ty,
880 actual_ty: actual_ty.to_string(),
881 },
882 _ => ConfigurationError::Generic { source: e.into() },
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[tokio::test]
891 async fn test_static_configuration() {
892 let (cfg, _) = ConfigurationLoader::for_tests(
893 Some(serde_json::json!({
894 "foo": "bar",
895 "baz": 5,
896 "foobar": { "a": false, "b": "c" }
897 })),
898 Some(&[("ENV_VAR".to_string(), "from_env".to_string())]),
899 false,
900 )
901 .await;
902 cfg.ready().await;
903
904 assert_eq!(cfg.get_typed::<String>("foo").unwrap(), "bar");
905 assert_eq!(cfg.get_typed::<i32>("baz").unwrap(), 5);
906 assert!(!cfg.get_typed::<bool>("foobar.a").unwrap());
907 assert_eq!(cfg.get_typed::<String>("env_var").unwrap(), "from_env");
908 assert!(matches!(
909 cfg.get::<String>("nonexistentKey"),
910 Err(ConfigurationError::MissingField { .. })
911 ));
912 }
913
914 #[tokio::test]
915 async fn test_dynamic_configuration() {
916 let (cfg, sender) = ConfigurationLoader::for_tests(
917 Some(serde_json::json!({
918 "foo": "bar",
919 "baz": 5,
920 "foobar": { "a": false, "b": "c" }
921 })),
922 Some(&[("ENV_VAR".to_string(), "from_env".to_string())]),
923 true,
924 )
925 .await;
926 let sender = sender.expect("sender should exist");
927 sender
928 .send(ConfigUpdate::Snapshot(serde_json::json!({
929 "new": "from_snapshot",
930 })))
931 .await
932 .unwrap();
933
934 cfg.ready().await;
935
936 assert_eq!(cfg.get_typed::<String>("foo").unwrap(), "bar");
938
939 assert_eq!(cfg.get_typed::<String>("new").unwrap(), "from_snapshot");
941
942 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
943
944 sender
945 .send(ConfigUpdate::Partial {
946 key: "new_key".to_string(),
947 value: "from dynamic update".to_string().into(),
948 })
949 .await
950 .unwrap();
951
952 tokio::time::timeout(std::time::Duration::from_secs(2), async {
953 loop {
954 match rx.recv().await {
955 Ok(ev) if ev.key == "new_key" => break ev,
956 Err(e) => panic!("updates channel closed: {e}"),
957 Ok(_) => continue,
958 }
959 }
960 })
961 .await
962 .expect("timed out waiting for new_key update");
963
964 assert_eq!(cfg.get_typed::<String>("new_key").unwrap(), "from dynamic update");
965
966 sender
968 .send(ConfigUpdate::Partial {
969 key: "foobar.a".to_string(),
970 value: serde_json::json!(true),
971 })
972 .await
973 .unwrap();
974
975 tokio::time::timeout(std::time::Duration::from_secs(2), async {
976 loop {
977 match rx.recv().await {
978 Ok(ev) if ev.key == "foobar.a" => break ev,
979 Err(e) => panic!("updates channel closed: {e}"),
980 Ok(_) => continue,
981 }
982 }
983 })
984 .await
985 .expect("timed out waiting for foobar.a update");
986
987 assert!(cfg.get_typed::<bool>("foobar.a").unwrap());
988 assert_eq!(cfg.get_typed::<String>("foobar.b").unwrap(), "c");
989 }
990
991 #[test]
992 fn update_events_are_sent_after_the_figment_map_is_updated() {
993 fn recv_with_timeout(
994 rx: &mut tokio::sync::broadcast::Receiver<ConfigChangeEvent>, timeout: std::time::Duration,
995 ) -> Option<ConfigChangeEvent> {
996 let deadline = std::time::Instant::now() + timeout;
997 loop {
998 match rx.try_recv() {
999 Ok(event) => return Some(event),
1000 Err(tokio::sync::broadcast::error::TryRecvError::Empty) if std::time::Instant::now() < deadline => {
1001 std::thread::sleep(std::time::Duration::from_millis(1));
1002 }
1003 Err(tokio::sync::broadcast::error::TryRecvError::Empty) => return None,
1004 Err(e) => panic!("updates channel failed: {e}"),
1005 }
1006 }
1007 }
1008
1009 let runtime = tokio::runtime::Builder::new_current_thread().build().unwrap();
1010 let (cfg, sender) = runtime.block_on(async {
1011 let (cfg, sender) = ConfigurationLoader::for_tests(None, None, true).await;
1012 let sender = sender.expect("sender should exist");
1013 sender
1014 .send(ConfigUpdate::Snapshot(serde_json::json!({ "observed": "old" })))
1015 .await
1016 .unwrap();
1017 cfg.ready().await;
1018 (cfg, sender)
1019 });
1020
1021 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
1022 let (started_tx, started_rx) = std::sync::mpsc::channel();
1023 let runtime_thread = std::thread::spawn(move || {
1024 runtime.block_on(async {
1025 started_tx.send(()).unwrap();
1026 let _ = stop_rx.await;
1027 });
1028 });
1029 started_rx.recv().unwrap();
1030
1031 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
1032 let figment_guard = cfg.inner.figment.read().unwrap();
1033
1034 sender
1035 .blocking_send(ConfigUpdate::Partial {
1036 key: "observed".to_string(),
1037 value: serde_json::json!("new"),
1038 })
1039 .unwrap();
1040
1041 let early_event = recv_with_timeout(&mut rx, std::time::Duration::from_millis(100));
1042 assert!(
1043 early_event.is_none(),
1044 "update event arrived before the figment map changed"
1045 );
1046
1047 drop(figment_guard);
1048
1049 let event = recv_with_timeout(&mut rx, std::time::Duration::from_secs(2))
1050 .expect("timed out waiting for observed update");
1051 assert_eq!(event.key, "observed");
1052 assert_eq!(cfg.get_typed::<String>("observed").unwrap(), "new");
1053
1054 let _ = stop_tx.send(());
1055 runtime_thread.join().unwrap();
1056 }
1057
1058 #[tokio::test]
1059 async fn test_environment_precedence_over_dynamic() {
1060 let (cfg, sender) = ConfigurationLoader::for_tests(
1061 Some(serde_json::json!({
1062 "foo": "bar",
1063 "baz": 5,
1064 "foobar": { "a": false, "b": "c" }
1065 })),
1066 Some(&[("ENV_VAR".to_string(), "from_env".to_string())]),
1067 true,
1068 )
1069 .await;
1070 let sender = sender.expect("sender should exist");
1071
1072 sender
1073 .send(ConfigUpdate::Snapshot(serde_json::json!({
1074 "env_var": "from_snapshot_env_var"
1075 })))
1076 .await
1077 .unwrap();
1078
1079 cfg.ready().await;
1080
1081 assert_eq!(cfg.get_typed::<String>("env_var").unwrap(), "from_env");
1083
1084 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
1085
1086 sender
1088 .send(ConfigUpdate::Partial {
1089 key: "env_var".to_string(),
1090 value: serde_json::json!("from_partial"),
1091 })
1092 .await
1093 .unwrap();
1094
1095 sender
1097 .send(ConfigUpdate::Partial {
1098 key: "foobar.a".to_string(),
1099 value: serde_json::json!(false),
1100 })
1101 .await
1102 .unwrap();
1103
1104 sender
1106 .send(ConfigUpdate::Partial {
1107 key: "dummy".to_string(),
1108 value: serde_json::json!(1),
1109 })
1110 .await
1111 .unwrap();
1112
1113 tokio::time::timeout(std::time::Duration::from_secs(2), async {
1114 loop {
1115 match rx.recv().await {
1116 Ok(ev) if ev.key == "dummy" => break,
1117 Err(e) => panic!("updates channel closed: {e}"),
1118 Ok(_) => continue,
1119 }
1120 }
1121 })
1122 .await
1123 .expect("timed out waiting for sync marker");
1124
1125 assert_eq!(cfg.get_typed::<String>("env_var").unwrap(), "from_env");
1126 }
1127
1128 #[tokio::test]
1129 async fn test_dynamic_configuration_add_new_nested_key() {
1130 let (cfg, sender) = ConfigurationLoader::for_tests(
1131 Some(serde_json::json!({
1132 "foo": "bar",
1133 "baz": 5,
1134 "foobar": { "a": false, "b": "c" }
1135 })),
1136 None,
1137 true,
1138 )
1139 .await;
1140 let sender = sender.expect("sender should exist");
1141
1142 sender
1143 .send(ConfigUpdate::Snapshot(serde_json::json!({})))
1144 .await
1145 .unwrap();
1146 cfg.ready().await;
1147
1148 let mut rx = cfg.subscribe_for_updates().expect("dynamic updates should be enabled");
1149
1150 sender
1151 .send(ConfigUpdate::Partial {
1152 key: "new_parent.new_child".to_string(),
1153 value: serde_json::json!(42),
1154 })
1155 .await
1156 .unwrap();
1157
1158 tokio::time::timeout(std::time::Duration::from_secs(2), async {
1160 loop {
1161 match rx.recv().await {
1162 Ok(ev) if ev.key == "new_parent" => break ev,
1163 Err(e) => panic!("updates channel closed: {e}"),
1164 Ok(_) => continue,
1165 }
1166 }
1167 })
1168 .await
1169 .expect("timed out waiting for new_parent.new_child update");
1170
1171 assert_eq!(cfg.get_typed::<i32>("new_parent.new_child").unwrap(), 42);
1172 }
1173
1174 #[tokio::test]
1175 async fn test_underscore_fallback_on_get() {
1176 let (cfg, _) = ConfigurationLoader::for_tests(
1177 Some(serde_json::json!({})),
1178 Some(&[("RANDOM_KEY".to_string(), "from_env_only".to_string())]),
1179 false,
1180 )
1181 .await;
1182 cfg.ready().await;
1183
1184 assert_eq!(cfg.get_typed::<String>("random.key").unwrap(), "from_env_only");
1185 }
1186
1187 #[tokio::test]
1188 async fn test_underscore_fallback_on_get_multi_segment_key() {
1189 let (cfg, _) = ConfigurationLoader::for_tests(
1194 Some(serde_json::json!({})),
1195 Some(&[(
1196 "DATA_PLANE_API_LISTEN_ADDRESS".to_string(),
1197 "tcp://0.0.0.0:55100".to_string(),
1198 )]),
1199 false,
1200 )
1201 .await;
1202 cfg.ready().await;
1203
1204 assert_eq!(
1205 cfg.try_get_typed::<String>("data_plane.api_listen_address").unwrap(),
1206 Some("tcp://0.0.0.0:55100".to_string()),
1207 );
1208 }
1209
1210 #[tokio::test]
1211 async fn test_static_configuration_ready_and_subscribe() {
1212 let (cfg, maybe_sender) = ConfigurationLoader::for_tests(Some(serde_json::json!({})), None, false).await;
1213 assert!(maybe_sender.is_none());
1214
1215 tokio::time::timeout(std::time::Duration::from_millis(500), cfg.ready())
1216 .await
1217 .expect("ready() should not block when dynamic is disabled");
1218
1219 assert!(cfg.subscribe_for_updates().is_none());
1220 }
1221
1222 #[tokio::test]
1223 async fn test_dynamic_configuration_ready_requires_initial_snapshot() {
1224 let (cfg, maybe_sender) = ConfigurationLoader::for_tests(Some(serde_json::json!({})), None, true).await;
1226 assert!(maybe_sender.is_some());
1227
1228 let res = tokio::time::timeout(std::time::Duration::from_millis(1000), cfg.ready()).await;
1230 assert!(res.is_err(), "ready() should time out without an initial snapshot");
1231 }
1232
1233 #[tokio::test]
1234 async fn test_flattened_keys_flat_and_nested() {
1235 let (cfg, _) = ConfigurationLoader::for_tests(
1236 Some(serde_json::json!({
1237 "top": "value",
1238 "nested": { "a": 1, "b": { "c": true } }
1239 })),
1240 None,
1241 false,
1242 )
1243 .await;
1244 cfg.ready().await;
1245
1246 let pairs = cfg.flattened_keys().unwrap();
1247 let map: std::collections::HashMap<&str, &serde_json::Value> =
1248 pairs.iter().map(|(k, v)| (k.as_str(), v)).collect();
1249
1250 assert_eq!(map.get("top"), Some(&&serde_json::json!("value")));
1251 assert_eq!(map.get("nested.a"), Some(&&serde_json::json!(1)));
1252 assert_eq!(map.get("nested.b.c"), Some(&&serde_json::json!(true)));
1253 assert!(!map.contains_key("nested"));
1254 assert!(!map.contains_key("nested.b"));
1255 }
1256
1257 #[tokio::test]
1258 async fn test_flattened_keys_arrays_are_leaves() {
1259 let (cfg, _) = ConfigurationLoader::for_tests(
1260 Some(serde_json::json!({
1261 "tags": ["a", "b"],
1262 "matrix": [[1, 2], [3, 4]]
1263 })),
1264 None,
1265 false,
1266 )
1267 .await;
1268 cfg.ready().await;
1269
1270 let pairs = cfg.flattened_keys().unwrap();
1271 let map: std::collections::HashMap<&str, &serde_json::Value> =
1272 pairs.iter().map(|(k, v)| (k.as_str(), v)).collect();
1273
1274 assert_eq!(map.get("tags"), Some(&&serde_json::json!(["a", "b"])));
1275 assert_eq!(map.get("matrix"), Some(&&serde_json::json!([[1, 2], [3, 4]])));
1276 }
1277
1278 #[tokio::test]
1279 async fn test_flattened_keys_null_values_absent() {
1280 let (cfg, _) = ConfigurationLoader::for_tests(
1281 Some(serde_json::json!({
1282 "present": "yes",
1283 "absent": null
1284 })),
1285 None,
1286 false,
1287 )
1288 .await;
1289 cfg.ready().await;
1290
1291 let pairs = cfg.flattened_keys().unwrap();
1292 let map: std::collections::HashMap<&str, &serde_json::Value> =
1293 pairs.iter().map(|(k, v)| (k.as_str(), v)).collect();
1294
1295 assert_eq!(map.get("present"), Some(&&serde_json::json!("yes")));
1296 assert!(!map.contains_key("absent"));
1298 }
1299}