Skip to main content

mockforge_core/config/
mod.rs

1//! Configuration management for MockForge
2
3mod auth;
4mod contracts;
5mod operational;
6mod protocol;
7mod routes;
8
9pub use auth::*;
10pub use contracts::*;
11pub use operational::*;
12pub use protocol::*;
13pub use routes::*;
14
15use crate::{Config as CoreConfig, Error, RealityLevel, Result};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::path::Path;
19use tokio::fs;
20
21/// Reality slider configuration for YAML config files
22///
23/// This is a simplified configuration that stores just the level.
24/// The full RealityConfig with all subsystem settings is generated
25/// automatically from the level via the RealityEngine.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(default)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29pub struct RealitySliderConfig {
30    /// Reality level (1-5)
31    pub level: RealityLevel,
32    /// Whether to enable reality slider (if false, uses individual subsystem configs)
33    pub enabled: bool,
34}
35
36impl Default for RealitySliderConfig {
37    fn default() -> Self {
38        Self {
39            level: RealityLevel::ModerateRealism,
40            enabled: true,
41        }
42    }
43}
44
45/// Server configuration
46#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[serde(default)]
49pub struct ServerConfig {
50    /// HTTP server configuration
51    pub http: HttpConfig,
52    /// WebSocket server configuration
53    pub websocket: WebSocketConfig,
54    /// GraphQL server configuration
55    pub graphql: GraphQLConfig,
56    /// gRPC server configuration
57    pub grpc: GrpcConfig,
58    /// MQTT server configuration
59    pub mqtt: MqttConfig,
60    /// SMTP server configuration
61    pub smtp: SmtpConfig,
62    /// FTP server configuration
63    pub ftp: FtpConfig,
64    /// Kafka server configuration
65    pub kafka: KafkaConfig,
66    /// AMQP server configuration
67    pub amqp: AmqpConfig,
68    /// TCP server configuration
69    pub tcp: TcpConfig,
70    /// Admin UI configuration
71    pub admin: AdminConfig,
72    /// Request chaining configuration
73    pub chaining: ChainingConfig,
74    /// Core MockForge configuration
75    pub core: CoreConfig,
76    /// Logging configuration
77    pub logging: LoggingConfig,
78    /// Data generation configuration
79    pub data: DataConfig,
80    /// MockAI (Behavioral Mock Intelligence) configuration
81    #[serde(default)]
82    pub mockai: MockAIConfig,
83    /// Observability configuration (metrics, tracing)
84    pub observability: ObservabilityConfig,
85    /// Multi-tenant workspace configuration
86    pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
87    /// Custom routes configuration
88    #[serde(default)]
89    pub routes: Vec<RouteConfig>,
90    /// Protocol enable/disable configuration
91    #[serde(default)]
92    pub protocols: ProtocolsConfig,
93    /// Named configuration profiles (dev, ci, demo, etc.)
94    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
95    pub profiles: HashMap<String, ProfileConfig>,
96    /// Deceptive deploy configuration for production-like mock APIs
97    #[serde(default)]
98    pub deceptive_deploy: DeceptiveDeployConfig,
99    /// Behavioral cloning configuration
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub behavioral_cloning: Option<BehavioralCloningConfig>,
102    /// Reality slider configuration for unified realism control
103    #[serde(default)]
104    pub reality: RealitySliderConfig,
105    /// Reality Continuum configuration for blending mock and real data sources
106    #[serde(default)]
107    pub reality_continuum: crate::reality_continuum::ContinuumConfig,
108    /// Security monitoring and SIEM configuration
109    #[serde(default)]
110    pub security: SecurityConfig,
111    /// Drift budget and contract monitoring configuration
112    #[serde(default)]
113    pub drift_budget: crate::contract_drift::DriftBudgetConfig,
114    /// Incident management configuration
115    #[serde(default)]
116    pub incidents: IncidentConfig,
117    /// PR generation configuration
118    #[serde(default)]
119    pub pr_generation: crate::pr_generation::PRGenerationConfig,
120    /// Consumer contracts configuration
121    #[serde(default)]
122    pub consumer_contracts: ConsumerContractsConfig,
123    /// Contracts configuration (fitness rules, etc.)
124    #[serde(default)]
125    pub contracts: ContractsConfig,
126    /// Behavioral Economics Engine configuration
127    #[serde(default)]
128    pub behavioral_economics: BehavioralEconomicsConfig,
129    /// Drift Learning configuration
130    #[serde(default)]
131    pub drift_learning: DriftLearningConfig,
132    /// Organization AI controls configuration (YAML defaults, DB overrides)
133    #[serde(default)]
134    pub org_ai_controls: crate::ai_studio::org_controls::OrgAiControlsConfig,
135    /// Performance and resource configuration
136    #[serde(default)]
137    pub performance: PerformanceConfig,
138    /// Plugin resource limits configuration
139    #[serde(default)]
140    pub plugins: PluginResourceConfig,
141    /// Configuration hot-reload settings
142    #[serde(default)]
143    pub hot_reload: ConfigHotReloadConfig,
144    /// Secret backend configuration
145    #[serde(default)]
146    pub secrets: SecretBackendConfig,
147}
148
149/// Profile configuration - a partial ServerConfig that overrides base settings
150#[derive(Debug, Clone, Serialize, Deserialize, Default)]
151#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[serde(default)]
153pub struct ProfileConfig {
154    /// HTTP server configuration overrides
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub http: Option<HttpConfig>,
157    /// WebSocket server configuration overrides
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub websocket: Option<WebSocketConfig>,
160    /// GraphQL server configuration overrides
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub graphql: Option<GraphQLConfig>,
163    /// gRPC server configuration overrides
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub grpc: Option<GrpcConfig>,
166    /// MQTT server configuration overrides
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub mqtt: Option<MqttConfig>,
169    /// SMTP server configuration overrides
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub smtp: Option<SmtpConfig>,
172    /// FTP server configuration overrides
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub ftp: Option<FtpConfig>,
175    /// Kafka server configuration overrides
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub kafka: Option<KafkaConfig>,
178    /// AMQP server configuration overrides
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub amqp: Option<AmqpConfig>,
181    /// TCP server configuration overrides
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub tcp: Option<TcpConfig>,
184    /// Admin UI configuration overrides
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub admin: Option<AdminConfig>,
187    /// Request chaining configuration overrides
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub chaining: Option<ChainingConfig>,
190    /// Core MockForge configuration overrides
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub core: Option<CoreConfig>,
193    /// Logging configuration overrides
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub logging: Option<LoggingConfig>,
196    /// Data generation configuration overrides
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub data: Option<DataConfig>,
199    /// MockAI configuration overrides
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub mockai: Option<MockAIConfig>,
202    /// Observability configuration overrides
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub observability: Option<ObservabilityConfig>,
205    /// Multi-tenant workspace configuration overrides
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
208    /// Custom routes configuration overrides
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub routes: Option<Vec<RouteConfig>>,
211    /// Protocol enable/disable configuration overrides
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub protocols: Option<ProtocolsConfig>,
214    /// Deceptive deploy configuration overrides
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub deceptive_deploy: Option<DeceptiveDeployConfig>,
217    /// Reality slider configuration overrides
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub reality: Option<RealitySliderConfig>,
220    /// Reality Continuum configuration overrides
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
223    /// Security configuration overrides
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub security: Option<SecurityConfig>,
226}
227
228impl ServerConfig {
229    /// Create a minimal configuration with all defaults.
230    pub fn minimal() -> Self {
231        Self::default()
232    }
233
234    /// Create a development-friendly configuration with admin UI enabled and
235    /// debug-level logging.
236    pub fn development() -> Self {
237        let mut cfg = Self::default();
238        cfg.admin.enabled = true;
239        cfg.logging.level = "debug".to_string();
240        cfg
241    }
242
243    /// Create a CI-oriented configuration with latency and failure injection
244    /// disabled for deterministic test runs.
245    pub fn ci() -> Self {
246        let mut cfg = Self::default();
247        cfg.core.latency_enabled = false;
248        cfg.core.failures_enabled = false;
249        cfg
250    }
251
252    /// Builder: set the HTTP port.
253    #[must_use]
254    pub fn with_http_port(mut self, port: u16) -> Self {
255        self.http.port = port;
256        self
257    }
258
259    /// Builder: enable the admin UI on the given port.
260    #[must_use]
261    pub fn with_admin(mut self, port: u16) -> Self {
262        self.admin.enabled = true;
263        self.admin.port = port;
264        self
265    }
266
267    /// Builder: enable gRPC on the given port.
268    #[must_use]
269    pub fn with_grpc(mut self, port: u16) -> Self {
270        self.grpc.enabled = true;
271        self.grpc.port = port;
272        self.protocols.grpc.enabled = true;
273        self
274    }
275
276    /// Builder: enable WebSocket on the given port.
277    #[must_use]
278    pub fn with_websocket(mut self, port: u16) -> Self {
279        self.websocket.enabled = true;
280        self.websocket.port = port;
281        self.protocols.websocket.enabled = true;
282        self
283    }
284
285    /// Builder: set the log level.
286    #[must_use]
287    pub fn with_log_level(mut self, level: &str) -> Self {
288        self.logging.level = level.to_string();
289        self
290    }
291
292    /// Check whether any advanced features (MockAI, behavioral cloning,
293    /// reality continuum) are enabled.
294    pub fn has_advanced_features(&self) -> bool {
295        self.mockai.enabled
296            || self.behavioral_cloning.as_ref().is_some_and(|bc| bc.enabled)
297            || self.reality_continuum.enabled
298    }
299
300    /// Check whether any enterprise features (multi-tenant, federation,
301    /// security monitoring) are enabled.
302    pub fn has_enterprise_features(&self) -> bool {
303        self.multi_tenant.enabled || self.security.monitoring.siem.enabled
304    }
305}
306
307/// Load configuration from file
308pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
309    let content = fs::read_to_string(&path)
310        .await
311        .map_err(|e| Error::io_with_context("reading config file", e.to_string()))?;
312
313    // Parse config with improved error messages
314    let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
315        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
316    {
317        serde_yaml::from_str(&content).map_err(|e| {
318            // Improve error message with field path context
319            let error_msg = e.to_string();
320            let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
321
322            // Add helpful context for common errors
323            if error_msg.contains("missing field") {
324                full_msg.push_str(
325                    "\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
326                );
327                full_msg.push_str(
328                    "\n   Omit fields you don't need - MockForge will use sensible defaults.",
329                );
330                full_msg.push_str("\n   See config.template.yaml for all available options.");
331            } else if error_msg.contains("unknown field") {
332                full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
333                full_msg.push_str("\n   See config.template.yaml for valid field names.");
334            }
335
336            Error::config(full_msg)
337        })?
338    } else {
339        serde_json::from_str(&content).map_err(|e| {
340            // Improve error message with field path context
341            let error_msg = e.to_string();
342            let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
343
344            // Add helpful context for common errors
345            if error_msg.contains("missing field") {
346                full_msg.push_str(
347                    "\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
348                );
349                full_msg.push_str(
350                    "\n   Omit fields you don't need - MockForge will use sensible defaults.",
351                );
352                full_msg.push_str("\n   See config.template.yaml for all available options.");
353            } else if error_msg.contains("unknown field") {
354                full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
355                full_msg.push_str("\n   See config.template.yaml for valid field names.");
356            }
357
358            Error::config(full_msg)
359        })?
360    };
361
362    Ok(config)
363}
364
365/// Save configuration to file
366pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
367    let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
368        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
369    {
370        serde_yaml::to_string(config)
371            .map_err(|e| Error::config(format!("Failed to serialize config to YAML: {}", e)))?
372    } else {
373        serde_json::to_string_pretty(config)
374            .map_err(|e| Error::config(format!("Failed to serialize config to JSON: {}", e)))?
375    };
376
377    fs::write(path, content)
378        .await
379        .map_err(|e| Error::io_with_context("writing config file", e.to_string()))?;
380
381    Ok(())
382}
383
384/// Load configuration with fallback to default
385pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
386    match load_config(&path).await {
387        Ok(config) => {
388            tracing::info!("Loaded configuration from {:?}", path.as_ref());
389            config
390        }
391        Err(e) => {
392            tracing::warn!(
393                "Failed to load config from {:?}: {}. Using defaults.",
394                path.as_ref(),
395                e
396            );
397            ServerConfig::default()
398        }
399    }
400}
401
402/// Create default configuration file
403pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
404    let config = ServerConfig::default();
405    save_config(path, &config).await?;
406    Ok(())
407}
408
409/// Environment variable overrides for configuration
410pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
411    // HTTP server overrides
412    if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
413        if let Ok(port_num) = port.parse() {
414            config.http.port = port_num;
415        }
416    }
417
418    if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
419        config.http.host = host;
420    }
421
422    // WebSocket server overrides
423    if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
424        if let Ok(port_num) = port.parse() {
425            config.websocket.port = port_num;
426        }
427    }
428
429    if let Ok(host) = std::env::var("MOCKFORGE_WS_HOST") {
430        config.websocket.host = host;
431    }
432
433    if let Ok(replay) = std::env::var("MOCKFORGE_WS_REPLAY_FILE") {
434        config.websocket.replay_file = Some(replay);
435    }
436
437    // gRPC server overrides
438    if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
439        if let Ok(port_num) = port.parse() {
440            config.grpc.port = port_num;
441        }
442    }
443
444    if let Ok(enabled) = std::env::var("MOCKFORGE_GRPC_ENABLED") {
445        config.grpc.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
446    }
447
448    // MQTT broker overrides
449    if let Ok(port) = std::env::var("MOCKFORGE_MQTT_PORT") {
450        if let Ok(port_num) = port.parse() {
451            config.mqtt.port = port_num;
452        }
453    }
454
455    if let Ok(host) = std::env::var("MOCKFORGE_MQTT_HOST") {
456        config.mqtt.host = host;
457    }
458
459    if let Ok(enabled) = std::env::var("MOCKFORGE_MQTT_ENABLED") {
460        config.mqtt.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
461    }
462
463    // Kafka broker overrides
464    if let Ok(port) = std::env::var("MOCKFORGE_KAFKA_PORT") {
465        if let Ok(port_num) = port.parse() {
466            config.kafka.port = port_num;
467        }
468    }
469
470    if let Ok(host) = std::env::var("MOCKFORGE_KAFKA_HOST") {
471        config.kafka.host = host;
472    }
473
474    if let Ok(enabled) = std::env::var("MOCKFORGE_KAFKA_ENABLED") {
475        config.kafka.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
476    }
477
478    if let Ok(dir) = std::env::var("MOCKFORGE_KAFKA_FIXTURES_DIR") {
479        config.kafka.fixtures_dir = Some(std::path::PathBuf::from(dir));
480    }
481
482    // Kafka advertised-listener overrides. Required when the broker is
483    // reachable on a different host/port from clients than where it binds —
484    // i.e. every hosted-mock deployment. The orchestrator templates the
485    // public `<app>.fly.dev` hostname into MOCKFORGE_KAFKA_ADVERTISED_HOST.
486    if let Ok(host) = std::env::var("MOCKFORGE_KAFKA_ADVERTISED_HOST") {
487        if !host.trim().is_empty() {
488            config.kafka.advertised_host = Some(host);
489        }
490    }
491
492    if let Ok(port) = std::env::var("MOCKFORGE_KAFKA_ADVERTISED_PORT") {
493        if let Ok(port_num) = port.parse() {
494            config.kafka.advertised_port = Some(port_num);
495        }
496    }
497
498    // OpenTelemetry / OTLP overrides. When `MOCKFORGE_OTLP_ENDPOINT` is set,
499    // turn on the observability tracing config and route exports to the
500    // given URL. Hosted-mock deployments get this set automatically by the
501    // orchestrator (#233) so spans flow back to MockForge Cloud.
502    if let Ok(endpoint) = std::env::var("MOCKFORGE_OTLP_ENDPOINT") {
503        if !endpoint.trim().is_empty() {
504            let otel = config
505                .observability
506                .opentelemetry
507                .get_or_insert_with(OpenTelemetryConfig::default);
508            otel.enabled = true;
509            otel.otlp_endpoint = Some(endpoint);
510        }
511    }
512
513    if let Ok(rate) = std::env::var("MOCKFORGE_OTLP_SAMPLING_RATE") {
514        if let Ok(parsed) = rate.parse::<f64>() {
515            if let Some(otel) = config.observability.opentelemetry.as_mut() {
516                otel.sampling_rate = parsed.clamp(0.0, 1.0);
517            }
518        }
519    }
520
521    if let Ok(service) = std::env::var("MOCKFORGE_OTLP_SERVICE_NAME") {
522        if !service.trim().is_empty() {
523            if let Some(otel) = config.observability.opentelemetry.as_mut() {
524                otel.service_name = service;
525            }
526        }
527    }
528
529    // AMQP broker overrides
530    if let Ok(port) = std::env::var("MOCKFORGE_AMQP_PORT") {
531        if let Ok(port_num) = port.parse() {
532            config.amqp.port = port_num;
533        }
534    }
535
536    if let Ok(host) = std::env::var("MOCKFORGE_AMQP_HOST") {
537        config.amqp.host = host;
538    }
539
540    if let Ok(enabled) = std::env::var("MOCKFORGE_AMQP_ENABLED") {
541        config.amqp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
542    }
543
544    // SMTP server overrides
545    if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
546        if let Ok(port_num) = port.parse() {
547            config.smtp.port = port_num;
548        }
549    }
550
551    if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
552        config.smtp.host = host;
553    }
554
555    if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
556        config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
557    }
558
559    if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
560        config.smtp.hostname = hostname;
561    }
562
563    // TCP server overrides
564    if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
565        if let Ok(port_num) = port.parse() {
566            config.tcp.port = port_num;
567        }
568    }
569
570    if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
571        config.tcp.host = host;
572    }
573
574    if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
575        config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
576    }
577
578    // Admin UI overrides
579    if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
580        if let Ok(port_num) = port.parse() {
581            config.admin.port = port_num;
582        }
583    }
584
585    if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
586        config.admin.enabled = true;
587    }
588
589    // Admin UI host override - critical for Docker deployments
590    if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
591        config.admin.host = host;
592    }
593
594    if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
595        if !mount_path.trim().is_empty() {
596            config.admin.mount_path = Some(mount_path);
597        }
598    }
599
600    if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
601        let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
602        config.admin.api_enabled = on;
603    }
604
605    if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
606        config.admin.prometheus_url = prometheus_url;
607    }
608
609    // Core configuration overrides
610    if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
611        let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
612        config.core.latency_enabled = enabled;
613    }
614
615    if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
616        let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
617        config.core.failures_enabled = enabled;
618    }
619
620    if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
621        let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
622        config.core.overrides_enabled = enabled;
623    }
624
625    if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
626        let enabled =
627            traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
628        config.core.traffic_shaping_enabled = enabled;
629    }
630
631    // Traffic shaping overrides
632    if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
633        let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
634        config.core.traffic_shaping.bandwidth.enabled = enabled;
635    }
636
637    if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
638        if let Ok(bytes) = max_bytes_per_sec.parse() {
639            config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
640            config.core.traffic_shaping.bandwidth.enabled = true;
641        }
642    }
643
644    if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
645        if let Ok(bytes) = burst_capacity.parse() {
646            config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
647        }
648    }
649
650    if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
651        let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
652        config.core.traffic_shaping.burst_loss.enabled = enabled;
653    }
654
655    if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
656        if let Ok(prob) = burst_probability.parse::<f64>() {
657            config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
658            config.core.traffic_shaping.burst_loss.enabled = true;
659        }
660    }
661
662    if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
663        if let Ok(ms) = burst_duration.parse() {
664            config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
665        }
666    }
667
668    if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
669        if let Ok(rate) = loss_rate.parse::<f64>() {
670            config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
671        }
672    }
673
674    if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
675        if let Ok(ms) = recovery_time.parse() {
676            config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
677        }
678    }
679
680    // Logging overrides
681    if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
682        config.logging.level = level;
683    }
684
685    config
686}
687
688/// Validate configuration
689pub fn validate_config(config: &ServerConfig) -> Result<()> {
690    // Validate port ranges
691    if config.http.port == 0 {
692        return Err(Error::config("HTTP port cannot be 0"));
693    }
694    if config.websocket.port == 0 {
695        return Err(Error::config("WebSocket port cannot be 0"));
696    }
697    if config.grpc.port == 0 {
698        return Err(Error::config("gRPC port cannot be 0"));
699    }
700    if config.admin.port == 0 {
701        return Err(Error::config("Admin port cannot be 0"));
702    }
703
704    // Check for port conflicts
705    let ports = [
706        ("HTTP", config.http.port),
707        ("WebSocket", config.websocket.port),
708        ("gRPC", config.grpc.port),
709        ("Admin", config.admin.port),
710    ];
711
712    for i in 0..ports.len() {
713        for j in (i + 1)..ports.len() {
714            if ports[i].1 == ports[j].1 {
715                return Err(Error::config(format!(
716                    "Port conflict: {} and {} both use port {}",
717                    ports[i].0, ports[j].0, ports[i].1
718                )));
719            }
720        }
721    }
722
723    // Validate log level
724    let valid_levels = ["trace", "debug", "info", "warn", "error"];
725    if !valid_levels.contains(&config.logging.level.as_str()) {
726        return Err(Error::config(format!(
727            "Invalid log level: {}. Valid levels: {}",
728            config.logging.level,
729            valid_levels.join(", ")
730        )));
731    }
732
733    Ok(())
734}
735
736/// Apply a profile to a base configuration
737pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
738    // Macro to merge optional fields
739    macro_rules! merge_field {
740        ($field:ident) => {
741            if let Some(override_val) = profile.$field {
742                base.$field = override_val;
743            }
744        };
745    }
746
747    merge_field!(http);
748    merge_field!(websocket);
749    merge_field!(graphql);
750    merge_field!(grpc);
751    merge_field!(mqtt);
752    merge_field!(smtp);
753    merge_field!(ftp);
754    merge_field!(kafka);
755    merge_field!(amqp);
756    merge_field!(tcp);
757    merge_field!(admin);
758    merge_field!(chaining);
759    merge_field!(core);
760    merge_field!(logging);
761    merge_field!(data);
762    merge_field!(mockai);
763    merge_field!(observability);
764    merge_field!(multi_tenant);
765    merge_field!(routes);
766    merge_field!(protocols);
767
768    base
769}
770
771/// Load configuration with profile support
772pub async fn load_config_with_profile<P: AsRef<Path>>(
773    path: P,
774    profile_name: Option<&str>,
775) -> Result<ServerConfig> {
776    // Use load_config_auto to support all formats
777    let mut config = load_config_auto(&path).await?;
778
779    // Apply profile if specified
780    if let Some(profile) = profile_name {
781        if let Some(profile_config) = config.profiles.remove(profile) {
782            tracing::info!("Applying profile: {}", profile);
783            config = apply_profile(config, profile_config);
784        } else {
785            return Err(Error::config(format!(
786                "Profile '{}' not found in configuration. Available profiles: {}",
787                profile,
788                config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
789            )));
790        }
791    }
792
793    // Clear profiles from final config to save memory
794    config.profiles.clear();
795
796    Ok(config)
797}
798
799/// Load configuration from TypeScript/JavaScript file
800#[cfg(feature = "scripting")]
801pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
802    use rquickjs::{Context, Runtime};
803
804    let content = fs::read_to_string(&path)
805        .await
806        .map_err(|e| Error::io_with_context("reading JS/TS config file", e.to_string()))?;
807
808    // Create a JavaScript runtime
809    let runtime =
810        Runtime::new().map_err(|e| Error::config(format!("Failed to create JS runtime: {}", e)))?;
811    let context = Context::full(&runtime)
812        .map_err(|e| Error::config(format!("Failed to create JS context: {}", e)))?;
813
814    context.with(|ctx| {
815        // For TypeScript files, we need to strip type annotations
816        // This is a simple approach - for production, consider using a proper TS compiler
817        let js_content = if path
818            .as_ref()
819            .extension()
820            .and_then(|s| s.to_str())
821            .map(|ext| ext == "ts")
822            .unwrap_or(false)
823        {
824            strip_typescript_types(&content)?
825        } else {
826            content
827        };
828
829        // Evaluate the config file — uses rquickjs sandboxed JS runtime (not arbitrary code execution)
830        let result: rquickjs::Value = ctx
831            .eval(js_content.as_bytes())
832            .map_err(|e| Error::config(format!("Failed to evaluate JS config: {}", e)))?;
833
834        // Convert to JSON string
835        let json_str: String = ctx
836            .json_stringify(result)
837            .map_err(|e| Error::config(format!("Failed to stringify JS config: {}", e)))?
838            .ok_or_else(|| Error::config("JS config returned undefined"))?
839            .get()
840            .map_err(|e| Error::config(format!("Failed to get JSON string: {}", e)))?;
841
842        // Parse JSON into ServerConfig
843        serde_json::from_str(&json_str)
844            .map_err(|e| Error::config(format!("Failed to parse JS config as ServerConfig: {}", e)))
845    })
846}
847
848/// Simple TypeScript type stripper (removes type annotations)
849/// Note: This is a basic implementation. For production use, consider using swc or esbuild
850///
851/// # Errors
852/// Returns an error if regex compilation fails. This should never happen with static patterns,
853/// but we handle it gracefully to prevent panics.
854#[cfg(feature = "scripting")]
855fn strip_typescript_types(content: &str) -> Result<String> {
856    use regex::Regex;
857
858    let mut result = content.to_string();
859
860    // Compile regex patterns with error handling
861    // Note: These patterns are statically known and should never fail,
862    // but we handle errors to prevent panics in edge cases
863
864    // Remove interface declarations (handles multi-line)
865    let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
866        .map_err(|e| Error::config(format!("Failed to compile interface regex: {}", e)))?;
867    result = interface_re.replace_all(&result, "").to_string();
868
869    // Remove type aliases
870    let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
871        .map_err(|e| Error::config(format!("Failed to compile type alias regex: {}", e)))?;
872    result = type_alias_re.replace_all(&result, "").to_string();
873
874    // Remove type annotations (: Type)
875    let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
876        .map_err(|e| Error::config(format!("Failed to compile type annotation regex: {}", e)))?;
877    result = type_annotation_re.replace_all(&result, "").to_string();
878
879    // Remove type imports and exports
880    let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
881        .map_err(|e| Error::config(format!("Failed to compile type import regex: {}", e)))?;
882    result = type_import_re.replace_all(&result, "").to_string();
883
884    // Remove as Type
885    let as_type_re = Regex::new(r"\s+as\s+\w+")
886        .map_err(|e| Error::config(format!("Failed to compile 'as type' regex: {}", e)))?;
887    result = as_type_re.replace_all(&result, "").to_string();
888
889    Ok(result)
890}
891
892/// Enhanced load_config that supports multiple formats including JS/TS
893pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
894    let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
895
896    match ext {
897        #[cfg(feature = "scripting")]
898        "ts" | "js" => load_config_from_js(&path).await,
899        #[cfg(not(feature = "scripting"))]
900        "ts" | "js" => Err(Error::config(
901            "JS/TS config files require the 'scripting' feature (rquickjs). \
902             Enable it with: cargo build --features scripting"
903                .to_string(),
904        )),
905        "yaml" | "yml" | "json" => load_config(&path).await,
906        _ => Err(Error::config(format!(
907            "Unsupported config file format: {}. Supported: .yaml, .yml, .json{}",
908            ext,
909            if cfg!(feature = "scripting") {
910                ", .ts, .js"
911            } else {
912                ""
913            }
914        ))),
915    }
916}
917
918/// Discover configuration file with support for all formats
919pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
920    let current_dir = std::env::current_dir()
921        .map_err(|e| Error::config(format!("Failed to get current directory: {}", e)))?;
922
923    let config_names = vec![
924        "mockforge.config.ts",
925        "mockforge.config.js",
926        "mockforge.yaml",
927        "mockforge.yml",
928        ".mockforge.yaml",
929        ".mockforge.yml",
930    ];
931
932    // Check current directory
933    for name in &config_names {
934        let path = current_dir.join(name);
935        if fs::metadata(&path).await.is_ok() {
936            return Ok(path);
937        }
938    }
939
940    // Check parent directories (up to 5 levels)
941    let mut dir = current_dir.clone();
942    for _ in 0..5 {
943        if let Some(parent) = dir.parent() {
944            for name in &config_names {
945                let path = parent.join(name);
946                if fs::metadata(&path).await.is_ok() {
947                    return Ok(path);
948                }
949            }
950            dir = parent.to_path_buf();
951        } else {
952            break;
953        }
954    }
955
956    Err(Error::config(
957        "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
958    ))
959}
960
961#[cfg(test)]
962mod tests {
963    use super::*;
964
965    #[test]
966    fn test_default_config() {
967        let config = ServerConfig::default();
968        assert_eq!(config.http.port, 3000);
969        assert_eq!(config.websocket.port, 3001);
970        assert_eq!(config.grpc.port, 50051);
971        assert_eq!(config.admin.port, 9080);
972    }
973
974    #[test]
975    fn test_config_validation() {
976        let mut config = ServerConfig::default();
977        assert!(validate_config(&config).is_ok());
978
979        // Test port conflict
980        config.websocket.port = config.http.port;
981        assert!(validate_config(&config).is_err());
982
983        // Test invalid log level
984        config.websocket.port = 3001; // Fix port conflict
985        config.logging.level = "invalid".to_string();
986        assert!(validate_config(&config).is_err());
987    }
988
989    #[test]
990    fn test_apply_profile() {
991        let base = ServerConfig::default();
992        assert_eq!(base.http.port, 3000);
993
994        let profile = ProfileConfig {
995            http: Some(HttpConfig {
996                port: 8080,
997                ..Default::default()
998            }),
999            logging: Some(LoggingConfig {
1000                level: "debug".to_string(),
1001                ..Default::default()
1002            }),
1003            ..Default::default()
1004        };
1005
1006        let merged = apply_profile(base, profile);
1007        assert_eq!(merged.http.port, 8080);
1008        assert_eq!(merged.logging.level, "debug");
1009        assert_eq!(merged.websocket.port, 3001); // Unchanged
1010    }
1011
1012    #[test]
1013    fn test_minimal_config() {
1014        let config = ServerConfig::minimal();
1015        assert_eq!(config.http.port, 3000);
1016        assert!(!config.admin.enabled);
1017    }
1018
1019    #[test]
1020    fn test_development_config() {
1021        let config = ServerConfig::development();
1022        assert!(config.admin.enabled);
1023        assert_eq!(config.logging.level, "debug");
1024    }
1025
1026    #[test]
1027    fn test_ci_config() {
1028        let config = ServerConfig::ci();
1029        assert!(!config.core.latency_enabled);
1030        assert!(!config.core.failures_enabled);
1031    }
1032
1033    #[test]
1034    fn test_builder_with_http_port() {
1035        let config = ServerConfig::minimal().with_http_port(8080);
1036        assert_eq!(config.http.port, 8080);
1037    }
1038
1039    #[test]
1040    fn test_builder_with_admin() {
1041        let config = ServerConfig::minimal().with_admin(9090);
1042        assert!(config.admin.enabled);
1043        assert_eq!(config.admin.port, 9090);
1044    }
1045
1046    #[test]
1047    fn test_builder_with_grpc() {
1048        let config = ServerConfig::minimal().with_grpc(50052);
1049        assert!(config.grpc.enabled);
1050        assert_eq!(config.grpc.port, 50052);
1051        assert!(config.protocols.grpc.enabled);
1052    }
1053
1054    #[test]
1055    fn test_builder_with_websocket() {
1056        let config = ServerConfig::minimal().with_websocket(3002);
1057        assert!(config.websocket.enabled);
1058        assert_eq!(config.websocket.port, 3002);
1059    }
1060
1061    #[test]
1062    fn test_builder_with_log_level() {
1063        let config = ServerConfig::minimal().with_log_level("trace");
1064        assert_eq!(config.logging.level, "trace");
1065    }
1066
1067    #[test]
1068    fn test_has_advanced_features_default() {
1069        let config = ServerConfig::minimal();
1070        assert!(!config.has_advanced_features());
1071    }
1072
1073    #[test]
1074    fn test_has_enterprise_features_default() {
1075        let config = ServerConfig::minimal();
1076        assert!(!config.has_enterprise_features());
1077    }
1078
1079    #[test]
1080    #[cfg(feature = "scripting")]
1081    fn test_strip_typescript_types() {
1082        let ts_code = r#"
1083interface Config {
1084    port: number;
1085    host: string;
1086}
1087
1088const config: Config = {
1089    port: 3000,
1090    host: "localhost"
1091} as Config;
1092"#;
1093
1094        let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
1095        assert!(!stripped.contains("interface"));
1096        assert!(!stripped.contains(": Config"));
1097        assert!(!stripped.contains("as Config"));
1098    }
1099}