riglr_config/
builder.rs

1//! Configuration builder for programmatic config construction
2//!
3//! Provides a fluent API for building configuration objects with validation.
4
5// Suppress analyzer warnings for legitimate new() method calls
6#![allow(clippy::new_without_default, clippy::empty_line_after_outer_attr)]
7
8use crate::{
9    AppConfig, Config, ConfigResult, DatabaseConfig, FeaturesConfig, LogLevel, NetworkConfig,
10    ProvidersConfig,
11};
12
13/// Builder for creating Config instances programmatically
14///
15/// This builder provides a fluent API for constructing configuration objects
16/// piece by piece. The builder validates the configuration when `build()` is called.
17///
18/// # Examples
19///
20/// ```rust
21/// use riglr_config::{ConfigBuilder, AppConfig, Environment, LogLevel};
22///
23/// let config = ConfigBuilder::default()
24///     .app(AppConfig {
25///         environment: Environment::Development,
26///         log_level: LogLevel::Debug,
27///         ..Default::default()
28///     })
29///     .build()
30///     .expect("Failed to build config");
31/// ```
32#[derive(Default)]
33pub struct ConfigBuilder {
34    app: AppConfig,
35    database: DatabaseConfig,
36    network: NetworkConfig,
37    providers: ProvidersConfig,
38    features: FeaturesConfig,
39}
40
41impl ConfigBuilder {
42    /// Create a new ConfigBuilder with default values
43    ///
44    /// This is equivalent to `ConfigBuilder::default()` but provides a more explicit API
45    /// for builder pattern initialization.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Merge another configuration into this builder
51    ///
52    /// Non-default values from the other config will override current values.
53    /// This allows for layered configuration where later sources take precedence.
54    #[must_use]
55    pub fn merge(mut self, other: Config) -> Self {
56        self.merge_app_config(other.app);
57        self.merge_database_config(other.database);
58        self.merge_network_config(other.network);
59        self.merge_providers_config(other.providers);
60        self.merge_features_config(other.features);
61        self
62    }
63
64    /// Merge AppConfig, taking non-default values from other
65    fn merge_app_config(&mut self, other: AppConfig) {
66        let default_app = AppConfig::default();
67
68        // Use non-default values from other, falling back to current values
69        if other.port != default_app.port {
70            self.app.port = other.port;
71        }
72        if other.environment != default_app.environment {
73            self.app.environment = other.environment;
74        }
75        if other.log_level != default_app.log_level {
76            self.app.log_level = other.log_level;
77        }
78        if other.use_testnet != default_app.use_testnet {
79            self.app.use_testnet = other.use_testnet;
80        }
81        // Merge nested configs - for simplicity, replace if they have any non-default values
82        if other.transaction.max_gas_price_gwei != default_app.transaction.max_gas_price_gwei
83            || other.transaction.priority_fee_gwei != default_app.transaction.priority_fee_gwei
84            || other.transaction.slippage_tolerance_percent
85                != default_app.transaction.slippage_tolerance_percent
86            || other.transaction.deadline_seconds != default_app.transaction.deadline_seconds
87        {
88            self.app.transaction = other.transaction;
89        }
90        if other.retry.max_retry_attempts != default_app.retry.max_retry_attempts
91            || other.retry.retry_delay_ms != default_app.retry.retry_delay_ms
92            || other.retry.retry_backoff_multiplier != default_app.retry.retry_backoff_multiplier
93            || other.retry.max_retry_delay_ms != default_app.retry.max_retry_delay_ms
94        {
95            self.app.retry = other.retry;
96        }
97    }
98
99    /// Merge DatabaseConfig, taking non-default values from other
100    fn merge_database_config(&mut self, other: DatabaseConfig) {
101        let default_database = DatabaseConfig::default();
102
103        if other.redis_url != default_database.redis_url {
104            self.database.redis_url = other.redis_url;
105        }
106        if other.neo4j_url != default_database.neo4j_url {
107            self.database.neo4j_url = other.neo4j_url;
108        }
109    }
110
111    /// Merge NetworkConfig, taking non-default values from other
112    fn merge_network_config(&mut self, other: NetworkConfig) {
113        let default_network = NetworkConfig::default();
114        if other.solana_rpc_url != default_network.solana_rpc_url {
115            self.network.solana_rpc_url = other.solana_rpc_url;
116        }
117        if other.solana_ws_url != default_network.solana_ws_url {
118            self.network.solana_ws_url = other.solana_ws_url;
119        }
120        if other.default_chain_id != default_network.default_chain_id {
121            self.network.default_chain_id = other.default_chain_id;
122        }
123        // Merge RPC URLs and chains - non-empty collections replace defaults
124        if !other.rpc_urls.is_empty() {
125            self.network.rpc_urls = other.rpc_urls;
126        }
127        if !other.chains.is_empty() {
128            self.network.chains = other.chains;
129        }
130        // Merge timeouts if different from default
131        if other.timeouts.rpc_timeout_secs != default_network.timeouts.rpc_timeout_secs
132            || other.timeouts.ws_timeout_secs != default_network.timeouts.ws_timeout_secs
133            || other.timeouts.http_timeout_secs != default_network.timeouts.http_timeout_secs
134        {
135            self.network.timeouts = other.timeouts;
136        }
137    }
138
139    /// Merge ProvidersConfig, taking non-default values from other
140    fn merge_providers_config(&mut self, other: ProvidersConfig) {
141        if other.anthropic_api_key.is_some() {
142            self.providers.anthropic_api_key = other.anthropic_api_key;
143        }
144        if other.openai_api_key.is_some() {
145            self.providers.openai_api_key = other.openai_api_key;
146        }
147        if other.alchemy_api_key.is_some() {
148            self.providers.alchemy_api_key = other.alchemy_api_key;
149        }
150        if other.lifi_api_key.is_some() {
151            self.providers.lifi_api_key = other.lifi_api_key;
152        }
153        if other.twitter_bearer_token.is_some() {
154            self.providers.twitter_bearer_token = other.twitter_bearer_token;
155        }
156        if other.dexscreener_api_key.is_some() {
157            self.providers.dexscreener_api_key = other.dexscreener_api_key;
158        }
159    }
160
161    /// Merge FeaturesConfig, taking non-default values from other
162    fn merge_features_config(&mut self, other: FeaturesConfig) {
163        let default_features = FeaturesConfig::default();
164        if other.enable_trading != default_features.enable_trading {
165            self.features.enable_trading = other.enable_trading;
166        }
167        if other.enable_graph_memory != default_features.enable_graph_memory {
168            self.features.enable_graph_memory = other.enable_graph_memory;
169        }
170        if other.enable_bridging != default_features.enable_bridging {
171            self.features.enable_bridging = other.enable_bridging;
172        }
173        if other.enable_social_monitoring != default_features.enable_social_monitoring {
174            self.features.enable_social_monitoring = other.enable_social_monitoring;
175        }
176        if other.enable_streaming != default_features.enable_streaming {
177            self.features.enable_streaming = other.enable_streaming;
178        }
179        if other.enable_webhooks != default_features.enable_webhooks {
180            self.features.enable_webhooks = other.enable_webhooks;
181        }
182        if other.enable_analytics != default_features.enable_analytics {
183            self.features.enable_analytics = other.enable_analytics;
184        }
185        if other.debug_mode != default_features.debug_mode {
186            self.features.debug_mode = other.debug_mode;
187        }
188        if other.experimental != default_features.experimental {
189            self.features.experimental = other.experimental;
190        }
191        // Merge custom feature flags
192        if !other.custom.is_empty() {
193            self.features.custom.extend(other.custom);
194        }
195    }
196
197    // ==== Bulk Setters ====
198
199    /// Set application configuration
200    #[must_use]
201    pub fn app(mut self, config: AppConfig) -> Self {
202        self.app = config;
203        self
204    }
205
206    /// Set database configuration
207    #[must_use]
208    pub fn database(mut self, config: DatabaseConfig) -> Self {
209        self.database = config;
210        self
211    }
212
213    /// Set network configuration
214    #[must_use]
215    pub fn network(mut self, config: NetworkConfig) -> Self {
216        self.network = config;
217        self
218    }
219
220    /// Set providers configuration
221    #[must_use]
222    pub fn providers(mut self, config: ProvidersConfig) -> Self {
223        self.providers = config;
224        self
225    }
226
227    /// Set features configuration
228    #[must_use]
229    pub fn features(mut self, config: FeaturesConfig) -> Self {
230        self.features = config;
231        self
232    }
233
234    // ==== AppConfig Individual Setters ====
235
236    /// Set the server port
237    ///
238    /// # Example
239    /// ```rust
240    /// use riglr_config::ConfigBuilder;
241    ///
242    /// let config = ConfigBuilder::new()
243    ///     .port(8080)
244    ///     .build()
245    ///     .unwrap();
246    /// assert_eq!(config.app.port, 8080);
247    /// ```
248    #[must_use]
249    pub fn port(mut self, port: u16) -> Self {
250        self.app.port = port;
251        self
252    }
253
254    /// Set the log level
255    ///
256    /// # Example
257    /// ```rust
258    /// use riglr_config::{ConfigBuilder, LogLevel};
259    ///
260    /// let config = ConfigBuilder::new()
261    ///     .log_level(LogLevel::Warn)
262    ///     .build()
263    ///     .unwrap();
264    /// assert_eq!(config.app.log_level, LogLevel::Warn);
265    /// ```
266    #[must_use]
267    pub fn log_level(mut self, level: LogLevel) -> Self {
268        self.app.log_level = level;
269        self
270    }
271
272    /// Set whether to use testnet
273    ///
274    /// # Example
275    /// ```rust
276    /// use riglr_config::ConfigBuilder;
277    ///
278    /// let config = ConfigBuilder::new()
279    ///     .use_testnet(true)
280    ///     .build()
281    ///     .unwrap();
282    /// assert!(config.app.use_testnet);
283    /// ```
284    #[must_use]
285    pub fn use_testnet(mut self, use_testnet: bool) -> Self {
286        self.app.use_testnet = use_testnet;
287        self
288    }
289
290    // ==== DatabaseConfig Individual Setters ====
291
292    /// Set the Redis URL
293    ///
294    /// # Example
295    /// ```rust
296    /// use riglr_config::ConfigBuilder;
297    ///
298    /// let config = ConfigBuilder::new()
299    ///     .redis_url("redis://localhost:6379".to_string())
300    ///     .build()
301    ///     .unwrap();
302    /// assert_eq!(config.database.redis_url, "redis://localhost:6379");
303    /// ```
304    #[must_use]
305    pub fn redis_url(mut self, url: String) -> Self {
306        self.database.redis_url = url;
307        self
308    }
309
310    /// Set the Neo4j URL (optional)
311    ///
312    /// # Example
313    /// ```rust
314    /// use riglr_config::ConfigBuilder;
315    ///
316    /// let config = ConfigBuilder::new()
317    ///     .neo4j_url(Some("bolt://localhost:7687".to_string()))
318    ///     .build()
319    ///     .unwrap();
320    /// assert_eq!(config.database.neo4j_url, Some("bolt://localhost:7687".to_string()));
321    /// ```
322    #[must_use]
323    pub fn neo4j_url(mut self, url: Option<String>) -> Self {
324        self.database.neo4j_url = url;
325        self
326    }
327
328    // ==== NetworkConfig Individual Setters ====
329
330    /// Set the Solana RPC URL
331    ///
332    /// # Example
333    /// ```rust
334    /// use riglr_config::ConfigBuilder;
335    ///
336    /// let config = ConfigBuilder::new()
337    ///     .solana_rpc_url("https://api.mainnet-beta.solana.com".to_string())
338    ///     .build()
339    ///     .unwrap();
340    /// assert_eq!(config.network.solana_rpc_url, "https://api.mainnet-beta.solana.com");
341    /// ```
342    #[must_use]
343    pub fn solana_rpc_url(mut self, url: String) -> Self {
344        self.network.solana_rpc_url = url;
345        self
346    }
347
348    /// Set the default chain ID
349    ///
350    /// # Example
351    /// ```rust
352    /// use riglr_config::ConfigBuilder;
353    ///
354    /// let config = ConfigBuilder::new()
355    ///     .default_chain_id(137) // Polygon mainnet
356    ///     .build()
357    ///     .unwrap();
358    /// assert_eq!(config.network.default_chain_id, 137);
359    /// ```
360    #[must_use]
361    pub fn default_chain_id(mut self, id: u64) -> Self {
362        self.network.default_chain_id = id;
363        self
364    }
365
366    // ==== ProvidersConfig Individual Setters ====
367
368    /// Set the Anthropic API key
369    ///
370    /// # Example
371    /// ```rust
372    /// use riglr_config::ConfigBuilder;
373    ///
374    /// let config = ConfigBuilder::new()
375    ///     .anthropic_api_key(Some("sk-ant-...".to_string()))
376    ///     .build()
377    ///     .unwrap();
378    /// assert!(config.providers.anthropic_api_key.is_some());
379    /// ```
380    #[must_use]
381    pub fn anthropic_api_key(mut self, key: Option<String>) -> Self {
382        self.providers.anthropic_api_key = key;
383        self
384    }
385
386    /// Set the OpenAI API key
387    ///
388    /// # Example
389    /// ```rust
390    /// use riglr_config::ConfigBuilder;
391    ///
392    /// let config = ConfigBuilder::new()
393    ///     .openai_api_key(Some("sk-...".to_string()))
394    ///     .build()
395    ///     .unwrap();
396    /// assert!(config.providers.openai_api_key.is_some());
397    /// ```
398    #[must_use]
399    pub fn openai_api_key(mut self, key: Option<String>) -> Self {
400        self.providers.openai_api_key = key;
401        self
402    }
403
404    // ==== Convenience Environment Setters ====
405
406    /// Configure for development environment
407    ///
408    /// Sets up common development defaults:
409    /// - Environment::Development
410    /// - LogLevel::Debug
411    /// - use_testnet = true
412    ///
413    /// # Example
414    /// ```rust
415    /// use riglr_config::{ConfigBuilder, Environment, LogLevel};
416    ///
417    /// let config = ConfigBuilder::new()
418    ///     .development()
419    ///     .build()
420    ///     .unwrap();
421    /// assert_eq!(config.app.environment, Environment::Development);
422    /// assert_eq!(config.app.log_level, LogLevel::Debug);
423    /// assert!(config.app.use_testnet);
424    /// ```
425    #[must_use]
426    pub fn development(mut self) -> Self {
427        use crate::{Environment, LogLevel};
428        self.app.environment = Environment::Development;
429        self.app.log_level = LogLevel::Debug;
430        self.app.use_testnet = true;
431        self
432    }
433
434    /// Configure for staging environment
435    ///
436    /// Sets up common staging defaults:
437    /// - Environment::Staging
438    /// - LogLevel::Info
439    /// - use_testnet = true
440    ///
441    /// # Example
442    /// ```rust
443    /// use riglr_config::{ConfigBuilder, Environment, LogLevel};
444    ///
445    /// let config = ConfigBuilder::new()
446    ///     .staging()
447    ///     .build()
448    ///     .unwrap();
449    /// assert_eq!(config.app.environment, Environment::Staging);
450    /// assert_eq!(config.app.log_level, LogLevel::Info);
451    /// assert!(config.app.use_testnet);
452    /// ```
453    #[must_use]
454    pub fn staging(mut self) -> Self {
455        use crate::{Environment, LogLevel};
456        self.app.environment = Environment::Staging;
457        self.app.log_level = LogLevel::Info;
458        self.app.use_testnet = true;
459        self
460    }
461
462    /// Configure for production environment
463    ///
464    /// Sets up common production defaults:
465    /// - Environment::Production
466    /// - LogLevel::Info
467    /// - use_testnet = false
468    ///
469    /// # Example
470    /// ```rust,no_run
471    /// use riglr_config::{ConfigBuilder, Environment, LogLevel};
472    ///
473    /// let config = ConfigBuilder::new()
474    ///     .production()
475    ///     .redis_url("redis://redis.example.com:6379".to_string())
476    ///     .build()
477    ///     .unwrap();
478    /// assert_eq!(config.app.environment, Environment::Production);
479    /// assert_eq!(config.app.log_level, LogLevel::Info);
480    /// assert!(!config.app.use_testnet);
481    /// ```
482    #[must_use]
483    pub fn production(mut self) -> Self {
484        use crate::{Environment, LogLevel};
485        self.app.environment = Environment::Production;
486        self.app.log_level = LogLevel::Info;
487        self.app.use_testnet = false;
488        self
489    }
490
491    /// Configure for testnet usage
492    ///
493    /// Sets up testnet-specific defaults:
494    /// - use_testnet = true
495    /// - solana_rpc_url = devnet RPC
496    /// - default_chain_id = 5 (Goerli)
497    ///
498    /// # Example
499    /// ```rust
500    /// use riglr_config::ConfigBuilder;
501    ///
502    /// let config = ConfigBuilder::new()
503    ///     .testnet()
504    ///     .build()
505    ///     .unwrap();
506    /// assert!(config.app.use_testnet);
507    /// assert_eq!(config.network.default_chain_id, 5); // Goerli
508    /// ```
509    #[must_use]
510    pub fn testnet(mut self) -> Self {
511        self.app.use_testnet = true;
512        self.network.solana_rpc_url = "https://api.devnet.solana.com".to_string();
513        self.network.default_chain_id = 5; // Goerli testnet
514        self
515    }
516
517    /// Configure for mainnet usage
518    ///
519    /// Sets up mainnet-specific defaults:
520    /// - use_testnet = false
521    /// - solana_rpc_url = mainnet-beta RPC
522    /// - default_chain_id = 1 (Ethereum Mainnet)
523    ///
524    /// # Example
525    /// ```rust
526    /// use riglr_config::ConfigBuilder;
527    ///
528    /// let config = ConfigBuilder::new()
529    ///     .mainnet()
530    ///     .build()
531    ///     .unwrap();
532    /// assert!(!config.app.use_testnet);
533    /// assert_eq!(config.network.default_chain_id, 1); // Ethereum Mainnet
534    /// ```
535    #[must_use]
536    pub fn mainnet(mut self) -> Self {
537        self.app.use_testnet = false;
538        self.network.solana_rpc_url = "https://api.mainnet-beta.solana.com".to_string();
539        self.network.default_chain_id = 1; // Ethereum mainnet
540        self
541    }
542
543    // ==== FeaturesConfig Individual Setters ====
544
545    /// Set whether trading is enabled
546    ///
547    /// # Example
548    /// ```rust
549    /// use riglr_config::ConfigBuilder;
550    ///
551    /// let config = ConfigBuilder::new()
552    ///     .enable_trading(false)
553    ///     .build()
554    ///     .unwrap();
555    /// assert!(!config.features.enable_trading);
556    /// ```
557    #[must_use]
558    pub fn enable_trading(mut self, enabled: bool) -> Self {
559        self.features.enable_trading = enabled;
560        self
561    }
562
563    /// Set whether graph memory is enabled
564    ///
565    /// # Example
566    /// ```rust
567    /// use riglr_config::ConfigBuilder;
568    ///
569    /// let config = ConfigBuilder::new()
570    ///     .neo4j_url(Some("bolt://localhost:7687".to_string())) // Required for graph memory
571    ///     .enable_graph_memory(true)
572    ///     .build()
573    ///     .unwrap();
574    /// assert!(config.features.enable_graph_memory);
575    /// ```
576    #[must_use]
577    pub fn enable_graph_memory(mut self, enabled: bool) -> Self {
578        self.features.enable_graph_memory = enabled;
579        self
580    }
581
582    // ==== Utility Methods ====
583
584    /// Load configuration from a TOML file
585    ///
586    /// This method loads configuration from a TOML file and merges it
587    /// with any existing builder settings.
588    ///
589    /// # Example
590    /// ```rust,no_run
591    /// use riglr_config::ConfigBuilder;
592    ///
593    /// let config = ConfigBuilder::new()
594    ///     .from_file("config.toml")
595    ///     .unwrap()
596    ///     .port(8080) // Can override file settings
597    ///     .build()
598    ///     .unwrap();
599    /// ```
600    pub fn from_file<P: AsRef<std::path::Path>>(mut self, path: P) -> ConfigResult<Self> {
601        let content = std::fs::read_to_string(path).map_err(|e| {
602            crate::ConfigError::validation(format!("Failed to read config file: {}", e))
603        })?;
604
605        let file_config: Config = toml::from_str(&content).map_err(|e| {
606            crate::ConfigError::validation(format!("Failed to parse TOML config: {}", e))
607        })?;
608
609        // Merge file config with current builder state (non-destructive)
610        self.merge_app_config(file_config.app);
611        self.merge_database_config(file_config.database);
612        self.merge_network_config(file_config.network);
613        self.merge_providers_config(file_config.providers);
614        self.merge_features_config(file_config.features);
615
616        Ok(self)
617    }
618
619    /// Load configuration from environment variables
620    ///
621    /// This method loads configuration from environment variables
622    /// and merges it with any existing builder settings.
623    ///
624    /// # Example
625    /// ```rust
626    /// use riglr_config::ConfigBuilder;
627    ///
628    /// let config = ConfigBuilder::new()
629    ///     .development()
630    ///     .from_env()
631    ///     .unwrap()
632    ///     .build()
633    ///     .unwrap();
634    /// ```
635    pub fn from_env(mut self) -> ConfigResult<Self> {
636        // Load .env file if present
637        dotenvy::dotenv().ok();
638
639        let env_config = envy::from_env::<Config>().map_err(|e| {
640            crate::ConfigError::EnvParse(format!("Failed to parse environment: {}", e))
641        })?;
642
643        // Merge environment config with current builder state (non-destructive)
644        self.merge_app_config(env_config.app);
645        self.merge_database_config(env_config.database);
646        self.merge_network_config(env_config.network);
647        self.merge_providers_config(env_config.providers);
648        self.merge_features_config(env_config.features);
649
650        Ok(self)
651    }
652
653    /// Validate the current configuration without building
654    ///
655    /// This allows you to check if the configuration is valid
656    /// before calling build().
657    ///
658    /// # Example
659    /// ```rust
660    /// use riglr_config::ConfigBuilder;
661    ///
662    /// let builder = ConfigBuilder::new()
663    ///     .development();
664    ///
665    /// // Check if valid before building
666    /// if builder.validate().is_ok() {
667    ///     let config = builder.build().unwrap();
668    /// }
669    /// ```
670    pub fn validate(&self) -> ConfigResult<()> {
671        let config = Config {
672            app: self.app.clone(),
673            database: self.database.clone(),
674            network: self.network.clone(),
675            providers: self.providers.clone(),
676            features: self.features.clone(),
677        };
678        config.validate_config()
679    }
680
681    /// Build the configuration
682    ///
683    /// This validates all configuration and returns an error if any validation fails.
684    pub fn build(self) -> ConfigResult<Config> {
685        let config = Config {
686            app: self.app,
687            database: self.database,
688            network: self.network,
689            providers: self.providers,
690            features: self.features,
691        };
692
693        config.validate_config()?;
694        Ok(config)
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use crate::Environment;
702
703    #[test]
704    fn test_builder_default() {
705        let config = ConfigBuilder::default().build().unwrap();
706        assert_eq!(config.app.environment, Environment::Development);
707    }
708
709    #[test]
710    fn test_builder_with_app_config() {
711        // Use development environment to avoid validation failures
712        let config = ConfigBuilder::default()
713            .app(AppConfig {
714                environment: Environment::Development,
715                log_level: LogLevel::Warn,
716                ..Default::default()
717            })
718            .build()
719            .unwrap();
720
721        assert_eq!(config.app.environment, Environment::Development);
722        assert_eq!(config.app.log_level, LogLevel::Warn);
723    }
724
725    #[test]
726    fn test_builder_chaining() {
727        let config = ConfigBuilder::default().build().unwrap();
728
729        assert_eq!(config.app.environment, Environment::Development);
730    }
731
732    #[test]
733    fn test_builder_individual_setters() {
734        let config = ConfigBuilder::default()
735            // AppConfig setters
736            .port(8080)
737            .log_level(LogLevel::Warn)
738            .use_testnet(true)
739            // DatabaseConfig setters
740            .redis_url("redis://localhost:6379".to_string())
741            .neo4j_url(Some("bolt://localhost:7687".to_string()))
742            // NetworkConfig setters
743            .solana_rpc_url("https://api.mainnet-beta.solana.com".to_string())
744            .default_chain_id(137)
745            // ProvidersConfig setters
746            .anthropic_api_key(Some("test_anthropic_key".to_string()))
747            .openai_api_key(Some("test_openai_key".to_string()))
748            // FeaturesConfig setters
749            .enable_trading(false)
750            .enable_graph_memory(true)
751            .build()
752            .unwrap();
753
754        // Verify AppConfig
755        assert_eq!(config.app.port, 8080);
756        assert_eq!(config.app.log_level, LogLevel::Warn);
757        assert!(config.app.use_testnet);
758
759        // Verify DatabaseConfig
760        assert_eq!(config.database.redis_url, "redis://localhost:6379");
761        assert_eq!(
762            config.database.neo4j_url,
763            Some("bolt://localhost:7687".to_string())
764        );
765
766        // Verify NetworkConfig
767        assert_eq!(
768            config.network.solana_rpc_url,
769            "https://api.mainnet-beta.solana.com"
770        );
771        assert_eq!(config.network.default_chain_id, 137);
772
773        // Verify ProvidersConfig
774        assert_eq!(
775            config.providers.anthropic_api_key,
776            Some("test_anthropic_key".to_string())
777        );
778        assert_eq!(
779            config.providers.openai_api_key,
780            Some("test_openai_key".to_string())
781        );
782
783        // Verify FeaturesConfig
784        assert!(!config.features.enable_trading);
785        assert!(config.features.enable_graph_memory);
786    }
787
788    #[test]
789    fn test_individual_setters_override_defaults() {
790        // Test that individual setters properly override default values
791        let config = ConfigBuilder::default().port(9000).build().unwrap();
792
793        assert_eq!(config.app.port, 9000);
794        // Other fields should retain default values
795        assert_eq!(config.app.environment, Environment::Development);
796    }
797
798    #[test]
799    fn test_mixed_bulk_and_individual_setters() {
800        // Test that individual setters can be mixed with bulk setters
801        let mut app_config = AppConfig::default();
802        app_config.environment = Environment::Development; // Use development to avoid validation issues
803
804        let config = ConfigBuilder::default()
805            .app(app_config)
806            .port(3333) // This should override the port from the bulk setter
807            .redis_url("redis://custom:6379".to_string())
808            .build()
809            .unwrap();
810
811        assert_eq!(config.app.port, 3333);
812        assert_eq!(config.app.environment, Environment::Development);
813        assert_eq!(config.database.redis_url, "redis://custom:6379");
814    }
815
816    #[test]
817    fn test_config_builder_development_preset() {
818        let config = ConfigBuilder::default().development().build().unwrap();
819
820        assert_eq!(config.app.environment, Environment::Development);
821        assert_eq!(config.app.log_level, LogLevel::Debug);
822        assert!(config.app.use_testnet);
823    }
824
825    #[test]
826    fn test_config_builder_staging_preset() {
827        let config = ConfigBuilder::default().staging().build().unwrap();
828
829        assert_eq!(config.app.environment, Environment::Staging);
830        assert_eq!(config.app.log_level, LogLevel::Info);
831        assert!(config.app.use_testnet);
832    }
833
834    #[test]
835    fn test_config_builder_production_preset() {
836        // Use a proper Redis URL for production and disable trading to avoid Alchemy key requirement
837        let config = ConfigBuilder::default()
838            .production()
839            .redis_url("redis://production-redis:6379".to_string())
840            .enable_trading(false) // Disable trading to avoid Alchemy key requirement
841            .build()
842            .unwrap();
843
844        assert_eq!(config.app.environment, Environment::Production);
845        assert_eq!(config.app.log_level, LogLevel::Info);
846        assert!(!config.app.use_testnet);
847    }
848
849    #[test]
850    fn test_config_builder_testnet_preset() {
851        let config = ConfigBuilder::default().testnet().build().unwrap();
852
853        assert!(config.app.use_testnet);
854        assert_eq!(config.network.default_chain_id, 5); // Goerli
855        assert_eq!(
856            config.network.solana_rpc_url,
857            "https://api.devnet.solana.com"
858        );
859    }
860
861    #[test]
862    fn test_config_builder_mainnet_preset() {
863        let config = ConfigBuilder::default().mainnet().build().unwrap();
864
865        assert!(!config.app.use_testnet);
866        assert_eq!(config.network.default_chain_id, 1); // Ethereum mainnet
867        assert_eq!(
868            config.network.solana_rpc_url,
869            "https://api.mainnet-beta.solana.com"
870        );
871    }
872
873    #[test]
874    fn test_config_builder_validate_without_building() {
875        let builder = ConfigBuilder::default().development();
876
877        // Should be able to validate without building
878        assert!(builder.validate().is_ok());
879
880        // Should still be able to build after validation
881        let config = builder.build().unwrap();
882        assert_eq!(config.app.environment, Environment::Development);
883    }
884
885    #[test]
886    fn test_config_builder_preset_chaining() {
887        let config = ConfigBuilder::default()
888            .development() // Set development defaults
889            .port(9000) // Override specific values
890            .build()
891            .unwrap();
892
893        assert_eq!(config.app.environment, Environment::Development);
894        assert_eq!(config.app.log_level, LogLevel::Debug);
895        assert!(config.app.use_testnet);
896        assert_eq!(config.app.port, 9000); // Should override the default
897    }
898
899    #[test]
900    fn test_config_builder_chaining_with_overrides() {
901        let config = ConfigBuilder::default()
902            .development() // Set development defaults
903            .production() // Override with production
904            .port(9000) // Override specific values
905            .redis_url("redis://prod:6379".to_string()) // Avoid localhost validation error
906            .enable_trading(false) // Disable trading to avoid Alchemy key requirement
907            .build()
908            .unwrap();
909
910        assert_eq!(config.app.environment, Environment::Production);
911        assert_eq!(config.app.log_level, LogLevel::Info);
912        assert!(!config.app.use_testnet); // Production overrode development
913        assert_eq!(config.app.port, 9000); // Should override the default
914    }
915
916    #[test]
917    fn test_config_builder_comprehensive_layering() {
918        use std::io::Write;
919        use tempfile::NamedTempFile;
920
921        // Create a temporary TOML config file with required fields
922        let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
923        let config_content = r#"
924# App config
925port = 8081
926environment = "development"
927log_level = "info"
928use_testnet = true
929
930# Database config  
931redis_url = "redis://file-redis:6379"
932
933# Network config
934solana_rpc_url = "https://file-solana-rpc.com"
935default_chain_id = 1
936
937# Features config
938enable_trading = false
939enable_graph_memory = false
940enable_bridging = false
941enable_social_monitoring = false
942enable_streaming = false
943enable_webhooks = false
944enable_analytics = false
945debug_mode = false
946experimental = false
947"#;
948        temp_file
949            .write_all(config_content.as_bytes())
950            .expect("Failed to write to temp file");
951
952        let temp_path = temp_file.path();
953
954        // Test the layering without environment variables first to verify basic file loading works
955        let file_only_config = ConfigBuilder::default()
956            .from_file(temp_path)
957            .expect("Failed to load from file")
958            .build()
959            .unwrap();
960
961        // Verify file values are loaded
962        assert_eq!(
963            file_only_config.app.port, 8081,
964            "Port should come from file"
965        );
966        assert_eq!(
967            file_only_config.database.redis_url, "redis://file-redis:6379",
968            "Redis URL should come from file"
969        );
970
971        // Now test with programmatic overrides
972        let config = ConfigBuilder::default()
973            .from_file(temp_path)
974            .expect("Failed to load from file")
975            .port(8083) // Programmatic setter should override all: port -> 8083
976            .redis_url("redis://programmatic:6379".to_string()) // Override file value
977            .anthropic_api_key(Some("programmatic-key".to_string())) // Set a new value not in file
978            .build()
979            .unwrap();
980
981        // Verify the layering:
982        // port: Default <- File (8081) <- Programmatic (8083) = 8083
983        assert_eq!(
984            config.app.port, 8083,
985            "Port should be set by programmatic setter"
986        );
987
988        // redis_url: Default <- File (file-redis) <- Programmatic (programmatic) = programmatic
989        assert_eq!(
990            config.database.redis_url, "redis://programmatic:6379",
991            "Redis URL should be set by programmatic setter"
992        );
993
994        // solana_rpc_url: Default <- File (file-solana-rpc) = file-solana-rpc (no override)
995        assert_eq!(
996            config.network.solana_rpc_url, "https://file-solana-rpc.com",
997            "Solana RPC URL should be set by file"
998        );
999
1000        // anthropic_api_key: Default (None) <- Programmatic (Some("key")) = Some("key")
1001        assert_eq!(
1002            config.providers.anthropic_api_key,
1003            Some("programmatic-key".to_string()),
1004            "Anthropic API key should be set by programmatic setter"
1005        );
1006
1007        // enable_trading: Default (true) <- File (false) = false
1008        assert!(
1009            !config.features.enable_trading,
1010            "Trading should be disabled by file config"
1011        );
1012    }
1013
1014    #[test]
1015    fn test_config_builder_merge_behavior() {
1016        use std::io::Write;
1017        use tempfile::NamedTempFile;
1018
1019        // Test that merge() preserves non-default values correctly
1020
1021        // Create a temporary TOML config file with some settings
1022        let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
1023        let config_content = r#"
1024port = 9001
1025redis_url = "redis://merge-test:6379"
1026solana_rpc_url = "https://file-rpc.com"
1027"#;
1028        temp_file
1029            .write_all(config_content.as_bytes())
1030            .expect("Failed to write to temp file");
1031
1032        let temp_path = temp_file.path();
1033
1034        // Build a base config with some values
1035        let base_config = ConfigBuilder::default()
1036            .port(7000) // Should be overridden by file
1037            .anthropic_api_key(Some("base-key".to_string())) // Should remain
1038            .from_file(temp_path)
1039            .expect("Failed to load from file")
1040            .build()
1041            .unwrap();
1042
1043        // Verify that file values override base values
1044        assert_eq!(base_config.app.port, 9001, "Port should come from file");
1045        assert_eq!(
1046            base_config.database.redis_url, "redis://merge-test:6379",
1047            "Redis URL should come from file"
1048        );
1049        assert_eq!(
1050            base_config.network.solana_rpc_url, "https://file-rpc.com",
1051            "Solana RPC should come from file"
1052        );
1053        assert_eq!(
1054            base_config.providers.anthropic_api_key,
1055            Some("base-key".to_string()),
1056            "Anthropic key should remain from base builder"
1057        );
1058    }
1059
1060    #[test]
1061    fn test_config_builder_layering_with_none_values() {
1062        use std::io::Write;
1063        use tempfile::NamedTempFile;
1064
1065        // Test that None/empty values don't override non-empty values inappropriately
1066
1067        // Create a TOML file with required fields and an optional field set
1068        let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
1069        let config_content = r#"
1070redis_url = "redis://required:6379"
1071solana_rpc_url = "https://api.devnet.solana.com"
1072neo4j_url = "bolt://file-neo4j:7687"
1073"#;
1074        temp_file
1075            .write_all(config_content.as_bytes())
1076            .expect("Failed to write to temp file");
1077
1078        let temp_path = temp_file.path();
1079
1080        // First test: load from file only to verify file parsing works
1081        let file_only_config = ConfigBuilder::default()
1082            .from_file(temp_path)
1083            .expect("Failed to load from file")
1084            .build()
1085            .unwrap();
1086
1087        assert_eq!(
1088            file_only_config.database.neo4j_url,
1089            Some("bolt://file-neo4j:7687".to_string()),
1090            "Neo4j URL should come from file when loaded directly"
1091        );
1092
1093        // Second test: verify layering behavior with programmatic settings
1094        let config_with_override = ConfigBuilder::default()
1095            .redis_url("redis://base:6379".to_string()) // Base value
1096            .from_file(temp_path)
1097            .expect("Failed to load from file")
1098            .redis_url("redis://override:6379".to_string()) // Override after file
1099            .build()
1100            .unwrap();
1101
1102        // Verify programmatic override takes precedence
1103        assert_eq!(
1104            config_with_override.database.redis_url, "redis://override:6379",
1105            "Redis URL should be overridden by programmatic setter"
1106        );
1107
1108        // Verify file value is preserved for other fields
1109        assert_eq!(
1110            config_with_override.database.neo4j_url,
1111            Some("bolt://file-neo4j:7687".to_string()),
1112            "Neo4j URL should still come from file"
1113        );
1114    }
1115}