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