options/prelude/
config.rs1use 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
51pub trait ConfigExt<C>: Sized {
53 fn apply_config<T>(&mut self, configuration: C) -> Builder<'_, T>
59 where
60 T: Value + Default + DeserializeOwned + Send + Sync + 'static;
61
62 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 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 let options = provider.get_required::<TestOptions>();
155
156 assert!(options.enabled);
158 }
159
160 #[test]
161 fn apply_config_should_bind_configuration_section_to_options() {
162 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 let options = provider.get_required::<TestOptions>();
174
175 assert!(options.enabled);
177 }
178
179 #[test]
180 fn apply_config_at_should_bind_configuration_to_options() {
181 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 let options = provider.get_required::<dyn Snapshot<TestOptions>>();
193
194 assert!(options.get_named_unchecked("Test").enabled);
196 }
197
198 #[test]
199 fn options_should_be_updated_after_configuration_change() {
200 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 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 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 let current = monitor.get_unchecked();
268
269 assert_eq!(original.enabled, true);
271 assert_eq!(current.enabled, false);
272 }
273}