Skip to main content

options/prelude/
config.rs

1use crate::{prelude::*, ChangeTokenSource, Value};
2use ::config::{prelude::*, Reloadable};
3use ::di::{transient_factory, ServiceCollection};
4use serde::de::DeserializeOwned;
5use std::marker::PhantomData;
6use tokens::{ChangeToken, NeverChangeToken};
7
8struct NeverReloads;
9
10impl Reloadable for NeverReloads {
11    #[inline]
12    fn can_reload(&self) -> bool {
13        false
14    }
15
16    #[inline]
17    fn reload_token(&self) -> impl ChangeToken + 'static {
18        NeverChangeToken
19    }
20}
21
22struct ConfigChangeTokenSource<R, V> {
23    name: String,
24    reloadable: R,
25    _data: PhantomData<V>,
26}
27
28impl<R, T> ConfigChangeTokenSource<R, T> {
29    #[inline]
30    pub fn new(name: String, reloadable: R) -> Self {
31        Self {
32            name,
33            reloadable,
34            _data: PhantomData,
35        }
36    }
37}
38
39impl<R: Value + Reloadable, T: Value> ChangeTokenSource<T> for ConfigChangeTokenSource<R, T> {
40    #[inline]
41    fn token(&self) -> Box<dyn ChangeToken> {
42        Box::new(self.reloadable.reload_token())
43    }
44
45    #[inline]
46    fn name(&self) -> &str {
47        &self.name
48    }
49}
50
51/// Defines configuration extension methods for a [ServiceCollection](::di::ServiceCollection).
52pub trait ConfigExt<C>: Sized {
53    /// Registers an options type that will have all of its associated services registered.
54    ///
55    /// # Arguments
56    ///
57    /// * `configuration` - The [configuration](::config::Configuration) applied to the options
58    fn apply_config<T>(&mut self, configuration: C) -> Builder<'_, T>
59    where
60        T: Value + Default + DeserializeOwned + Send + Sync + 'static;
61
62    /// Registers an options type that will have all of its associated services registered.
63    ///
64    /// # Arguments
65    ///
66    /// * `configuration` - The [configuration](::config::Configuration) applied to the options
67    /// * `key` - The key to the part of the [configuration](::config::Configuration) applied to the options
68    fn apply_config_at<T>(&mut self, configuration: C, key: impl AsRef<str>) -> Builder<'_, T>
69    where
70        T: Value + Default + DeserializeOwned + Send + Sync + 'static;
71}
72
73fn add_change_token_source<B, T>(services: &mut ServiceCollection, name: String, configuration: &B)
74where
75    B: Value + Reloadable + Clone + 'static,
76    T: Value + 'static,
77{
78    if configuration.can_reload() {
79        let reloadable = configuration.clone();
80
81        services.add(transient_factory(move |_| {
82            ::di::Ref::new(ConfigChangeTokenSource::<B, T>::new(name.clone(), reloadable.clone()))
83                as ::di::Ref<dyn ChangeTokenSource<T>>
84        }));
85    } else {
86        services.add(transient_factory(move |_| {
87            ::di::Ref::new(ConfigChangeTokenSource::<_, T>::new(name.clone(), NeverReloads))
88                as ::di::Ref<dyn ChangeTokenSource<T>>
89        }));
90    }
91}
92
93impl<C> ConfigExt<C> for ServiceCollection
94where
95    C: Value + Binder + Reloadable + Clone + 'static,
96{
97    fn apply_config<T>(&mut self, configuration: C) -> Builder<'_, T>
98    where
99        T: Value + Default + DeserializeOwned + 'static,
100    {
101        add_change_token_source::<_, T>(self, String::new(), &configuration);
102        self.add_options()
103            .configure(move |options: &mut T| configuration.bind_unchecked(options))
104    }
105
106    fn apply_config_at<T>(&mut self, configuration: C, key: impl AsRef<str>) -> Builder<'_, T>
107    where
108        T: Value + Default + DeserializeOwned + 'static,
109    {
110        let name = key;
111        let key = name.as_ref().to_owned();
112
113        add_change_token_source::<_, T>(self, key.clone(), &configuration);
114
115        self.add_named_options(name)
116            .configure(move |options: &mut T| configuration.bind_at_unchecked(&key, options))
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::{Monitor, Snapshot};
124    use ::config::ReloadableConfiguration;
125    use ::di::ServiceCollection;
126    use cfg_if::cfg_if;
127    use serde::Deserialize;
128    use serde_json::json;
129    use std::convert::TryInto;
130    use std::fs::File;
131    use std::io::Write;
132    use std::sync::{Arc, Condvar, Mutex};
133    use std::time::{Duration, Instant};
134    use tempfile::tempdir;
135
136    #[derive(Default, Deserialize)]
137    struct TestOptions {
138        enabled: bool,
139    }
140
141    #[test]
142    fn apply_config_should_bind_configuration_to_options() {
143        // arrange
144        let config = ::config::builder()
145            .add_in_memory(&[("Enabled", "true")])
146            .build()
147            .unwrap();
148        let provider = ServiceCollection::new()
149            .apply_config::<TestOptions>(config)
150            .build_provider()
151            .unwrap();
152
153        // act
154        let options = provider.get_required::<TestOptions>();
155
156        // assert
157        assert!(options.enabled);
158    }
159
160    #[test]
161    fn apply_config_should_bind_configuration_section_to_options() {
162        // arrange
163        let config = ::config::builder()
164            .add_in_memory(&[("Test:Enabled", "true")])
165            .build()
166            .unwrap();
167        let provider = ServiceCollection::new()
168            .apply_config::<TestOptions>(config.section("Test").to_owned())
169            .build_provider()
170            .unwrap();
171
172        // act
173        let options = provider.get_required::<TestOptions>();
174
175        // assert
176        assert!(options.enabled);
177    }
178
179    #[test]
180    fn apply_config_at_should_bind_configuration_to_options() {
181        // arrange
182        let config = ::config::builder()
183            .add_in_memory(&[("Test:Enabled", "true")])
184            .build()
185            .unwrap();
186        let provider = ServiceCollection::new()
187            .apply_config_at::<TestOptions>(config, "Test")
188            .build_provider()
189            .unwrap();
190
191        // act
192        let options = provider.get_required::<dyn Snapshot<TestOptions>>();
193
194        // assert
195        assert!(options.get_named_unchecked("Test").enabled);
196    }
197
198    #[test]
199    fn options_should_be_updated_after_configuration_change() {
200        // arrange
201        let dir = tempdir().unwrap();
202        let path = dir.path().join("options.json");
203        let mut json = json!({"enabled": true});
204        let mut file = File::create(&path).unwrap();
205
206        file.write_all(json.to_string().as_bytes()).unwrap();
207        drop(file);
208
209        let config: ReloadableConfiguration = ::config::builder()
210            .add_json_file(&path.is().reloadable())
211            .try_into()
212            .unwrap();
213        let provider = ServiceCollection::new()
214            .apply_config::<TestOptions>(config)
215            .build_provider()
216            .unwrap();
217        let monitor = provider.get_required::<dyn Monitor<TestOptions>>();
218        let original = monitor.get_unchecked();
219        let state = Arc::new((Mutex::new(false), Condvar::new()));
220        let state_clone = state.clone();
221        let _sub = monitor.on_change(Box::new(move |_, _| {
222            let (reloaded, event) = &*state_clone;
223            *reloaded.lock().unwrap() = true;
224            event.notify_one();
225        }));
226
227        json = json!({"enabled": false});
228        file = File::create(&path).unwrap();
229        file.write_all(json.to_string().as_bytes()).unwrap();
230        drop(file);
231
232        // give the file watcher time to detect the change;  on Linux CI (inotify), async notification delivery can be
233        // slower than on Windows (ReadDirectoryChangesW)
234        std::thread::sleep(Duration::from_millis(100));
235
236        let (mutex, event) = &*state;
237        let deadline = Instant::now();
238
239        loop {
240            cfg_if! {
241                if #[cfg(not(feature = "async"))] {
242                    // the sync path is not guaranteed to be Send or Sync so it cannot automatically updated in the
243                    // background. the change happens in the background, but it is not realized until the next time
244                    // the value is polled. poll the monitor to force realization of the change
245                    let _ = monitor.get_unchecked();
246                }
247            }
248
249            let mut reloaded = mutex.lock().unwrap();
250
251            if *reloaded {
252                break;
253            }
254
255            if deadline.elapsed() > Duration::from_secs(3) {
256                panic!("timed out waiting for configuration reload");
257            }
258
259            reloaded = event.wait_timeout(reloaded, Duration::from_millis(250)).unwrap().0;
260
261            if *reloaded {
262                break;
263            }
264        }
265
266        // act
267        let current = monitor.get_unchecked();
268
269        // assert
270        assert_eq!(original.enabled, true);
271        assert_eq!(current.enabled, false);
272    }
273}