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
17unsafe impl<T: Send + Sync> Send for ConfigurationChangeTokenSource<T> {}
18unsafe impl<T: Send + Sync> Sync for ConfigurationChangeTokenSource<T> {}
19
20impl<T: Value> ConfigurationChangeTokenSource<T> {
21    /// Initializes a new configuration change token source.
22    ///
23    /// # Arguments
24    ///
25    /// * `name` - The optional name of the options being watched
26    /// * `configuration` - The source [configuration](config::Configuration)
27    pub fn new(name: Option<&str>, configuration: Ref<dyn Configuration>) -> Self {
28        Self {
29            name: name.map(|s| s.to_owned()),
30            configuration,
31            _data: PhantomData,
32        }
33    }
34}
35
36impl<T: Value> OptionsChangeTokenSource<T> for ConfigurationChangeTokenSource<T> {
37    fn token(&self) -> Box<dyn ChangeToken> {
38        self.configuration.reload_token()
39    }
40
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(
78            None,
79            configuration.clone(),
80        ));
81        let descriptor =
82            existing::<dyn OptionsChangeTokenSource<T>, ConfigurationChangeTokenSource<T>>(source);
83
84        self.add(descriptor)
85            .add_options()
86            .configure(move |options: &mut T| configuration.bind(options))
87    }
88
89    fn apply_config_at<T>(
90        &mut self,
91        configuration: Ref<dyn Configuration>,
92        key: impl AsRef<str>,
93    ) -> OptionsBuilder<T>
94    where
95        T: Value + Default + DeserializeOwned + 'static,
96    {
97        let source = Box::new(ConfigurationChangeTokenSource::<T>::new(
98            Some(key.as_ref()),
99            configuration.clone(),
100        ));
101        let descriptor =
102            existing::<dyn OptionsChangeTokenSource<T>, ConfigurationChangeTokenSource<T>>(source);
103        let key = key.as_ref().to_owned();
104
105        self.add(descriptor)
106            .add_named_options(&key)
107            .configure(move |options: &mut T| configuration.bind_at(&key, options))
108    }
109}
110
111#[cfg(test)]
112mod tests {
113
114    use super::*;
115    use config::{ConfigurationBuilder, DefaultConfigurationBuilder};
116    use di::ServiceCollection;
117    use serde::Deserialize;
118    use serde_json::json;
119    use std::env::temp_dir;
120    use std::fs::{remove_file, File};
121    use std::io::Write;
122    use std::sync::{Arc, Condvar, Mutex};
123    use std::time::Duration;
124
125    #[derive(Default, Deserialize)]
126    #[serde(rename_all(deserialize = "PascalCase"))]
127    struct TestOptions {
128        enabled: bool,
129    }
130
131    #[test]
132    fn apply_config_should_bind_configuration_to_options() {
133        // arrange
134        let config = Ref::from(
135            DefaultConfigurationBuilder::new()
136                .add_in_memory(&[("Enabled", "true")])
137                .build()
138                .unwrap()
139                .as_config(),
140        );
141        let provider = ServiceCollection::new()
142            .apply_config::<TestOptions>(config)
143            .build_provider()
144            .unwrap();
145
146        // act
147        let options = provider.get_required::<dyn Options<TestOptions>>();
148
149        // assert
150        assert!(options.value().enabled);
151    }
152
153    #[test]
154    fn apply_config_should_bind_configuration_section_to_options() {
155        // arrange
156        let config = DefaultConfigurationBuilder::new()
157            .add_in_memory(&[("Test:Enabled", "true")])
158            .build()
159            .unwrap();
160        let provider = ServiceCollection::new()
161            .apply_config::<TestOptions>(config.section("Test").as_config().into())
162            .build_provider()
163            .unwrap();
164
165        // act
166        let options = provider.get_required::<dyn Options<TestOptions>>();
167
168        // assert
169        assert!(options.value().enabled);
170    }
171
172    #[test]
173    fn apply_config_at_should_bind_configuration_to_options() {
174        // arrange
175        let config = Ref::from(
176            DefaultConfigurationBuilder::new()
177                .add_in_memory(&[("Test:Enabled", "true")])
178                .build()
179                .unwrap()
180                .as_config(),
181        );
182        let provider = ServiceCollection::new()
183            .apply_config_at::<TestOptions>(config, "Test")
184            .build_provider()
185            .unwrap();
186
187        // act
188        let options = provider.get_required::<dyn OptionsSnapshot<TestOptions>>();
189
190        // assert
191        assert!(options.get(Some("Test")).enabled);
192    }
193
194    #[test]
195    fn options_should_be_updated_after_configuration_change() {
196        // arrange
197        let path = temp_dir().join("options_from_json_1.json");
198        let mut json = json!({"enabled": true});
199
200        let mut file = File::create(&path).unwrap();
201        file.write_all(json.to_string().as_bytes()).unwrap();
202        drop(file);
203
204        let config: Ref<dyn Configuration> = Ref::from(
205            DefaultConfigurationBuilder::new()
206                .add_json_file(&path.is().reloadable())
207                .build()
208                .unwrap()
209                .as_config(),
210        );
211        let provider = ServiceCollection::new()
212            .apply_config::<TestOptions>(config.clone())
213            .build_provider()
214            .unwrap();
215
216        let token = config.reload_token();
217        let original = provider
218            .get_required::<dyn OptionsMonitor<TestOptions>>()
219            .current_value();
220        let state = Arc::new((Mutex::new(false), Condvar::new()));
221        let _unused = token.register(
222            Box::new(|s| {
223                let data = s.unwrap();
224                let (reloaded, event) = &*(data.downcast_ref::<(Mutex<bool>, Condvar)>().unwrap());
225                *reloaded.lock().unwrap() = true;
226                event.notify_one();
227            }),
228            Some(state.clone()),
229        );
230
231        json = json!({"enabled": false});
232        file = File::create(&path).unwrap();
233        file.write_all(json.to_string().as_bytes()).unwrap();
234        drop(file);
235
236        let (mutex, event) = &*state;
237        let mut reloaded = mutex.lock().unwrap();
238
239        while !*reloaded {
240            reloaded = event
241                .wait_timeout(reloaded, Duration::from_secs(1))
242                .unwrap()
243                .0;
244        }
245
246        // act
247        let current = provider
248            .get_required::<dyn OptionsMonitor<TestOptions>>()
249            .current_value();
250
251        // assert
252        if path.exists() {
253            remove_file(&path).ok();
254        }
255
256        assert_eq!(original.enabled, true);
257        assert_eq!(current.enabled, false);
258    }
259}