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
9pub 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 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
46pub trait OptionsConfigurationServiceExtensions {
48 fn apply_config<T>(&mut self, configuration: Ref<dyn Configuration>) -> OptionsBuilder<T>
54 where
55 T: Value + Default + DeserializeOwned + 'static;
56
57 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 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 let options = provider.get_required::<dyn Options<TestOptions>>();
148
149 assert!(options.value().enabled);
151 }
152
153 #[test]
154 fn apply_config_should_bind_configuration_section_to_options() {
155 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 let options = provider.get_required::<dyn Options<TestOptions>>();
167
168 assert!(options.value().enabled);
170 }
171
172 #[test]
173 fn apply_config_at_should_bind_configuration_to_options() {
174 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 let options = provider.get_required::<dyn OptionsSnapshot<TestOptions>>();
189
190 assert!(options.get(Some("Test")).enabled);
192 }
193
194 #[test]
195 fn options_should_be_updated_after_configuration_change() {
196 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 let current = provider
248 .get_required::<dyn OptionsMonitor<TestOptions>>()
249 .current_value();
250
251 if path.exists() {
253 remove_file(&path).ok();
254 }
255
256 assert_eq!(original.enabled, true);
257 assert_eq!(current.enabled, false);
258 }
259}