apollo_rust_client/
lib.rs

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