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