Skip to main content

options/
cfg_ext.rs

1use crate::{ext::*, *};
2use config::ext::*;
3use config::Configuration;
4use di::{existing, Ref, ServiceCollection};
5use serde::de::DeserializeOwned;
6use std::marker::PhantomData;
7use tokens::ChangeToken;
8
9/// Represents a change token for monitored [`Options`](crate::Options) that are
10/// notified when configuration changes.
11pub struct ConfigurationChangeTokenSource<T: Value> {
12    name: Option<String>,
13    configuration: Ref<dyn Configuration>,
14    _data: PhantomData<T>,
15}
16
17impl<T: Value> ConfigurationChangeTokenSource<T> {
18    /// Initializes a new configuration change token source.
19    ///
20    /// # Arguments
21    ///
22    /// * `name` - The optional name of the options being watched
23    /// * `configuration` - The source [configuration](config::Configuration)
24    #[inline]
25    pub fn new(name: Option<&str>, configuration: Ref<dyn Configuration>) -> Self {
26        Self {
27            name: name.map(|s| s.to_owned()),
28            configuration,
29            _data: PhantomData,
30        }
31    }
32}
33
34impl<T: Value> OptionsChangeTokenSource<T> for ConfigurationChangeTokenSource<T> {
35    #[inline]
36    fn token(&self) -> Box<dyn ChangeToken> {
37        self.configuration.reload_token()
38    }
39
40    #[inline]
41    fn name(&self) -> Option<&str> {
42        self.name.as_deref()
43    }
44}
45
46/// Defines extension methods for the [`ServiceCollection`](di::ServiceCollection) struct.
47pub trait OptionsConfigurationServiceExtensions {
48    /// Registers an options type that will have all of its associated services registered.
49    ///
50    /// # Arguments
51    ///
52    /// * `configuration` - The [configuration](config::Configuration) applied to the options
53    fn apply_config<T>(&mut self, configuration: Ref<dyn Configuration>) -> OptionsBuilder<'_, T>
54    where
55        T: Value + Default + DeserializeOwned + 'static;
56
57    /// Registers an options type that will have all of its associated services registered.
58    ///
59    /// # Arguments
60    ///
61    /// * `configuration` - The [configuration](config::Configuration) applied to the options
62    /// * `key` - The key to the part of the [configuration](config::Configuration) applied to the options
63    fn apply_config_at<T>(
64        &mut self,
65        configuration: Ref<dyn Configuration>,
66        key: impl AsRef<str>,
67    ) -> OptionsBuilder<'_, T>
68    where
69        T: Value + Default + DeserializeOwned + 'static;
70}
71
72impl OptionsConfigurationServiceExtensions for ServiceCollection {
73    fn apply_config<T>(&mut self, configuration: Ref<dyn Configuration>) -> OptionsBuilder<'_, T>
74    where
75        T: Value + Default + DeserializeOwned + 'static,
76    {
77        let source = Box::new(ConfigurationChangeTokenSource::<T>::new(None, configuration.clone()));
78        let descriptor = existing::<dyn OptionsChangeTokenSource<T>, ConfigurationChangeTokenSource<T>>(source);
79
80        self.add(descriptor)
81            .add_options()
82            .configure(move |options: &mut T| configuration.bind(options))
83    }
84
85    fn apply_config_at<T>(
86        &mut self,
87        configuration: Ref<dyn Configuration>,
88        key: impl AsRef<str>,
89    ) -> OptionsBuilder<'_, T>
90    where
91        T: Value + Default + DeserializeOwned + 'static,
92    {
93        let source = Box::new(ConfigurationChangeTokenSource::<T>::new(
94            Some(key.as_ref()),
95            configuration.clone(),
96        ));
97        let descriptor = existing::<dyn OptionsChangeTokenSource<T>, ConfigurationChangeTokenSource<T>>(source);
98        let key = key.as_ref().to_owned();
99
100        self.add(descriptor)
101            .add_named_options(&key)
102            .configure(move |options: &mut T| configuration.bind_at(&key, options))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108
109    use super::*;
110    use config::{ConfigurationBuilder, DefaultConfigurationBuilder};
111    use di::ServiceCollection;
112    use serde::Deserialize;
113    use serde_json::json;
114    use std::env::temp_dir;
115    use std::fs::{remove_file, File};
116    use std::io::Write;
117    use std::sync::{Arc, Condvar, Mutex};
118    use std::time::Duration;
119
120    #[derive(Default, Deserialize)]
121    #[serde(rename_all(deserialize = "PascalCase"))]
122    struct TestOptions {
123        enabled: bool,
124    }
125
126    #[test]
127    fn apply_config_should_bind_configuration_to_options() {
128        // arrange
129        let config = Ref::from(
130            DefaultConfigurationBuilder::new()
131                .add_in_memory(&[("Enabled", "true")])
132                .build()
133                .unwrap()
134                .as_config(),
135        );
136        let provider = ServiceCollection::new()
137            .apply_config::<TestOptions>(config)
138            .build_provider()
139            .unwrap();
140
141        // act
142        let options = provider.get_required::<dyn Options<TestOptions>>();
143
144        // assert
145        assert!(options.value().enabled);
146    }
147
148    #[test]
149    fn apply_config_should_bind_configuration_section_to_options() {
150        // arrange
151        let config = DefaultConfigurationBuilder::new()
152            .add_in_memory(&[("Test:Enabled", "true")])
153            .build()
154            .unwrap();
155        let provider = ServiceCollection::new()
156            .apply_config::<TestOptions>(config.section("Test").as_config().into())
157            .build_provider()
158            .unwrap();
159
160        // act
161        let options = provider.get_required::<dyn Options<TestOptions>>();
162
163        // assert
164        assert!(options.value().enabled);
165    }
166
167    #[test]
168    fn apply_config_at_should_bind_configuration_to_options() {
169        // arrange
170        let config = Ref::from(
171            DefaultConfigurationBuilder::new()
172                .add_in_memory(&[("Test:Enabled", "true")])
173                .build()
174                .unwrap()
175                .as_config(),
176        );
177        let provider = ServiceCollection::new()
178            .apply_config_at::<TestOptions>(config, "Test")
179            .build_provider()
180            .unwrap();
181
182        // act
183        let options = provider.get_required::<dyn OptionsSnapshot<TestOptions>>();
184
185        // assert
186        assert!(options.get(Some("Test")).enabled);
187    }
188
189    #[test]
190    fn options_should_be_updated_after_configuration_change() {
191        // arrange
192        let path = temp_dir().join("options_from_json_1.json");
193        let mut json = json!({"enabled": true});
194
195        let mut file = File::create(&path).unwrap();
196        file.write_all(json.to_string().as_bytes()).unwrap();
197        drop(file);
198
199        let config: Ref<dyn Configuration> = Ref::from(
200            DefaultConfigurationBuilder::new()
201                .add_json_file(&path.is().reloadable())
202                .build()
203                .unwrap()
204                .as_config(),
205        );
206        let provider = ServiceCollection::new()
207            .apply_config::<TestOptions>(config.clone())
208            .build_provider()
209            .unwrap();
210
211        let token = config.reload_token();
212        let original = provider
213            .get_required::<dyn OptionsMonitor<TestOptions>>()
214            .current_value();
215        let state = Arc::new((Mutex::new(false), Condvar::new()));
216        let _unused = token.register(
217            Box::new(|s| {
218                let data = s.unwrap();
219                let (reloaded, event) = &*(data.downcast_ref::<(Mutex<bool>, Condvar)>().unwrap());
220                *reloaded.lock().unwrap() = true;
221                event.notify_one();
222            }),
223            Some(state.clone()),
224        );
225
226        json = json!({"enabled": false});
227        file = File::create(&path).unwrap();
228        file.write_all(json.to_string().as_bytes()).unwrap();
229        drop(file);
230
231        let (mutex, event) = &*state;
232        let mut reloaded = mutex.lock().unwrap();
233
234        while !*reloaded {
235            reloaded = event.wait_timeout(reloaded, Duration::from_secs(1)).unwrap().0;
236        }
237
238        // act
239        let current = provider
240            .get_required::<dyn OptionsMonitor<TestOptions>>()
241            .current_value();
242
243        // assert
244        if path.exists() {
245            remove_file(&path).ok();
246        }
247
248        assert_eq!(original.enabled, true);
249        assert_eq!(current.enabled, false);
250    }
251}