Skip to main content

apollo_rust_client/
client_config.rs

1//! Configuration management for the Apollo client.
2//!
3//! This module provides the `ClientConfig` struct and related functionality for configuring
4//! the Apollo client. It supports both direct configuration and environment variable-based
5//! configuration, with platform-specific optimizations for native Rust and WebAssembly targets.
6//!
7//! # Configuration Sources
8//!
9//! - **Direct Configuration**: Manually specify all configuration fields
10//! - **Environment Variables**: Automatically load configuration from environment variables
11//! - **Mixed Approach**: Load from environment variables and override specific fields
12//!
13//! ## Environment Variables
14//!
15//! The following environment variables are supported:
16//! - `APP_ID`: Your application identifier in Apollo
17//! - `APOLLO_CONFIG_SERVICE`: The Apollo configuration server URL
18//! - `IDC`: The cluster name (defaults to "default")
19//! - `APOLLO_ACCESS_KEY_SECRET`: Authentication secret key
20//! - `APOLLO_LABEL`: Labels for grayscale release targeting
21//! - `APOLLO_CACHE_DIR`: Local cache directory
22//! - `APOLLO_CACHE_TTL`: Cache time-to-live in seconds
23//! - `APOLLO_ALLOW_INSECURE_HTTPS`: Whether to allow insecure HTTPS connections
24//!
25//! # Platform Support
26//!
27//! - **Native Rust**: Full feature set including file caching and environment variable support
28//! - **WebAssembly**: Optimized for browser environments with persistent localStorage caching (and Node.js in-memory fallback)
29//!
30//! # Examples
31//!
32//! ## Direct Configuration
33//!
34//! ```rust
35//! use apollo_rust_client::client_config::ClientConfig;
36//!
37//! let config = ClientConfig {
38//!     app_id: "my-app".to_string(),
39//!     config_server: "http://apollo-server:8080".to_string(),
40//!     cluster: "default".to_string(),
41//!     secret: Some("secret-key".to_string()),
42//!     cache_dir: None, // Uses default
43//!     label: Some("production".to_string()),
44//!     ip: Some("192.168.1.100".to_string()),
45//!     allow_insecure_https: None,
46//!     #[cfg(not(target_arch = "wasm32"))]
47//!     cache_ttl: None,
48//! };
49//! ```
50//!
51//! ## Environment Variable Configuration
52//!
53//! ```rust,no_run
54//! use apollo_rust_client::client_config::ClientConfig;
55//!
56//! // Requires APP_ID and APOLLO_CONFIG_SERVICE environment variables
57//! let config = ClientConfig::from_env()?;
58//! # Ok::<(), apollo_rust_client::client_config::Error>(())
59//! ```
60
61use cfg_if::cfg_if;
62use wasm_bindgen::prelude::*;
63
64/// Comprehensive error types that can occur during client configuration.
65///
66/// This enum covers all possible error conditions that may arise during
67/// client configuration operations, from environment variable access to
68/// configuration validation.
69///
70/// # Error Categories
71///
72/// - **Environment Variable Errors**: Issues with accessing or parsing environment variables
73///
74/// # Examples
75///
76/// ```rust
77/// use apollo_rust_client::client_config::{ClientConfig, Error};
78///
79/// match ClientConfig::from_env() {
80///     Ok(config) => {
81///         // Handle successful configuration creation
82///     }
83///     Err(Error::EnvVar(var_error, var_name)) => {
84///         // Handle missing or invalid environment variables
85///         eprintln!("Environment variable '{}' error: {}", var_name, var_error);
86///     }
87///     Err(e) => {
88///         // Handle other errors
89///         eprintln!("Configuration error: {}", e);
90///     }
91/// }
92/// ```
93#[derive(Debug, thiserror::Error)]
94pub enum Error {
95    /// An environment variable access error occurred.
96    ///
97    /// This error occurs when attempting to read an environment variable
98    /// that is not set or cannot be accessed. The error includes both
99    /// the underlying system error and the name of the variable that failed.
100    #[error("Environment variable is not set: {1}")]
101    EnvVar(std::env::VarError, String),
102}
103
104/// Configuration settings for the Apollo client.
105///
106/// This struct contains all the necessary information to connect to and interact with
107/// an Apollo Configuration Center. It supports various configuration options including
108/// authentication, caching, and grayscale release targeting.
109///
110/// # Required Fields
111///
112/// - `app_id`: Your application identifier in Apollo
113/// - `config_server`: The Apollo configuration server URL
114/// - `cluster`: The cluster name (typically "default")
115///
116/// # Optional Fields
117///
118/// - `secret`: Authentication secret key for secure access
119/// - `cache_dir`: Local cache directory (native targets only)
120/// - `label`: Labels for grayscale release targeting
121/// - `ip`: IP address for grayscale release targeting
122/// - `allow_insecure_https`: Whether to allow insecure HTTPS connections (self-signed certificates)
123///
124/// # Examples
125///
126/// ## Minimal Configuration
127///
128/// ```rust
129/// use apollo_rust_client::client_config::ClientConfig;
130///
131/// let config = ClientConfig {
132///     app_id: "my-app".to_string(),
133///     config_server: "http://apollo-server:8080".to_string(),
134///     cluster: "default".to_string(),
135///     secret: None,
136///     cache_dir: None,
137///     label: None,
138///     ip: None,
139///     allow_insecure_https: None,
140///     #[cfg(not(target_arch = "wasm32"))]
141///     cache_ttl: None,
142/// };
143/// ```
144///
145/// ## Full Configuration
146///
147/// ```rust
148/// use apollo_rust_client::client_config::ClientConfig;
149///
150/// let config = ClientConfig {
151///     app_id: "my-app".to_string(),
152///     config_server: "http://apollo-server:8080".to_string(),
153///     cluster: "production".to_string(),
154///     secret: Some("secret-key".to_string()),
155///     cache_dir: Some("/custom/cache/path".to_string()),
156///     label: Some("canary,beta".to_string()),
157///     ip: Some("192.168.1.100".to_string()),
158///     allow_insecure_https: Some(true), // Allow self-signed certificates
159///     #[cfg(not(target_arch = "wasm32"))]
160///     cache_ttl: None,
161/// };
162/// ```
163#[derive(Clone, Debug)]
164#[wasm_bindgen(getter_with_clone)]
165pub struct ClientConfig {
166    /// The unique identifier for your application in Apollo.
167    ///
168    /// This is used to identify which application's configuration to retrieve
169    /// from the Apollo Configuration Center.
170    pub app_id: String,
171
172    /// The cluster name to connect to.
173    ///
174    /// Clusters allow you to organize different environments or deployment
175    /// groups. Common values include "default", "production", "staging", etc.
176    pub cluster: String,
177
178    /// The directory to store local cache files (native targets only).
179    ///
180    /// On native Rust targets, this specifies where configuration files should
181    /// be cached locally. If `None`, defaults to `/opt/data/{app_id}/config-cache`.
182    /// On WebAssembly targets, this is always `None` as file system access is not available.
183    pub cache_dir: Option<String>,
184
185    /// The Apollo configuration server URL.
186    ///
187    /// This should be the base URL of your Apollo Configuration Center server,
188    /// including the protocol (http/https) and port if necessary.
189    /// Example: "http://apollo-server:8080"
190    #[allow(clippy::doc_markdown)]
191    pub config_server: String,
192
193    /// Optional secret key for authentication with the Apollo server.
194    ///
195    /// If your Apollo namespace requires authentication, provide the secret key here.
196    /// This is used to generate HMAC-SHA1 signatures for secure access to protected
197    /// configuration namespaces.
198    pub secret: Option<String>,
199
200    /// Labels for grayscale release targeting.
201    ///
202    /// Comma-separated list of labels that identify this client instance.
203    /// Apollo can use these labels to determine which configuration version
204    /// to serve during grayscale releases. Example: "canary,beta"
205    pub label: Option<String>,
206
207    /// IP address for grayscale release targeting.
208    ///
209    /// The IP address of this client instance. Apollo can use this IP address
210    /// to determine which configuration version to serve during grayscale releases
211    /// based on IP-based targeting rules.
212    pub ip: Option<String>,
213
214    /// Whether to allow insecure HTTPS connections (self-signed certificates).
215    ///
216    /// When set to `true`, the client will accept self-signed SSL certificates
217    /// and other insecure HTTPS connections. This is useful in company internal
218    /// networks or development environments where self-signed certificates are used.
219    ///
220    /// **Warning**: Setting this to `true` reduces security by bypassing SSL
221    /// certificate validation. Only use this in trusted internal networks.
222    pub allow_insecure_https: Option<bool>,
223
224    /// Time-to-live for the cache, in seconds (native targets only).
225    ///
226    /// When using `from_env`, this defaults to 600 seconds (10 minutes) if
227    /// the `APOLLO_CACHE_TTL` environment variable is not set.
228    /// This field is not available on WebAssembly targets as disk caching is not supported.
229    #[cfg(not(target_arch = "wasm32"))]
230    pub cache_ttl: Option<u64>,
231
232    /// The refresh interval in seconds for the background namespace cache refresh loop (native targets only).
233    ///
234    /// When using `from_env`, this defaults to 30 seconds if
235    /// the `APOLLO_REFRESH_INTERVAL` environment variable is not set.
236    /// This field is not available on WebAssembly targets as background refresh is not supported.
237    #[cfg(not(target_arch = "wasm32"))]
238    pub refresh_interval: Option<u64>,
239
240    /// A pre-configured `reqwest::Client` (native targets only) to allow custom HTTP pools, proxies, headers, or tracers.
241    ///
242    /// If not specified, defaults to standard client construction.
243    #[cfg(not(target_arch = "wasm32"))]
244    #[wasm_bindgen(skip)]
245    pub http_client: Option<reqwest::Client>,
246}
247
248impl From<Error> for JsValue {
249    fn from(error: Error) -> Self {
250        JsValue::from_str(&error.to_string())
251    }
252}
253
254cfg_if! {
255    if #[cfg(not(target_arch = "wasm32"))] {
256        impl ClientConfig {
257            /// Creates a new `ClientConfig` by reading environment variables.
258            ///
259            /// This function loads configuration values from the following environment variables:
260            /// - `APP_ID` (required): The Apollo application ID.
261            /// - `APOLLO_ACCESS_KEY_SECRET` (optional): Secret key for authentication.
262            /// - `IDC` (optional): Cluster name. Defaults to `"default"` if not set.
263            /// - `APOLLO_CONFIG_SERVICE` (required): The Apollo config server URL.
264            /// - `APOLLO_LABEL` (optional): Comma-separated labels for grayscale release.
265            /// - `APOLLO_CACHE_DIR` (optional): Directory for local cache storage.
266            /// - `APOLLO_ALLOW_INSECURE_HTTPS` (optional): If set to `"true"`, allows insecure HTTPS.
267            /// - `APOLLO_CACHE_TTL` (optional): Cache time-to-live in seconds. Defaults to 600 if not set.
268            ///
269            /// # Returns
270            ///
271            /// * `Ok(ClientConfig)` if all required environment variables are present and valid.
272            /// * `Err(Error)` if a required environment variable is missing or invalid.
273            ///
274            /// # Errors
275            ///
276            /// This function will return an error if:
277            /// - The `APP_ID` environment variable is missing.
278            /// - The `APOLLO_CONFIG_SERVICE` environment variable is missing.
279            /// - Any environment variable that is expected to be a number (such as `APOLLO_CACHE_TTL`)
280            ///   cannot be parsed as the correct type.
281            /// - Any other required environment variable is missing or invalid.
282            pub fn from_env() -> Result<Self, Error> {
283                let app_id =
284                    std::env::var("APP_ID").map_err(|e| Error::EnvVar(e, "APP_ID".to_string()))?;
285                let secret = std::env::var("APOLLO_ACCESS_KEY_SECRET")
286                    .map_err(|e| Error::EnvVar(e, "APOLLO_ACCESS_KEY_SECRET".to_string()))
287                    .ok();
288                let cluster = std::env::var("IDC").unwrap_or("default".to_string());
289                let config_server = std::env::var("APOLLO_CONFIG_SERVICE")
290                    .map_err(|e| Error::EnvVar(e, "APOLLO_CONFIG_SERVICE".to_string()))?;
291                let label = std::env::var("APOLLO_LABEL")
292                    .map_err(|e| Error::EnvVar(e, "APOLLO_LABEL".to_string()))
293                    .ok();
294                let cache_dir = std::env::var("APOLLO_CACHE_DIR").ok();
295                let allow_insecure_https = std::env::var("APOLLO_ALLOW_INSECURE_HTTPS")
296                    .ok()
297                    .and_then(|s| s.parse().ok());
298                let cache_ttl = std::env::var("APOLLO_CACHE_TTL")
299                    .ok()
300                    .and_then(|s| s.parse().ok())
301                    .or(Some(600));
302                let refresh_interval = std::env::var("APOLLO_REFRESH_INTERVAL")
303                    .ok()
304                    .and_then(|s| s.parse().ok())
305                    .map(|v| {
306                        let min_val = if cfg!(test) { 1 } else { 30 };
307                        if v < min_val { min_val } else { v }
308                    })
309                    .or(Some(30));
310                Ok(Self {
311                    app_id,
312                    secret,
313                    cluster,
314                    config_server,
315                    cache_dir,
316                    label,
317                    ip: None,
318                    allow_insecure_https,
319                    cache_ttl,
320                    refresh_interval,
321                    http_client: None,
322                })
323            }
324        }
325    } else {
326        #[wasm_bindgen]
327        impl ClientConfig {
328            /// Create a new configuration from environment variables.
329            ///
330            /// # Returns
331            ///
332            /// A new configuration instance.
333            pub fn from_env() -> Result<Self, Error> {
334                let app_id =
335                    std::env::var("APP_ID").map_err(|e| Error::EnvVar(e, "APP_ID".to_string()))?;
336                let secret = std::env::var("APOLLO_ACCESS_KEY_SECRET")
337                    .map_err(|e| Error::EnvVar(e, "APOLLO_ACCESS_KEY_SECRET".to_string()))
338                    .ok();
339                let cluster = std::env::var("IDC").unwrap_or("default".to_string());
340                let config_server = std::env::var("APOLLO_CONFIG_SERVICE")
341                    .map_err(|e| Error::EnvVar(e, "APOLLO_CONFIG_SERVICE".to_string()))?;
342                let label = std::env::var("APOLLO_LABEL")
343                    .map_err(|e| Error::EnvVar(e, "APOLLO_LABEL".to_string()))
344                    .ok();
345                let cache_dir = std::env::var("APOLLO_CACHE_DIR").ok();
346                let allow_insecure_https = std::env::var("APOLLO_ALLOW_INSECURE_HTTPS")
347                    .ok()
348                    .and_then(|s| s.parse().ok());
349                Ok(Self {
350                    app_id,
351                    secret,
352                    cluster,
353                    config_server,
354                    cache_dir,
355                    label,
356                    ip: None,
357                    allow_insecure_https,
358                })
359            }
360        }
361    }
362}
363
364cfg_if! {
365    if #[cfg(not(target_arch = "wasm32"))] {
366        impl ClientConfig {
367            /// Returns the path to the cache directory for the Apollo client.
368            ///
369            /// This method constructs a `std::path::PathBuf` representing the directory
370            /// where Apollo configuration cache files will be stored. The logic is as follows:
371            ///
372            /// 1.  It uses the `cache_dir` field from the `ClientConfig` instance if it's set.
373            /// 2.  If `cache_dir` is `None`, it defaults to `/opt/data`.
374            /// 3.  It then appends the `app_id` (from `ClientConfig`) as a subdirectory.
375            /// 4.  Finally, it appends `config-cache` as another subdirectory.
376            ///
377            /// # Examples
378            ///
379            /// - If `cache_dir` is `Some("/my/custom/path".to_string())` and `app_id` is `"my_app"`,
380            ///   the result will be `/my/custom/path/my_app/config-cache`.
381            /// - If `cache_dir` is `None` and `app_id` is `"another_app"`,
382            ///   the result will be `/opt/data/another_app/config-cache`.
383            ///
384            /// # Returns
385            ///
386            /// A `std::path::PathBuf` for the cache directory.
387            pub(crate) fn get_cache_dir(&self) -> std::path::PathBuf {
388                let base = std::path::PathBuf::from(
389                    &self
390                        .cache_dir
391                        .clone()
392                        .unwrap_or_else(|| String::from("/opt/data")),
393                );
394                base.join(&self.app_id).join("config-cache")
395            }
396        }
397    } else {
398        #[wasm_bindgen]
399        impl ClientConfig {
400            /// Creates a new `ClientConfig` instance specifically for wasm32 targets.
401            ///
402            /// This constructor takes essential configuration parameters (`app_id`, `config_server`, `cluster`)
403            /// directly as arguments. Other configuration fields are initialized to `None` or their
404            /// default values:
405            /// - `cache_dir`: `None` (file system caching is not typically used in wasm32).
406            /// - `secret`: `None`.
407            /// - `label`: `None`.
408            /// - `ip`: `None`.
409            ///
410            /// This is in contrast to the `from_env` method, which attempts to read all
411            /// configuration values from environment variables.
412            ///
413            /// # Arguments
414            ///
415            /// * `app_id` - The unique identifier for your application.
416            /// * `config_server` - The Apollo config server URL.
417            /// * `cluster` - The cluster name (e.g., "default").
418            ///
419            /// # Returns
420            ///
421            /// A new `ClientConfig` instance.
422            #[wasm_bindgen(constructor)]
423            pub fn new(app_id: String, config_server: String, cluster: String) -> Self {
424                Self {
425                    app_id,
426                    config_server,
427                    cluster,
428                    cache_dir: None,
429                    secret: None,
430                    label: None,
431                    ip: None,
432                    allow_insecure_https: None,
433                }
434            }
435        }
436    }
437}