apollo_rust_client/
lib.rs

1//! # Apollo Rust Client
2//!
3//! A robust Rust client for the Apollo Configuration Centre, with support for WebAssembly
4//! for browser and Node.js environments.
5//!
6//! This crate provides a comprehensive client for interacting with Apollo configuration services.
7//! The client manages configurations for different namespaces, supports multiple configuration
8//! formats (Properties, JSON, Text), provides caching mechanisms, and offers real-time updates
9//! through background polling and event listeners.
10//!
11//! ## Key Features
12//!
13//! - **Multiple Configuration Formats**: Support for Properties, JSON, Text formats with automatic detection
14//! - **Cross-Platform**: Native Rust and WebAssembly targets with platform-specific optimizations
15//! - **Real-Time Updates**: Background polling with configurable intervals and event listeners
16//! - **Comprehensive Caching**: Multi-level caching with file persistence (native) and memory-only (WASM)
17//! - **Type-Safe API**: Compile-time guarantees and runtime type conversion
18//! - **Error Handling**: Detailed error diagnostics with comprehensive error types
19//! - **Grayscale Release Support**: IP and label-based configuration targeting
20//!
21//! ## Quick Start
22//!
23//! ```rust,no_run
24//! use apollo_rust_client::{Client, client_config::ClientConfig};
25//!
26//! # #[tokio::main]
27//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
28//! let config = ClientConfig {
29//!     app_id: "my-app".to_string(),
30//!     config_server: "http://apollo-server:8080".to_string(),
31//!     cluster: "default".to_string(),
32//!     secret: None,
33//!     cache_dir: None,
34//!     label: None,
35//!     ip: None,
36//!     cache_ttl: None,
37//! };
38//!
39//! let mut client = Client::new(config);
40//! client.start().await?;
41//!
42//! let namespace = client.namespace("application").await?;
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ## Platform Support
48//!
49//! The library supports different behavior for wasm32 and non-wasm32 targets:
50//!
51//! - **Native Rust**: Full feature set with file caching, background tasks, and threading
52//! - **WebAssembly**: Memory-only caching, single-threaded execution, JavaScript interop
53
54use crate::namespace::Namespace;
55use async_std::sync::RwLock;
56use cache::Cache;
57use client_config::ClientConfig;
58use log::{debug, error, trace};
59use std::{collections::HashMap, sync::Arc, time::Duration};
60use wasm_bindgen::prelude::wasm_bindgen;
61
62cfg_if::cfg_if! {
63    if #[cfg(target_arch = "wasm32")] {
64        use wasm_bindgen_futures::spawn_local as spawn;
65    } else {
66        use async_std::task::spawn as spawn;
67    }
68}
69
70mod cache;
71
72pub mod client_config;
73pub mod namespace;
74
75/// Comprehensive error types that can occur when using the Apollo client.
76///
77/// This enum covers all possible error conditions that may arise during client operations,
78/// from initialization and configuration to runtime cache operations and namespace handling.
79///
80/// # Error Categories
81///
82/// - **Client State Errors**: Issues related to client lifecycle management
83/// - **Namespace Errors**: Problems with namespace format detection and processing
84/// - **Cache Errors**: Network, I/O, and caching-related failures
85///
86/// # Examples
87///
88/// ```rust,no_run
89/// use apollo_rust_client::{Client, Error};
90///
91/// # #[tokio::main]
92/// # async fn main() {
93/// # let client = Client::new(apollo_rust_client::client_config::ClientConfig {
94/// #     app_id: "test".to_string(),
95/// #     config_server: "http://localhost:8080".to_string(),
96/// #     cluster: "default".to_string(),
97/// #     secret: None,
98/// #     cache_dir: None,
99/// #     label: None,
100/// #     ip: None,
101/// #     cache_ttl: None,
102/// # });
103/// match client.namespace("application").await {
104///     Ok(namespace) => {
105///         // Handle successful namespace retrieval
106///     }
107///     Err(Error::Cache(cache_error)) => {
108///         // Handle cache-related errors (network, parsing, etc.)
109///         eprintln!("Cache error: {}", cache_error);
110///     }
111///     Err(Error::Namespace(namespace_error)) => {
112///         // Handle namespace-related errors (format detection, etc.)
113///         eprintln!("Namespace error: {}", namespace_error);
114///     }
115///     Err(e) => {
116///         // Handle other errors
117///         eprintln!("Error: {}", e);
118///     }
119/// }
120/// # }
121/// ```
122#[derive(Debug, thiserror::Error)]
123pub enum Error {
124    /// The client background task is already running.
125    ///
126    /// This error occurs when attempting to start a client that is already
127    /// running its background refresh task. Each client instance can only
128    /// have one active background task at a time.
129    #[error("Client is already running")]
130    AlreadyRunning,
131
132    /// An error occurred during namespace processing.
133    ///
134    /// This includes errors from format detection, parsing, or type conversion
135    /// operations specific to namespace handling.
136    #[error("Namespace error: {0}")]
137    Namespace(#[from] namespace::Error),
138
139    /// An error occurred during cache operations.
140    ///
141    /// This encompasses network errors, I/O failures, serialization issues,
142    /// and other cache-related problems during configuration retrieval or storage.
143    #[error("Cache error: {0}")]
144    Cache(#[from] cache::Error),
145}
146
147impl From<Error> for wasm_bindgen::JsValue {
148    fn from(error: Error) -> Self {
149        error.to_string().into()
150    }
151}
152
153// Type alias for event listeners that can be registered with the cache.
154// For WASM targets, listeners don't need to be Send + Sync since WASM is single-threaded.
155// Listeners are functions that take a `Result<Value, Error>` as an argument.
156// `Value` is the `serde_json::Value` representing the configuration.
157// `Error` is the cache's error enum.
158cfg_if::cfg_if! {
159    if #[cfg(target_arch = "wasm32")] {
160        /// Type alias for event listeners that can be registered with the cache.
161        /// For WASM targets, listeners don't need to be Send + Sync since WASM is single-threaded.
162        /// Listeners are functions that take a `Result<Value, Error>` as an argument.
163        pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>)>;
164    } else {
165        /// Type alias for event listeners that can be registered with the client.
166        /// For native targets, listeners need to be `Send` and `Sync` to be safely
167        /// shared across threads.
168        ///
169        /// Listeners are functions that take a `Result<Namespace<T>, Error>` as an
170        /// argument, where `Namespace<T>` is a fresh copy of the updated namespace,`
171        /// and `Error` is the cache's error enum.
172        pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>) + Send + Sync>;
173    }
174}
175
176/// The main Apollo configuration client.
177///
178/// This struct provides the primary interface for interacting with Apollo configuration services.
179/// It manages multiple namespace caches, handles background refresh tasks, and provides
180/// event listener functionality for real-time configuration updates.
181///
182/// # Features
183///
184/// - **Namespace Management**: Automatically creates and manages caches for different namespaces
185/// - **Background Refresh**: Optional background task that periodically refreshes all namespaces
186/// - **Event Listeners**: Support for registering callbacks on configuration changes
187/// - **Cross-Platform**: Works on both native Rust and WebAssembly targets
188/// - **Thread Safety**: All operations are thread-safe and async-friendly
189///
190/// # Examples
191///
192/// ## Basic Usage
193///
194/// ```rust,no_run
195/// use apollo_rust_client::{Client, client_config::ClientConfig};
196///
197/// # #[tokio::main]
198/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
199/// let config = ClientConfig {
200///     app_id: "my-app".to_string(),
201///     config_server: "http://apollo-server:8080".to_string(),
202///     cluster: "default".to_string(),
203///     secret: None,
204///     cache_dir: None,
205///     label: None,
206///     ip: None,
207///     cache_ttl: None,
208/// };
209///
210/// let mut client = Client::new(config);
211/// client.start().await?;
212///
213/// let namespace = client.namespace("application").await?;
214/// # Ok(())
215/// # }
216/// ```
217///
218/// ## With Event Listeners
219///
220/// ```rust,no_run
221/// use apollo_rust_client::{Client, client_config::ClientConfig};
222/// use std::sync::Arc;
223///
224/// # #[tokio::main]
225/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
226/// # let config = ClientConfig {
227/// #     app_id: "my-app".to_string(),
228/// #     config_server: "http://apollo-server:8080".to_string(),
229/// #     cluster: "default".to_string(),
230/// #     secret: None,
231/// #     cache_dir: None,
232/// #     label: None,
233/// #     ip: None,
234/// #     cache_ttl: None,
235/// # };
236/// # let client = Client::new(config);
237/// client.add_listener("application", Arc::new(|result| {
238///     match result {
239///         Ok(namespace) => println!("Config updated: {:?}", namespace),
240///         Err(e) => eprintln!("Update error: {}", e),
241///     }
242/// })).await;
243/// # Ok(())
244/// # }
245/// ```
246///
247/// # Platform Differences
248///
249/// The client behaves differently on different platforms:
250///
251/// - **Native Rust**: Full feature set with file caching and background tasks
252/// - **WebAssembly**: Memory-only caching, single-threaded execution
253#[wasm_bindgen]
254pub struct Client {
255    /// The configuration settings for this Apollo client instance.
256    ///
257    /// Contains all necessary information to connect to Apollo servers,
258    /// including server URL, application ID, cluster, authentication, and caching settings.
259    client_config: ClientConfig,
260
261    /// Thread-safe storage for namespace-specific caches.
262    ///
263    /// Each namespace gets its own `Cache` instance, wrapped in `Arc` for shared ownership.
264    /// The `RwLock` provides thread-safe read/write access to the namespace map.
265    /// The outer `Arc` allows the background refresh task to safely access the namespaces.
266    namespaces: Arc<RwLock<HashMap<String, Arc<Cache>>>>,
267
268    /// Handle to the background refresh task (native targets only).
269    ///
270    /// On non-wasm32 targets, this holds a `JoinHandle` to the spawned background task
271    /// that periodically refreshes all namespace caches. On wasm32 targets, this is
272    /// always `None` as task management differs in single-threaded environments.
273    handle: Option<async_std::task::JoinHandle<()>>,
274
275    /// Flag indicating whether the background refresh task is active.
276    ///
277    /// Wrapped in `Arc<RwLock<bool>>` for thread-safe shared access between the
278    /// client and its background task. Used to coordinate task lifecycle management.
279    running: Arc<RwLock<bool>>,
280}
281
282impl Client {
283    /// Get a cache for a given namespace.
284    ///
285    /// # Arguments
286    ///
287    /// * `namespace` - The namespace to get the cache for.
288    ///
289    /// # Returns
290    ///
291    /// A cache for the given namespace.
292    pub(crate) async fn cache(&self, namespace: &str) -> Arc<Cache> {
293        let mut namespaces = self.namespaces.write().await;
294        let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
295            trace!("Cache miss, creating cache for namespace {namespace}");
296            Arc::new(Cache::new(self.client_config.clone(), namespace))
297        });
298        cache.clone()
299    }
300
301    pub async fn add_listener(&self, namespace: &str, listener: EventListener) {
302        let mut namespaces = self.namespaces.write().await;
303        let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
304            trace!("Cache miss, creating cache for namespace {namespace}");
305            Arc::new(Cache::new(self.client_config.clone(), namespace))
306        });
307        cache.add_listener(listener).await;
308    }
309
310    pub async fn namespace(&self, namespace: &str) -> Result<namespace::Namespace, Error> {
311        let cache = self.cache(namespace).await;
312        let value = cache.get_value().await?;
313        Ok(namespace::get_namespace(namespace, value)?)
314    }
315
316    /// Starts a background task that periodically refreshes all registered namespace caches.
317    ///
318    /// This method spawns an asynchronous task using `async_std::task::spawn` on native targets
319    /// or `wasm_bindgen_futures::spawn_local` on wasm32 targets. The task loops indefinitely
320    /// (until `stop` is called or the client is dropped) and performs the following actions
321    /// in each iteration:
322    ///
323    /// 1. Iterates through all namespaces currently managed by the client.
324    /// 2. Calls the `refresh` method on each namespace's `Cache` instance.
325    /// 3. Logs any errors encountered during the refresh process.
326    /// 4. Sleeps for a predefined interval (currently 30 seconds) before the next refresh cycle.
327    ///
328    /// # Returns
329    ///
330    /// * `Ok(())` if the background task was successfully started.
331    /// * `Err(Error::AlreadyRunning)` if the background task is already active.
332    pub async fn start(&mut self) -> Result<(), Error> {
333        let mut running = self.running.write().await;
334        if *running {
335            return Err(Error::AlreadyRunning);
336        }
337
338        *running = true;
339
340        let running = self.running.clone();
341        let namespaces = self.namespaces.clone();
342
343        // Spawn a background thread to refresh caches
344        let _handle = spawn(async move {
345            loop {
346                let running = running.read().await;
347                if !*running {
348                    break;
349                }
350
351                let namespaces = namespaces.read().await;
352                // Refresh each namespace's cache
353                for (namespace, cache) in namespaces.iter() {
354                    if let Err(err) = cache.refresh().await {
355                        error!("Failed to refresh cache for namespace {namespace}: {err:?}");
356                    } else {
357                        debug!("Successfully refreshed cache for namespace {namespace}");
358                    }
359                }
360
361                // Sleep for 30 seconds before the next refresh
362                async_std::task::sleep(Duration::from_secs(30)).await;
363            }
364        });
365
366        cfg_if::cfg_if! {
367            if #[cfg(target_arch = "wasm32")] {
368                self.handle = None;
369            } else {
370                self.handle = Some(_handle);
371            }
372        }
373
374        Ok(())
375    }
376
377    /// Stops the background cache refresh task.
378    ///
379    /// This method sets the `running` flag to `false`, signaling the background task
380    /// to terminate its refresh loop.
381    ///
382    /// On non-wasm32 targets, it also attempts to explicitly cancel the spawned task
383    /// by calling `cancel()` on its `JoinHandle` if it exists. This helps to ensure
384    /// that the task is properly cleaned up. On wasm32 targets, there is no direct
385    /// handle to cancel, so setting the `running` flag is the primary mechanism for stopping.
386    pub async fn stop(&mut self) {
387        let mut running = self.running.write().await;
388        *running = false;
389
390        cfg_if::cfg_if! {
391            if #[cfg(not(target_arch = "wasm32"))] {
392                if let Some(handle) = self.handle.take() {
393                    handle.cancel().await;
394                }
395            }
396        }
397    }
398}
399
400#[wasm_bindgen]
401impl Client {
402    /// Create a new Apollo client.
403    ///
404    /// # Arguments
405    ///
406    /// * `client_config` - The configuration for the Apollo client.
407    ///
408    /// # Returns
409    ///
410    /// A new Apollo client.
411    #[wasm_bindgen(constructor)]
412    pub fn new(client_config: ClientConfig) -> Self {
413        Self {
414            client_config,
415            namespaces: Arc::new(RwLock::new(HashMap::new())),
416            handle: None,
417            running: Arc::new(RwLock::new(false)),
418        }
419    }
420
421    /// Registers a JavaScript function as an event listener for this cache (WASM only).
422    ///
423    /// This method is exposed to JavaScript as `addListener`.
424    /// The provided JavaScript function will be called when the cache is refreshed.
425    ///
426    /// The JavaScript listener function is expected to have a signature like:
427    /// `function(data, error)`
428    /// - `data`: The JSON configuration object (if the refresh was successful and data
429    ///           could be serialized) or `null` if an error occurred or serialization failed.
430    /// - `error`: A string describing the error if one occurred during the configuration
431    ///            fetch or processing. If the operation was successful, this will be `null`.
432    ///
433    /// # Arguments
434    ///
435    /// * `js_listener` - A JavaScript `Function` to be called on cache events.
436    ///
437    /// # Example (JavaScript)
438    ///
439    /// ```javascript
440    /// // Assuming `cacheInstance` is an instance of the Rust `Cache` object in JS
441    /// cacheInstance.addListener((data, error) => {
442    ///   if (error) {
443    ///     console.error('Cache update error:', error);
444    ///   } else {
445    ///     console.log('Cache updated:', data);
446    ///   }
447    /// });
448    /// // ... later, when the cache refreshes, the callback will be invoked.
449    /// ```
450    #[cfg(target_arch = "wasm32")]
451    #[wasm_bindgen(js_name = "add_listener")]
452    pub async fn add_listener_wasm(&self, namespace: &str, js_listener: js_sys::Function) {
453        let js_listener_clone = js_listener.clone();
454
455        let event_listener: EventListener = Arc::new(move |result: Result<Namespace, Error>| {
456            let err_js_val: wasm_bindgen::JsValue;
457            let data_js_val: wasm_bindgen::JsValue;
458
459            match result {
460                Ok(value) => {
461                    data_js_val = value.into();
462                    err_js_val = wasm_bindgen::JsValue::UNDEFINED;
463                }
464                Err(cache_error) => {
465                    err_js_val = cache_error.into();
466                    data_js_val = wasm_bindgen::JsValue::UNDEFINED;
467                }
468            };
469
470            // Call the JavaScript listener: listener(data, error)
471            match js_listener_clone.call2(
472                &wasm_bindgen::JsValue::UNDEFINED,
473                &data_js_val,
474                &err_js_val,
475            ) {
476                Ok(_) => {
477                    // JS function called successfully
478                }
479                Err(e) => {
480                    // JS function threw an error or call failed
481                    log::error!("JavaScript listener threw an error: {:?}", e);
482                }
483            }
484        });
485
486        self.add_listener(namespace, event_listener).await; // Call the renamed Rust method
487    }
488
489    #[cfg(target_arch = "wasm32")]
490    #[wasm_bindgen(js_name = "namespace")]
491    pub async fn namespace_wasm(&self, namespace: &str) -> Result<wasm_bindgen::JsValue, Error> {
492        let cache = self.cache(namespace).await;
493        let value = cache.get_value().await?;
494        Ok(namespace::get_namespace(namespace, value)?.into())
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    #[cfg(not(target_arch = "wasm32"))]
502    use lazy_static::lazy_static;
503
504    cfg_if::cfg_if! {
505        if #[cfg(target_arch = "wasm32")] {
506            use std::sync::Mutex;
507        } else {
508            use async_std::sync::Mutex;
509            use async_std::task::block_on;
510        }
511    }
512
513    #[cfg(not(target_arch = "wasm32"))]
514    lazy_static! {
515        pub(crate) static ref CLIENT_NO_SECRET: Client = {
516            let config = ClientConfig {
517                app_id: String::from("101010101"),
518                cluster: String::from("default"),
519                config_server: String::from("http://81.68.181.139:8080"),
520                label: None,
521                secret: None,
522                cache_dir: Some(String::from("/tmp/apollo")),
523                ip: None,
524                cache_ttl: None,
525            };
526            Client::new(config)
527        };
528        pub(crate) static ref CLIENT_WITH_SECRET: Client = {
529            let config = ClientConfig {
530                app_id: String::from("101010102"),
531                cluster: String::from("default"),
532                config_server: String::from("http://81.68.181.139:8080"),
533                label: None,
534                secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
535                cache_dir: Some(String::from("/tmp/apollo")),
536                ip: None,
537                cache_ttl: None,
538            };
539            Client::new(config)
540        };
541        pub(crate) static ref CLIENT_WITH_GRAYSCALE_IP: Client = {
542            let config = ClientConfig {
543                app_id: String::from("101010101"),
544                cluster: String::from("default"),
545                config_server: String::from("http://81.68.181.139:8080"),
546                label: None,
547                secret: None,
548                cache_dir: Some(String::from("/tmp/apollo")),
549                ip: Some(String::from("1.2.3.4")),
550                cache_ttl: None,
551            };
552            Client::new(config)
553        };
554        pub(crate) static ref CLIENT_WITH_GRAYSCALE_LABEL: Client = {
555            let config = ClientConfig {
556                app_id: String::from("101010101"),
557                cluster: String::from("default"),
558                config_server: String::from("http://81.68.181.139:8080"),
559                label: Some(String::from("GrayScale")),
560                secret: None,
561                cache_dir: Some(String::from("/tmp/apollo")),
562                ip: None,
563                cache_ttl: None,
564            };
565            Client::new(config)
566        };
567    }
568
569    pub(crate) fn setup() {
570        cfg_if::cfg_if! {
571            if #[cfg(target_arch = "wasm32")] {
572                let _ = wasm_logger::init(wasm_logger::Config::default());
573                console_error_panic_hook::set_once();
574            } else {
575                let _ = env_logger::builder().is_test(true).try_init();
576            }
577        }
578    }
579
580    #[cfg(not(target_arch = "wasm32"))]
581    #[tokio::test]
582    async fn test_missing_value() {
583        setup();
584        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
585            namespace::Namespace::Properties(properties) => properties,
586            _ => panic!("Expected Properties namespace"),
587        };
588
589        assert_eq!(
590            properties.get_property::<String>("missingValue").await,
591            None
592        );
593    }
594
595    #[cfg(target_arch = "wasm32")]
596    #[wasm_bindgen_test::wasm_bindgen_test]
597    #[allow(dead_code)]
598    async fn test_missing_value_wasm() {
599        setup();
600        let client = create_client_no_secret();
601        let namespace = client.namespace("application").await;
602        match namespace {
603            Ok(namespace) => match namespace {
604                namespace::Namespace::Properties(properties) => {
605                    assert_eq!(properties.get_string("missingValue").await, None);
606                }
607                _ => panic!("Expected Properties namespace"),
608            },
609            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
610        }
611    }
612
613    #[cfg(not(target_arch = "wasm32"))]
614    #[tokio::test]
615    async fn test_string_value() {
616        setup();
617        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
618            namespace::Namespace::Properties(properties) => properties,
619            _ => panic!("Expected Properties namespace"),
620        };
621        assert_eq!(
622            properties.get_property::<String>("stringValue").await,
623            Some("string value".to_string())
624        );
625    }
626
627    #[cfg(target_arch = "wasm32")]
628    #[wasm_bindgen_test::wasm_bindgen_test]
629    #[allow(dead_code)]
630    async fn test_string_value_wasm() {
631        setup();
632        let client = create_client_no_secret();
633        let namespace = client.namespace("application").await;
634        match namespace {
635            Ok(namespace) => match namespace {
636                namespace::Namespace::Properties(properties) => {
637                    assert_eq!(
638                        properties.get_string("stringValue").await,
639                        Some("string value".to_string())
640                    );
641                }
642                _ => panic!("Expected Properties namespace"),
643            },
644            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
645        }
646    }
647
648    #[cfg(not(target_arch = "wasm32"))]
649    #[tokio::test]
650    async fn test_string_value_with_secret() {
651        setup();
652        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
653            namespace::Namespace::Properties(properties) => properties,
654            _ => panic!("Expected Properties namespace"),
655        };
656        assert_eq!(
657            properties.get_property::<String>("stringValue").await,
658            Some("string value".to_string())
659        );
660    }
661
662    #[cfg(target_arch = "wasm32")]
663    #[wasm_bindgen_test::wasm_bindgen_test]
664    #[allow(dead_code)]
665    async fn test_string_value_with_secret_wasm() {
666        setup();
667        let client = create_client_with_secret();
668        let namespace = client.namespace("application").await;
669        match namespace {
670            Ok(namespace) => match namespace {
671                namespace::Namespace::Properties(properties) => {
672                    assert_eq!(
673                        properties.get_string("stringValue").await,
674                        Some("string value".to_string())
675                    );
676                }
677                _ => panic!("Expected Properties namespace"),
678            },
679            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
680        }
681    }
682
683    #[cfg(not(target_arch = "wasm32"))]
684    #[tokio::test]
685    async fn test_int_value() {
686        setup();
687        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
688            namespace::Namespace::Properties(properties) => properties,
689            _ => panic!("Expected Properties namespace"),
690        };
691        assert_eq!(properties.get_property::<i32>("intValue").await, Some(42));
692    }
693
694    #[cfg(target_arch = "wasm32")]
695    #[wasm_bindgen_test::wasm_bindgen_test]
696    #[allow(dead_code)]
697    async fn test_int_value_wasm() {
698        setup();
699        let client = create_client_no_secret();
700        let namespace = client.namespace("application").await;
701        match namespace {
702            Ok(namespace) => match namespace {
703                namespace::Namespace::Properties(properties) => {
704                    assert_eq!(properties.get_int("intValue").await, Some(42));
705                }
706                _ => panic!("Expected Properties namespace"),
707            },
708            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
709        }
710    }
711
712    #[cfg(not(target_arch = "wasm32"))]
713    #[tokio::test]
714    async fn test_int_value_with_secret() {
715        setup();
716        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
717            namespace::Namespace::Properties(properties) => properties,
718            _ => panic!("Expected Properties namespace"),
719        };
720        assert_eq!(properties.get_property::<i32>("intValue").await, Some(42));
721    }
722
723    #[cfg(target_arch = "wasm32")]
724    #[wasm_bindgen_test::wasm_bindgen_test]
725    #[allow(dead_code)]
726    async fn test_int_value_with_secret_wasm() {
727        setup();
728        let client = create_client_with_secret();
729        let namespace = client.namespace("application").await;
730        match namespace {
731            Ok(namespace) => match namespace {
732                namespace::Namespace::Properties(properties) => {
733                    assert_eq!(properties.get_int("intValue").await, Some(42));
734                }
735                _ => panic!("Expected Properties namespace"),
736            },
737            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
738        }
739    }
740
741    #[cfg(not(target_arch = "wasm32"))]
742    #[tokio::test]
743    async fn test_float_value() {
744        setup();
745        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
746            namespace::Namespace::Properties(properties) => properties,
747            _ => panic!("Expected Properties namespace"),
748        };
749        assert_eq!(
750            properties.get_property::<f64>("floatValue").await,
751            Some(4.20)
752        );
753    }
754
755    #[cfg(target_arch = "wasm32")]
756    #[wasm_bindgen_test::wasm_bindgen_test]
757    #[allow(dead_code)]
758    async fn test_float_value_wasm() {
759        setup();
760        let client = create_client_no_secret();
761        let namespace = client.namespace("application").await;
762        match namespace {
763            Ok(namespace) => match namespace {
764                namespace::Namespace::Properties(properties) => {
765                    assert_eq!(properties.get_float("floatValue").await, Some(4.20));
766                }
767                _ => panic!("Expected Properties namespace"),
768            },
769            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
770        }
771    }
772
773    #[cfg(not(target_arch = "wasm32"))]
774    #[tokio::test]
775    async fn test_float_value_with_secret() {
776        setup();
777        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
778            namespace::Namespace::Properties(properties) => properties,
779            _ => panic!("Expected Properties namespace"),
780        };
781        assert_eq!(
782            properties.get_property::<f64>("floatValue").await,
783            Some(4.20)
784        );
785    }
786
787    #[cfg(target_arch = "wasm32")]
788    #[wasm_bindgen_test::wasm_bindgen_test]
789    #[allow(dead_code)]
790    async fn test_float_value_with_secret_wasm() {
791        setup();
792        let client = create_client_with_secret();
793        let namespace = client.namespace("application").await;
794        match namespace {
795            Ok(namespace) => match namespace {
796                namespace::Namespace::Properties(properties) => {
797                    assert_eq!(properties.get_float("floatValue").await, Some(4.20));
798                }
799                _ => panic!("Expected Properties namespace"),
800            },
801            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
802        }
803    }
804
805    #[cfg(not(target_arch = "wasm32"))]
806    #[tokio::test]
807    async fn test_bool_value() {
808        setup();
809        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
810            namespace::Namespace::Properties(properties) => properties,
811            _ => panic!("Expected Properties namespace"),
812        };
813        assert_eq!(
814            properties.get_property::<bool>("boolValue").await,
815            Some(false)
816        );
817    }
818
819    #[cfg(target_arch = "wasm32")]
820    #[wasm_bindgen_test::wasm_bindgen_test]
821    #[allow(dead_code)]
822    async fn test_bool_value_wasm() {
823        setup();
824        let client = create_client_no_secret();
825        let namespace = client.namespace("application").await;
826        match namespace {
827            Ok(namespace) => match namespace {
828                namespace::Namespace::Properties(properties) => {
829                    assert_eq!(properties.get_bool("boolValue").await, Some(false));
830                }
831                _ => panic!("Expected Properties namespace"),
832            },
833            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
834        }
835    }
836
837    #[cfg(not(target_arch = "wasm32"))]
838    #[tokio::test]
839    async fn test_bool_value_with_secret() {
840        setup();
841        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
842            namespace::Namespace::Properties(properties) => properties,
843            _ => panic!("Expected Properties namespace"),
844        };
845        assert_eq!(
846            properties.get_property::<bool>("boolValue").await,
847            Some(false)
848        );
849    }
850
851    #[cfg(target_arch = "wasm32")]
852    #[wasm_bindgen_test::wasm_bindgen_test]
853    #[allow(dead_code)]
854    async fn test_bool_value_with_secret_wasm() {
855        setup();
856        let client = create_client_with_secret();
857        let namespace = client.namespace("application").await;
858        match namespace {
859            Ok(namespace) => match namespace {
860                namespace::Namespace::Properties(properties) => {
861                    assert_eq!(properties.get_bool("boolValue").await, Some(false));
862                }
863                _ => panic!("Expected Properties namespace"),
864            },
865            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
866        }
867    }
868
869    #[cfg(not(target_arch = "wasm32"))]
870    #[tokio::test]
871    async fn test_bool_value_with_grayscale_ip() {
872        setup();
873        let properties = match CLIENT_WITH_GRAYSCALE_IP
874            .namespace("application")
875            .await
876            .unwrap()
877        {
878            namespace::Namespace::Properties(properties) => properties,
879            _ => panic!("Expected Properties namespace"),
880        };
881        assert_eq!(
882            properties.get_property::<bool>("grayScaleValue").await,
883            Some(true)
884        );
885        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
886            namespace::Namespace::Properties(properties) => properties,
887            _ => panic!("Expected Properties namespace"),
888        };
889        assert_eq!(
890            properties.get_property::<bool>("grayScaleValue").await,
891            Some(false)
892        );
893    }
894
895    #[cfg(target_arch = "wasm32")]
896    #[wasm_bindgen_test::wasm_bindgen_test]
897    #[allow(dead_code)]
898    async fn test_bool_value_with_grayscale_ip_wasm() {
899        setup();
900        let client1 = create_client_with_grayscale_ip();
901        let namespace = client1.namespace("application").await;
902        match namespace {
903            Ok(namespace) => match namespace {
904                namespace::Namespace::Properties(properties) => {
905                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(true));
906                }
907                _ => panic!("Expected Properties namespace"),
908            },
909            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
910        }
911
912        let client2 = create_client_no_secret();
913        let namespace = client2.namespace("application").await;
914        match namespace {
915            Ok(namespace) => match namespace {
916                namespace::Namespace::Properties(properties) => {
917                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(false));
918                }
919                _ => panic!("Expected Properties namespace"),
920            },
921            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
922        }
923    }
924
925    #[cfg(not(target_arch = "wasm32"))]
926    #[tokio::test]
927    async fn test_bool_value_with_grayscale_label() {
928        setup();
929        let properties = match CLIENT_WITH_GRAYSCALE_LABEL
930            .namespace("application")
931            .await
932            .unwrap()
933        {
934            namespace::Namespace::Properties(properties) => properties,
935            _ => panic!("Expected Properties namespace"),
936        };
937        assert_eq!(
938            properties.get_property::<bool>("grayScaleValue").await,
939            Some(true)
940        );
941        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
942            namespace::Namespace::Properties(properties) => properties,
943            _ => panic!("Expected Properties namespace"),
944        };
945        assert_eq!(
946            properties.get_property::<bool>("grayScaleValue").await,
947            Some(false)
948        );
949    }
950
951    #[cfg(target_arch = "wasm32")]
952    #[wasm_bindgen_test::wasm_bindgen_test]
953    #[allow(dead_code)]
954    async fn test_bool_value_with_grayscale_label_wasm() {
955        setup();
956        let client1 = create_client_with_grayscale_label();
957        let namespace = client1.namespace("application").await;
958        match namespace {
959            Ok(namespace) => match namespace {
960                namespace::Namespace::Properties(properties) => {
961                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(true));
962                }
963                _ => panic!("Expected Properties namespace"),
964            },
965            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
966        }
967
968        let client2 = create_client_no_secret();
969        let namespace = client2.namespace("application").await;
970        match namespace {
971            Ok(namespace) => match namespace {
972                namespace::Namespace::Properties(properties) => {
973                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(false));
974                }
975                _ => panic!("Expected Properties namespace"),
976            },
977            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
978        }
979    }
980
981    #[cfg(target_arch = "wasm32")]
982    fn create_client_no_secret() -> Client {
983        let config = ClientConfig {
984            app_id: String::from("101010101"),
985            cluster: String::from("default"),
986            config_server: String::from("http://81.68.181.139:8080"),
987            label: None,
988            secret: None,
989            cache_dir: None,
990            ip: None,
991            cache_ttl: None,
992        };
993        Client::new(config)
994    }
995
996    #[cfg(target_arch = "wasm32")]
997    fn create_client_with_secret() -> Client {
998        let config = ClientConfig {
999            app_id: String::from("101010102"),
1000            cluster: String::from("default"),
1001            config_server: String::from("http://81.68.181.139:8080"),
1002            label: None,
1003            secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
1004            cache_dir: None,
1005            ip: None,
1006            cache_ttl: None,
1007        };
1008        Client::new(config)
1009    }
1010
1011    #[cfg(target_arch = "wasm32")]
1012    fn create_client_with_grayscale_ip() -> Client {
1013        let config = ClientConfig {
1014            app_id: String::from("101010101"),
1015            cluster: String::from("default"),
1016            config_server: String::from("http://81.68.181.139:8080"),
1017            label: None,
1018            secret: None,
1019            cache_dir: None,
1020            ip: Some(String::from("1.2.3.4")),
1021            cache_ttl: None,
1022        };
1023        Client::new(config)
1024    }
1025
1026    #[cfg(target_arch = "wasm32")]
1027    fn create_client_with_grayscale_label() -> Client {
1028        let config = ClientConfig {
1029            app_id: String::from("101010101"),
1030            cluster: String::from("default"),
1031            config_server: String::from("http://81.68.181.139:8080"),
1032            label: Some(String::from("GrayScale")),
1033            secret: None,
1034            cache_dir: None,
1035            ip: None,
1036            cache_ttl: None,
1037        };
1038        Client::new(config)
1039    }
1040
1041    #[cfg(not(target_arch = "wasm32"))]
1042    #[tokio::test] // Re-enable for WASM
1043    async fn test_add_listener_and_notify_on_refresh() {
1044        setup();
1045
1046        // Shared state to check if listener was called and what it received
1047        let listener_called_flag = Arc::new(Mutex::new(false));
1048        let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1049
1050        // ClientConfig similar to CLIENT_NO_SECRET from lib.rs tests
1051        // Using the same external test server and app_id as tests in lib.rs
1052        let config = ClientConfig {
1053            config_server: "http://81.68.181.139:8080".to_string(), // Use external test server
1054            app_id: "101010101".to_string(), // Use existing app_id from lib.rs tests
1055            cluster: "default".to_string(),
1056            cache_dir: Some(String::from("/tmp/apollo")), // Use a writable directory
1057            secret: None,
1058            label: None,
1059            ip: None,
1060            cache_ttl: None,
1061            // ..Default::default() // Be careful with Default if it doesn't set all needed fields for tests
1062        };
1063
1064        let client = Client::new(config);
1065
1066        let flag_clone = listener_called_flag.clone();
1067        let data_clone = received_config_data.clone();
1068
1069        let listener: EventListener = Arc::new(move |result| {
1070            let mut called_guard = block_on(flag_clone.lock());
1071            *called_guard = true;
1072            if let Ok(config_value) = result {
1073                match config_value {
1074                    Namespace::Properties(_) => {
1075                        let mut data_guard = block_on(data_clone.lock());
1076                        *data_guard = Some(config_value.clone());
1077                    }
1078                    _ => {
1079                        panic!("Expected Properties namespace, got {config_value:?}");
1080                    }
1081                }
1082            }
1083            // In a real scenario, avoid panicking in a listener.
1084            // For a test, this is acceptable to signal issues.
1085        });
1086
1087        client.add_listener("application", listener).await;
1088
1089        let cache = client.cache("application").await;
1090
1091        // Perform a refresh. This should trigger the listener.
1092        // The test Apollo server (localhost:8071) should have some known config for "SampleApp" "application" namespace.
1093        match cache.refresh().await {
1094            Ok(_) => log::debug!("Refresh successful for test_add_listener_and_notify_on_refresh"),
1095            Err(e) => panic!("Cache refresh failed during test: {e:?}"),
1096        }
1097
1098        // Check if the listener was called
1099        let called = *listener_called_flag.lock().await;
1100        assert!(called, "Listener was not called.");
1101
1102        // Check if config data was received
1103        let config_data_guard = received_config_data.lock().await;
1104        assert!(
1105            config_data_guard.is_some(),
1106            "Listener did not receive config data."
1107        );
1108
1109        // Optionally, assert specific content if known.
1110        // Assert based on known data for app_id "101010101", namespace "application"
1111        // from the external test server. Example: "stringValue"
1112        if let Some(value) = config_data_guard.as_ref() {
1113            match value {
1114                Namespace::Properties(properties) => {
1115                    assert_eq!(
1116                        properties.get_string("stringValue").await,
1117                        Some(String::from("string value")),
1118                        "Received config data does not match expected content for stringValue."
1119                    );
1120                }
1121                _ => {
1122                    panic!("Expected Properties namespace, got {value:?}");
1123                }
1124            }
1125        }
1126    }
1127
1128    #[cfg(target_arch = "wasm32")]
1129    #[wasm_bindgen_test::wasm_bindgen_test]
1130    async fn test_add_listener_wasm_and_notify() {
1131        setup(); // Existing test setup
1132
1133        // Shared state to check if listener was called and what it received
1134        let listener_called_flag = Arc::new(Mutex::new(false));
1135        let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1136
1137        let flag_clone = listener_called_flag.clone();
1138        let data_clone = received_config_data.clone();
1139
1140        // Create JS Listener Function that updates our shared state
1141        let js_listener_func_body = format!(
1142            r#"
1143            (data, error) => {{
1144                // We can't use window in Node.js, so we'll use a different approach
1145                // The Rust closure will handle the verification
1146                console.log('JS Listener called with error:', error);
1147                console.log('JS Listener called with data:', data);
1148            }}
1149        "#
1150        );
1151
1152        let js_listener = js_sys::Function::new_with_args("data, error", &js_listener_func_body);
1153
1154        let client = create_client_no_secret();
1155
1156        // Add a Rust listener to verify the functionality
1157        let rust_listener: EventListener = Arc::new(move |result| {
1158            let mut called_guard = flag_clone.lock().unwrap();
1159            *called_guard = true;
1160            if let Ok(config_value) = result {
1161                let mut data_guard = data_clone.lock().unwrap();
1162                *data_guard = Some(config_value);
1163            }
1164        });
1165
1166        client.add_listener("application", rust_listener).await;
1167
1168        // Add JS Listener
1169        client.add_listener_wasm("application", js_listener).await;
1170
1171        let cache = client.cache("application").await;
1172
1173        // Trigger Refresh
1174        match cache.refresh().await {
1175            Ok(_) => web_sys::console::log_1(&"WASM Test: Refresh successful".into()), // web_sys::console for logging
1176            Err(e) => panic!("WASM Test: Cache refresh failed: {:?}", e),
1177        }
1178
1179        // Verify Listener Was Called using our Rust listener
1180        let called = *listener_called_flag.lock().unwrap();
1181        assert!(called, "Listener was not called.");
1182
1183        // Check if config data was received
1184        let config_data_guard = received_config_data.lock().unwrap();
1185        assert!(
1186            config_data_guard.is_some(),
1187            "Listener did not receive config data."
1188        );
1189
1190        // Verify the content
1191        if let Some(value) = config_data_guard.as_ref() {
1192            match value {
1193                namespace::Namespace::Properties(properties) => {
1194                    assert_eq!(
1195                        properties.get_string("stringValue").await,
1196                        Some("string value".to_string())
1197                    );
1198                }
1199                _ => panic!("Expected Properties namespace"),
1200            }
1201        }
1202    }
1203}