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}