apollo_rust_client/
lib.rs

1use cache::Cache;
2use client_config::ClientConfig;
3use futures::executor::block_on;
4use log::trace;
5use std::{
6    collections::HashMap,
7    sync::{Arc, RwLock},
8    thread::{self, JoinHandle},
9    time::Duration,
10};
11use wasm_bindgen::prelude::wasm_bindgen;
12
13pub mod cache;
14pub mod client_config;
15
16/// Different types of errors that can occur when using the client.
17#[derive(Debug, thiserror::Error)]
18pub enum Error {
19    #[error("Client is already running")]
20    AlreadyRunning,
21}
22
23/// Apollo client.
24#[wasm_bindgen]
25pub struct Client {
26    client_config: ClientConfig,
27    namespaces: Arc<RwLock<HashMap<String, Arc<Cache>>>>,
28    handle: Option<JoinHandle<()>>,
29    running: Arc<RwLock<bool>>,
30}
31
32impl Client {
33    pub fn start(&mut self) -> Result<(), Error> {
34        let mut running = self.running.write().unwrap();
35        if *running {
36            return Err(Error::AlreadyRunning);
37        }
38
39        *running = true;
40
41        let running = self.running.clone();
42        let namespaces = self.namespaces.clone();
43        // Spawn a background thread to refresh caches
44        let handle = thread::spawn(move || {
45            loop {
46                let running = running.read().unwrap();
47                if !*running {
48                    break;
49                }
50
51                let namespaces = namespaces.read().unwrap();
52                // Refresh each namespace's cache
53                for (namespace, cache) in namespaces.iter() {
54                    if let Err(err) = block_on(cache.refresh()) {
55                        log::error!(
56                            "Failed to refresh cache for namespace {}: {:?}",
57                            namespace,
58                            err
59                        );
60                    } else {
61                        log::debug!("Successfully refreshed cache for namespace {}", namespace);
62                    }
63                }
64
65                // Sleep for 30 seconds before the next refresh
66                thread::sleep(Duration::from_secs(30));
67            }
68        });
69
70        self.handle = Some(handle);
71
72        Ok(())
73    }
74
75    pub fn stop(&mut self) {
76        let mut running = self.running.write().unwrap();
77        *running = false;
78        if let Some(handle) = self.handle.take() {
79            handle.join().unwrap();
80        }
81    }
82}
83
84cfg_if::cfg_if! {
85    if #[cfg(target_arch = "wasm32")] {
86        #[wasm_bindgen]
87        impl Client {
88            /// Get a cache for a given namespace.
89            ///
90            /// # Arguments
91            ///
92            /// * `namespace` - The namespace to get the cache for.
93            ///
94            /// # Returns
95            ///
96            /// A cache for the given namespace.
97            pub fn namespace(&self, namespace: &str) -> Cache {
98                let mut namespaces = self.namespaces.write().unwrap();
99                let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
100                    trace!("Cache miss, creating cache for namespace {}", namespace);
101                    Arc::new(Cache::new(self.client_config.clone(), namespace))
102                });
103                let cache = (*cache).clone();
104                (*cache).to_owned()
105            }
106        }
107    } else {
108        impl Client {
109            /// Get a cache for a given namespace.
110            ///
111            /// # Arguments
112            ///
113            /// * `namespace` - The namespace to get the cache for.
114            ///
115            /// # Returns
116            ///
117            /// A cache for the given namespace.
118            pub fn namespace(&self, namespace: &str) -> Arc<Cache> {
119                let mut namespaces = self.namespaces.write().unwrap();
120                let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
121                    trace!("Cache miss, creating cache for namespace {}", namespace);
122                    Arc::new(Cache::new(self.client_config.clone(), namespace))
123                });
124                cache.clone()
125            }
126        }
127    }
128}
129
130#[wasm_bindgen]
131impl Client {
132    /// Create a new Apollo client.
133    ///
134    /// # Arguments
135    ///
136    /// * `client_config` - The configuration for the Apollo client.
137    ///
138    /// # Returns
139    ///
140    /// A new Apollo client.
141    #[wasm_bindgen(constructor)]
142    pub fn new(client_config: ClientConfig) -> Self {
143        Self {
144            client_config,
145            namespaces: Arc::new(RwLock::new(HashMap::new())),
146            handle: None,
147            running: Arc::new(RwLock::new(false)),
148        }
149    }
150}
151
152impl Drop for Client {
153    fn drop(&mut self) {
154        self.stop();
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use lazy_static::lazy_static;
162    use wasm_bindgen_test::wasm_bindgen_test;
163
164    lazy_static! {
165        static ref CLIENT_NO_SECRET: Client = {
166            let config = ClientConfig {
167                app_id: String::from("101010101"),
168                cluster: String::from("default"),
169                config_server: String::from("http://81.68.181.139:8080"),
170                label: None,
171                secret: None,
172                cache_dir: Some(String::from("/tmp/apollo")),
173                ip: None,
174            };
175            Client::new(config)
176        };
177        static ref CLIENT_WITH_SECRET: Client = {
178            let config = ClientConfig {
179                app_id: String::from("101010102"),
180                cluster: String::from("default"),
181                config_server: String::from("http://81.68.181.139:8080"),
182                label: None,
183                secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
184                cache_dir: Some(String::from("/tmp/apollo")),
185                ip: None,
186            };
187            Client::new(config)
188        };
189        static ref CLIENT_WITH_GRAYSCALE_IP: Client = {
190            let config = ClientConfig {
191                app_id: String::from("101010101"),
192                cluster: String::from("default"),
193                config_server: String::from("http://81.68.181.139:8080"),
194                label: None,
195                secret: None,
196                cache_dir: Some(String::from("/tmp/apollo")),
197                ip: Some(String::from("1.2.3.4")),
198            };
199            Client::new(config)
200        };
201        static ref CLIENT_WITH_GRAYSCALE_LABEL: Client = {
202            let config = ClientConfig {
203                app_id: String::from("101010101"),
204                cluster: String::from("default"),
205                config_server: String::from("http://81.68.181.139:8080"),
206                label: Some(String::from("GrayScale")),
207                secret: None,
208                cache_dir: Some(String::from("/tmp/apollo")),
209                ip: None,
210            };
211            Client::new(config)
212        };
213    }
214
215    pub(crate) fn setup() {
216        cfg_if::cfg_if! {
217            if #[cfg(target_arch = "wasm32")] {
218                let _ = wasm_logger::init(wasm_logger::Config::default());
219                console_error_panic_hook::set_once();
220            } else {
221                let _ = env_logger::builder().is_test(true).try_init();
222            }
223        }
224    }
225
226    #[tokio::test]
227    async fn test_missing_value() {
228        setup();
229        let cache = CLIENT_NO_SECRET.namespace("application");
230        assert_eq!(cache.get_property::<String>("missingValue").await, None);
231    }
232
233    #[wasm_bindgen_test]
234    #[allow(dead_code)]
235    async fn test_missing_value_wasm() {
236        setup();
237        let cache = CLIENT_NO_SECRET.namespace("application");
238        assert_eq!(cache.get_property::<String>("missingValue").await, None);
239    }
240
241    #[tokio::test]
242    async fn test_string_value() {
243        setup();
244        let cache = CLIENT_NO_SECRET.namespace("application");
245        assert_eq!(
246            cache.get_string("stringValue").await,
247            Some("string value".to_string())
248        );
249    }
250
251    #[wasm_bindgen_test]
252    #[allow(dead_code)]
253    async fn test_string_value_wasm() {
254        setup();
255        let cache = CLIENT_NO_SECRET.namespace("application");
256        assert_eq!(
257            cache.get_string("stringValue").await,
258            Some("string value".to_string())
259        );
260    }
261
262    #[tokio::test]
263    async fn test_string_value_with_secret() {
264        setup();
265        console_error_panic_hook::set_once();
266        let cache = CLIENT_WITH_SECRET.namespace("application");
267        assert_eq!(
268            cache.get_property::<String>("stringValue").await,
269            Some("string value".to_string())
270        );
271    }
272
273    #[wasm_bindgen_test]
274    #[allow(dead_code)]
275    async fn test_string_value_with_secret_wasm() {
276        setup();
277        let cache = CLIENT_WITH_SECRET.namespace("application");
278        assert_eq!(
279            cache.get_string("stringValue").await,
280            Some("string value".to_string())
281        );
282    }
283
284    #[tokio::test]
285    async fn test_int_value() {
286        setup();
287        let cache = CLIENT_NO_SECRET.namespace("application");
288        assert_eq!(cache.get_property::<i32>("intValue").await, Some(42));
289    }
290
291    #[wasm_bindgen_test]
292    #[allow(dead_code)]
293    async fn test_int_value_wasm() {
294        setup();
295        let cache = CLIENT_NO_SECRET.namespace("application");
296        assert_eq!(cache.get_int("intValue").await, Some(42));
297    }
298
299    #[tokio::test]
300    async fn test_int_value_with_secret() {
301        setup();
302        let cache = CLIENT_WITH_SECRET.namespace("application");
303        assert_eq!(cache.get_property::<i32>("intValue").await, Some(42));
304    }
305
306    #[wasm_bindgen_test]
307    #[allow(dead_code)]
308    async fn test_int_value_with_secret_wasm() {
309        setup();
310        let cache = CLIENT_WITH_SECRET.namespace("application");
311        assert_eq!(cache.get_int("intValue").await, Some(42));
312    }
313
314    #[tokio::test]
315    async fn test_float_value() {
316        setup();
317        let cache = CLIENT_NO_SECRET.namespace("application");
318        assert_eq!(cache.get_property::<f64>("floatValue").await, Some(4.20));
319    }
320
321    #[wasm_bindgen_test]
322    #[allow(dead_code)]
323    async fn test_float_value_wasm() {
324        setup();
325        let cache = CLIENT_NO_SECRET.namespace("application");
326        assert_eq!(cache.get_float("floatValue").await, Some(4.20));
327    }
328
329    #[tokio::test]
330    async fn test_float_value_with_secret() {
331        setup();
332        let cache = CLIENT_WITH_SECRET.namespace("application");
333        assert_eq!(cache.get_property::<f64>("floatValue").await, Some(4.20));
334    }
335
336    #[wasm_bindgen_test]
337    #[allow(dead_code)]
338    async fn test_float_value_with_secret_wasm() {
339        setup();
340        let cache = CLIENT_WITH_SECRET.namespace("application");
341        assert_eq!(cache.get_float("floatValue").await, Some(4.20));
342    }
343
344    #[tokio::test]
345    async fn test_bool_value() {
346        setup();
347        let cache = CLIENT_NO_SECRET.namespace("application");
348        assert_eq!(cache.get_property::<bool>("boolValue").await, Some(false));
349    }
350
351    #[wasm_bindgen_test]
352    #[allow(dead_code)]
353    async fn test_bool_value_wasm() {
354        setup();
355        let cache = CLIENT_NO_SECRET.namespace("application");
356        assert_eq!(cache.get_bool("boolValue").await, Some(false));
357    }
358
359    #[tokio::test]
360    async fn test_bool_value_with_secret() {
361        setup();
362        let cache = CLIENT_WITH_SECRET.namespace("application");
363        assert_eq!(cache.get_property::<bool>("boolValue").await, Some(false));
364    }
365
366    #[wasm_bindgen_test]
367    #[allow(dead_code)]
368    async fn test_bool_value_with_secret_wasm() {
369        setup();
370        let cache = CLIENT_WITH_SECRET.namespace("application");
371        assert_eq!(cache.get_bool("boolValue").await, Some(false));
372    }
373
374    #[tokio::test]
375    async fn test_bool_value_with_grayscale_ip() {
376        setup();
377        let cache = CLIENT_WITH_GRAYSCALE_IP.namespace("application");
378        assert_eq!(
379            cache.get_property::<bool>("grayScaleValue").await,
380            Some(true)
381        );
382        let cache = CLIENT_NO_SECRET.namespace("application");
383        assert_eq!(
384            cache.get_property::<bool>("grayScaleValue").await,
385            Some(false)
386        );
387    }
388
389    #[wasm_bindgen_test]
390    #[allow(dead_code)]
391    async fn test_bool_value_with_grayscale_ip_wasm() {
392        setup();
393        let cache = CLIENT_WITH_GRAYSCALE_IP.namespace("application");
394        assert_eq!(cache.get_bool("grayScaleValue").await, Some(true));
395
396        let cache = CLIENT_NO_SECRET.namespace("application");
397        assert_eq!(cache.get_bool("grayScaleValue").await, Some(false));
398    }
399
400    #[tokio::test]
401    async fn test_bool_value_with_grayscale_label() {
402        setup();
403        let cache = CLIENT_WITH_GRAYSCALE_LABEL.namespace("application");
404        assert_eq!(
405            cache.get_property::<bool>("grayScaleValue").await,
406            Some(true)
407        );
408        let cache = CLIENT_NO_SECRET.namespace("application");
409        assert_eq!(
410            cache.get_property::<bool>("grayScaleValue").await,
411            Some(false)
412        );
413    }
414
415    #[wasm_bindgen_test]
416    #[allow(dead_code)]
417    async fn test_bool_value_with_grayscale_label_wasm() {
418        setup();
419        let cache = CLIENT_WITH_GRAYSCALE_LABEL.namespace("application");
420        assert_eq!(cache.get_bool("grayScaleValue").await, Some(true));
421
422        let cache = CLIENT_NO_SECRET.namespace("application");
423        assert_eq!(cache.get_bool("grayScaleValue").await, Some(false));
424    }
425}