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 memory-only caching
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
233impl From<Error> for JsValue {
234    fn from(error: Error) -> Self {
235        JsValue::from_str(&error.to_string())
236    }
237}
238
239cfg_if! {
240    if #[cfg(not(target_arch = "wasm32"))] {
241        impl ClientConfig {
242            /// Creates a new `ClientConfig` by reading environment variables.
243            ///
244            /// This function loads configuration values from the following environment variables:
245            /// - `APP_ID` (required): The Apollo application ID.
246            /// - `APOLLO_ACCESS_KEY_SECRET` (optional): Secret key for authentication.
247            /// - `IDC` (optional): Cluster name. Defaults to `"default"` if not set.
248            /// - `APOLLO_CONFIG_SERVICE` (required): The Apollo config server URL.
249            /// - `APOLLO_LABEL` (optional): Comma-separated labels for grayscale release.
250            /// - `APOLLO_CACHE_DIR` (optional): Directory for local cache storage.
251            /// - `APOLLO_ALLOW_INSECURE_HTTPS` (optional): If set to `"true"`, allows insecure HTTPS.
252            /// - `APOLLO_CACHE_TTL` (optional): Cache time-to-live in seconds. Defaults to 600 if not set.
253            ///
254            /// # Returns
255            ///
256            /// * `Ok(ClientConfig)` if all required environment variables are present and valid.
257            /// * `Err(Error)` if a required environment variable is missing or invalid.
258            ///
259            /// # Errors
260            ///
261            /// This function will return an error if:
262            /// - The `APP_ID` environment variable is missing.
263            /// - The `APOLLO_CONFIG_SERVICE` environment variable is missing.
264            /// - Any environment variable that is expected to be a number (such as `APOLLO_CACHE_TTL`)
265            ///   cannot be parsed as the correct type.
266            /// - Any other required environment variable is missing or invalid.
267            pub fn from_env() -> Result<Self, Error> {
268                let app_id =
269                    std::env::var("APP_ID").map_err(|e| (Error::EnvVar(e, "APP_ID".to_string())))?;
270                let secret = std::env::var("APOLLO_ACCESS_KEY_SECRET")
271                    .map_err(|e| (Error::EnvVar(e, "APOLLO_ACCESS_KEY_SECRET".to_string())))
272                    .ok();
273                let cluster = std::env::var("IDC").unwrap_or("default".to_string());
274                let config_server = std::env::var("APOLLO_CONFIG_SERVICE")
275                    .map_err(|e| (Error::EnvVar(e, "APOLLO_CONFIG_SERVICE".to_string())))?;
276                let label = std::env::var("APOLLO_LABEL")
277                    .map_err(|e| (Error::EnvVar(e, "APOLLO_LABEL".to_string())))
278                    .ok();
279                let cache_dir = std::env::var("APOLLO_CACHE_DIR").ok();
280                let allow_insecure_https = std::env::var("APOLLO_ALLOW_INSECURE_HTTPS")
281                    .ok()
282                    .and_then(|s| s.parse().ok());
283                let cache_ttl = std::env::var("APOLLO_CACHE_TTL")
284                    .ok()
285                    .and_then(|s| s.parse().ok())
286                    .or(Some(600));
287                Ok(Self {
288                    app_id,
289                    secret,
290                    cluster,
291                    config_server,
292                    cache_dir,
293                    label,
294                    ip: None,
295                    allow_insecure_https,
296                    cache_ttl,
297                })
298            }
299        }
300    } else {
301        #[wasm_bindgen]
302        impl ClientConfig {
303            /// Create a new configuration from environment variables.
304            ///
305            /// # Returns
306            ///
307            /// A new configuration instance.
308            pub fn from_env() -> Result<Self, Error> {
309                let app_id =
310                    std::env::var("APP_ID").map_err(|e| (Error::EnvVar(e, "APP_ID".to_string())))?;
311                let secret = std::env::var("APOLLO_ACCESS_KEY_SECRET")
312                    .map_err(|e| (Error::EnvVar(e, "APOLLO_ACCESS_KEY_SECRET".to_string())))
313                    .ok();
314                let cluster = std::env::var("IDC").unwrap_or("default".to_string());
315                let config_server = std::env::var("APOLLO_CONFIG_SERVICE")
316                    .map_err(|e| (Error::EnvVar(e, "APOLLO_CONFIG_SERVICE".to_string())))?;
317                let label = std::env::var("APOLLO_LABEL")
318                    .map_err(|e| (Error::EnvVar(e, "APOLLO_LABEL".to_string())))
319                    .ok();
320                let cache_dir = std::env::var("APOLLO_CACHE_DIR").ok();
321                let allow_insecure_https = std::env::var("APOLLO_ALLOW_INSECURE_HTTPS")
322                    .ok()
323                    .and_then(|s| s.parse().ok());
324                Ok(Self {
325                    app_id,
326                    secret,
327                    cluster,
328                    config_server,
329                    cache_dir,
330                    label,
331                    ip: None,
332                    allow_insecure_https,
333                })
334            }
335        }
336    }
337}
338
339cfg_if! {
340    if #[cfg(not(target_arch = "wasm32"))] {
341        impl ClientConfig {
342            /// Returns the path to the cache directory for the Apollo client.
343            ///
344            /// This method constructs a `std::path::PathBuf` representing the directory
345            /// where Apollo configuration cache files will be stored. The logic is as follows:
346            ///
347            /// 1.  It uses the `cache_dir` field from the `ClientConfig` instance if it's set.
348            /// 2.  If `cache_dir` is `None`, it defaults to `/opt/data`.
349            /// 3.  It then appends the `app_id` (from `ClientConfig`) as a subdirectory.
350            /// 4.  Finally, it appends `config-cache` as another subdirectory.
351            ///
352            /// # Examples
353            ///
354            /// - If `cache_dir` is `Some("/my/custom/path".to_string())` and `app_id` is `"my_app"`,
355            ///   the result will be `/my/custom/path/my_app/config-cache`.
356            /// - If `cache_dir` is `None` and `app_id` is `"another_app"`,
357            ///   the result will be `/opt/data/another_app/config-cache`.
358            ///
359            /// # Returns
360            ///
361            /// A `std::path::PathBuf` for the cache directory.
362            pub(crate) fn get_cache_dir(&self) -> std::path::PathBuf {
363                let base = std::path::PathBuf::from(
364                    &self
365                        .cache_dir
366                        .clone()
367                        .unwrap_or_else(|| String::from("/opt/data")),
368                );
369                base.join(&self.app_id).join("config-cache")
370            }
371        }
372    } else {
373        #[wasm_bindgen]
374        impl ClientConfig {
375            /// Creates a new `ClientConfig` instance specifically for wasm32 targets.
376            ///
377            /// This constructor takes essential configuration parameters (`app_id`, `config_server`, `cluster`)
378            /// directly as arguments. Other configuration fields are initialized to `None` or their
379            /// default values:
380            /// - `cache_dir`: `None` (file system caching is not typically used in wasm32).
381            /// - `secret`: `None`.
382            /// - `label`: `None`.
383            /// - `ip`: `None`.
384            ///
385            /// This is in contrast to the `from_env` method, which attempts to read all
386            /// configuration values from environment variables.
387            ///
388            /// # Arguments
389            ///
390            /// * `app_id` - The unique identifier for your application.
391            /// * `config_server` - The Apollo config server URL.
392            /// * `cluster` - The cluster name (e.g., "default").
393            ///
394            /// # Returns
395            ///
396            /// A new `ClientConfig` instance.
397            #[wasm_bindgen(constructor)]
398            pub fn new(app_id: String, config_server: String, cluster: String) -> Self {
399                Self {
400                    app_id,
401                    config_server,
402                    cluster,
403                    cache_dir: None,
404                    secret: None,
405                    label: None,
406                    ip: None,
407                    allow_insecure_https: None,
408                }
409            }
410        }
411    }
412}