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
228// Default is derived for ServerConfig
229
230/// Load configuration from file
231pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
232    let content = fs::read_to_string(&path)
233        .await
234        .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
235
236    // Parse config with improved error messages
237    let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
238        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
239    {
240        serde_yaml::from_str(&content).map_err(|e| {
241            // Improve error message with field path context
242            let error_msg = e.to_string();
243            let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
244
245            // Add helpful context for common errors
246            if error_msg.contains("missing field") {
247                full_msg.push_str(
248                    "\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
249                );
250                full_msg.push_str(
251                    "\n   Omit fields you don't need - MockForge will use sensible defaults.",
252                );
253                full_msg.push_str("\n   See config.template.yaml for all available options.");
254            } else if error_msg.contains("unknown field") {
255                full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
256                full_msg.push_str("\n   See config.template.yaml for valid field names.");
257            }
258
259            Error::generic(full_msg)
260        })?
261    } else {
262        serde_json::from_str(&content).map_err(|e| {
263            // Improve error message with field path context
264            let error_msg = e.to_string();
265            let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
266
267            // Add helpful context for common errors
268            if error_msg.contains("missing field") {
269                full_msg.push_str(
270                    "\n\n\u{1f4a1} Most configuration fields are optional with defaults.",
271                );
272                full_msg.push_str(
273                    "\n   Omit fields you don't need - MockForge will use sensible defaults.",
274                );
275                full_msg.push_str("\n   See config.template.yaml for all available options.");
276            } else if error_msg.contains("unknown field") {
277                full_msg.push_str("\n\n\u{1f4a1} Check for typos in field names.");
278                full_msg.push_str("\n   See config.template.yaml for valid field names.");
279            }
280
281            Error::generic(full_msg)
282        })?
283    };
284
285    Ok(config)
286}
287
288/// Save configuration to file
289pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
290    let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
291        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
292    {
293        serde_yaml::to_string(config)
294            .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
295    } else {
296        serde_json::to_string_pretty(config)
297            .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
298    };
299
300    fs::write(path, content)
301        .await
302        .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
303
304    Ok(())
305}
306
307/// Load configuration with fallback to default
308pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
309    match load_config(&path).await {
310        Ok(config) => {
311            tracing::info!("Loaded configuration from {:?}", path.as_ref());
312            config
313        }
314        Err(e) => {
315            tracing::warn!(
316                "Failed to load config from {:?}: {}. Using defaults.",
317                path.as_ref(),
318                e
319            );
320            ServerConfig::default()
321        }
322    }
323}
324
325/// Create default configuration file
326pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
327    let config = ServerConfig::default();
328    save_config(path, &config).await?;
329    Ok(())
330}
331
332/// Environment variable overrides for configuration
333pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
334    // HTTP server overrides
335    if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
336        if let Ok(port_num) = port.parse() {
337            config.http.port = port_num;
338        }
339    }
340
341    if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
342        config.http.host = host;
343    }
344
345    // WebSocket server overrides
346    if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
347        if let Ok(port_num) = port.parse() {
348            config.websocket.port = port_num;
349        }
350    }
351
352    // gRPC server overrides
353    if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
354        if let Ok(port_num) = port.parse() {
355            config.grpc.port = port_num;
356        }
357    }
358
359    // SMTP server overrides
360    if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
361        if let Ok(port_num) = port.parse() {
362            config.smtp.port = port_num;
363        }
364    }
365
366    if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
367        config.smtp.host = host;
368    }
369
370    if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
371        config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
372    }
373
374    if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
375        config.smtp.hostname = hostname;
376    }
377
378    // TCP server overrides
379    if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
380        if let Ok(port_num) = port.parse() {
381            config.tcp.port = port_num;
382        }
383    }
384
385    if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
386        config.tcp.host = host;
387    }
388
389    if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
390        config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
391    }
392
393    // Admin UI overrides
394    if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
395        if let Ok(port_num) = port.parse() {
396            config.admin.port = port_num;
397        }
398    }
399
400    if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
401        config.admin.enabled = true;
402    }
403
404    // Admin UI host override - critical for Docker deployments
405    if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
406        config.admin.host = host;
407    }
408
409    if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
410        if !mount_path.trim().is_empty() {
411            config.admin.mount_path = Some(mount_path);
412        }
413    }
414
415    if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
416        let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
417        config.admin.api_enabled = on;
418    }
419
420    if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
421        config.admin.prometheus_url = prometheus_url;
422    }
423
424    // Core configuration overrides
425    if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
426        let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
427        config.core.latency_enabled = enabled;
428    }
429
430    if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
431        let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
432        config.core.failures_enabled = enabled;
433    }
434
435    if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
436        let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
437        config.core.overrides_enabled = enabled;
438    }
439
440    if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
441        let enabled =
442            traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
443        config.core.traffic_shaping_enabled = enabled;
444    }
445
446    // Traffic shaping overrides
447    if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
448        let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
449        config.core.traffic_shaping.bandwidth.enabled = enabled;
450    }
451
452    if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
453        if let Ok(bytes) = max_bytes_per_sec.parse() {
454            config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
455            config.core.traffic_shaping.bandwidth.enabled = true;
456        }
457    }
458
459    if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
460        if let Ok(bytes) = burst_capacity.parse() {
461            config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
462        }
463    }
464
465    if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
466        let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
467        config.core.traffic_shaping.burst_loss.enabled = enabled;
468    }
469
470    if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
471        if let Ok(prob) = burst_probability.parse::<f64>() {
472            config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
473            config.core.traffic_shaping.burst_loss.enabled = true;
474        }
475    }
476
477    if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
478        if let Ok(ms) = burst_duration.parse() {
479            config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
480        }
481    }
482
483    if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
484        if let Ok(rate) = loss_rate.parse::<f64>() {
485            config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
486        }
487    }
488
489    if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
490        if let Ok(ms) = recovery_time.parse() {
491            config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
492        }
493    }
494
495    // Logging overrides
496    if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
497        config.logging.level = level;
498    }
499
500    config
501}
502
503/// Validate configuration
504pub fn validate_config(config: &ServerConfig) -> Result<()> {
505    // Validate port ranges
506    if config.http.port == 0 {
507        return Err(Error::generic("HTTP port cannot be 0"));
508    }
509    if config.websocket.port == 0 {
510        return Err(Error::generic("WebSocket port cannot be 0"));
511    }
512    if config.grpc.port == 0 {
513        return Err(Error::generic("gRPC port cannot be 0"));
514    }
515    if config.admin.port == 0 {
516        return Err(Error::generic("Admin port cannot be 0"));
517    }
518
519    // Check for port conflicts
520    let ports = [
521        ("HTTP", config.http.port),
522        ("WebSocket", config.websocket.port),
523        ("gRPC", config.grpc.port),
524        ("Admin", config.admin.port),
525    ];
526
527    for i in 0..ports.len() {
528        for j in (i + 1)..ports.len() {
529            if ports[i].1 == ports[j].1 {
530                return Err(Error::generic(format!(
531                    "Port conflict: {} and {} both use port {}",
532                    ports[i].0, ports[j].0, ports[i].1
533                )));
534            }
535        }
536    }
537
538    // Validate log level
539    let valid_levels = ["trace", "debug", "info", "warn", "error"];
540    if !valid_levels.contains(&config.logging.level.as_str()) {
541        return Err(Error::generic(format!(
542            "Invalid log level: {}. Valid levels: {}",
543            config.logging.level,
544            valid_levels.join(", ")
545        )));
546    }
547
548    Ok(())
549}
550
551/// Apply a profile to a base configuration
552pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
553    // Macro to merge optional fields
554    macro_rules! merge_field {
555        ($field:ident) => {
556            if let Some(override_val) = profile.$field {
557                base.$field = override_val;
558            }
559        };
560    }
561
562    merge_field!(http);
563    merge_field!(websocket);
564    merge_field!(graphql);
565    merge_field!(grpc);
566    merge_field!(mqtt);
567    merge_field!(smtp);
568    merge_field!(ftp);
569    merge_field!(kafka);
570    merge_field!(amqp);
571    merge_field!(tcp);
572    merge_field!(admin);
573    merge_field!(chaining);
574    merge_field!(core);
575    merge_field!(logging);
576    merge_field!(data);
577    merge_field!(mockai);
578    merge_field!(observability);
579    merge_field!(multi_tenant);
580    merge_field!(routes);
581    merge_field!(protocols);
582
583    base
584}
585
586/// Load configuration with profile support
587pub async fn load_config_with_profile<P: AsRef<Path>>(
588    path: P,
589    profile_name: Option<&str>,
590) -> Result<ServerConfig> {
591    // Use load_config_auto to support all formats
592    let mut config = load_config_auto(&path).await?;
593
594    // Apply profile if specified
595    if let Some(profile) = profile_name {
596        if let Some(profile_config) = config.profiles.remove(profile) {
597            tracing::info!("Applying profile: {}", profile);
598            config = apply_profile(config, profile_config);
599        } else {
600            return Err(Error::generic(format!(
601                "Profile '{}' not found in configuration. Available profiles: {}",
602                profile,
603                config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
604            )));
605        }
606    }
607
608    // Clear profiles from final config to save memory
609    config.profiles.clear();
610
611    Ok(config)
612}
613
614/// Load configuration from TypeScript/JavaScript file
615pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
616    use rquickjs::{Context, Runtime};
617
618    let content = fs::read_to_string(&path)
619        .await
620        .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
621
622    // Create a JavaScript runtime
623    let runtime = Runtime::new()
624        .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
625    let context = Context::full(&runtime)
626        .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
627
628    context.with(|ctx| {
629        // For TypeScript files, we need to strip type annotations
630        // This is a simple approach - for production, consider using a proper TS compiler
631        let js_content = if path
632            .as_ref()
633            .extension()
634            .and_then(|s| s.to_str())
635            .map(|ext| ext == "ts")
636            .unwrap_or(false)
637        {
638            strip_typescript_types(&content)?
639        } else {
640            content
641        };
642
643        // Evaluate the config file — uses rquickjs sandboxed JS runtime (not arbitrary code execution)
644        let result: rquickjs::Value = ctx
645            .eval(js_content.as_bytes())
646            .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
647
648        // Convert to JSON string
649        let json_str: String = ctx
650            .json_stringify(result)
651            .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
652            .ok_or_else(|| Error::generic("JS config returned undefined"))?
653            .get()
654            .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
655
656        // Parse JSON into ServerConfig
657        serde_json::from_str(&json_str).map_err(|e| {
658            Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
659        })
660    })
661}
662
663/// Simple TypeScript type stripper (removes type annotations)
664/// Note: This is a basic implementation. For production use, consider using swc or esbuild
665///
666/// # Errors
667/// Returns an error if regex compilation fails. This should never happen with static patterns,
668/// but we handle it gracefully to prevent panics.
669fn strip_typescript_types(content: &str) -> Result<String> {
670    use regex::Regex;
671
672    let mut result = content.to_string();
673
674    // Compile regex patterns with error handling
675    // Note: These patterns are statically known and should never fail,
676    // but we handle errors to prevent panics in edge cases
677
678    // Remove interface declarations (handles multi-line)
679    let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
680        .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
681    result = interface_re.replace_all(&result, "").to_string();
682
683    // Remove type aliases
684    let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
685        .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
686    result = type_alias_re.replace_all(&result, "").to_string();
687
688    // Remove type annotations (: Type)
689    let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
690        .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
691    result = type_annotation_re.replace_all(&result, "").to_string();
692
693    // Remove type imports and exports
694    let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
695        .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
696    result = type_import_re.replace_all(&result, "").to_string();
697
698    // Remove as Type
699    let as_type_re = Regex::new(r"\s+as\s+\w+")
700        .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
701    result = as_type_re.replace_all(&result, "").to_string();
702
703    Ok(result)
704}
705
706/// Enhanced load_config that supports multiple formats including JS/TS
707pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
708    let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
709
710    match ext {
711        "ts" | "js" => load_config_from_js(&path).await,
712        "yaml" | "yml" | "json" => load_config(&path).await,
713        _ => Err(Error::generic(format!(
714            "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
715            ext
716        ))),
717    }
718}
719
720/// Discover configuration file with support for all formats
721pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
722    let current_dir = std::env::current_dir()
723        .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
724
725    let config_names = vec![
726        "mockforge.config.ts",
727        "mockforge.config.js",
728        "mockforge.yaml",
729        "mockforge.yml",
730        ".mockforge.yaml",
731        ".mockforge.yml",
732    ];
733
734    // Check current directory
735    for name in &config_names {
736        let path = current_dir.join(name);
737        if fs::metadata(&path).await.is_ok() {
738            return Ok(path);
739        }
740    }
741
742    // Check parent directories (up to 5 levels)
743    let mut dir = current_dir.clone();
744    for _ in 0..5 {
745        if let Some(parent) = dir.parent() {
746            for name in &config_names {
747                let path = parent.join(name);
748                if fs::metadata(&path).await.is_ok() {
749                    return Ok(path);
750                }
751            }
752            dir = parent.to_path_buf();
753        } else {
754            break;
755        }
756    }
757
758    Err(Error::generic(
759        "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
760    ))
761}
762
763#[cfg(test)]
764mod tests {
765    use super::*;
766
767    #[test]
768    fn test_default_config() {
769        let config = ServerConfig::default();
770        assert_eq!(config.http.port, 3000);
771        assert_eq!(config.websocket.port, 3001);
772        assert_eq!(config.grpc.port, 50051);
773        assert_eq!(config.admin.port, 9080);
774    }
775
776    #[test]
777    fn test_config_validation() {
778        let mut config = ServerConfig::default();
779        assert!(validate_config(&config).is_ok());
780
781        // Test port conflict
782        config.websocket.port = config.http.port;
783        assert!(validate_config(&config).is_err());
784
785        // Test invalid log level
786        config.websocket.port = 3001; // Fix port conflict
787        config.logging.level = "invalid".to_string();
788        assert!(validate_config(&config).is_err());
789    }
790
791    #[test]
792    fn test_apply_profile() {
793        let base = ServerConfig::default();
794        assert_eq!(base.http.port, 3000);
795
796        let profile = ProfileConfig {
797            http: Some(HttpConfig {
798                port: 8080,
799                ..Default::default()
800            }),
801            logging: Some(LoggingConfig {
802                level: "debug".to_string(),
803                ..Default::default()
804            }),
805            ..Default::default()
806        };
807
808        let merged = apply_profile(base, profile);
809        assert_eq!(merged.http.port, 8080);
810        assert_eq!(merged.logging.level, "debug");
811        assert_eq!(merged.websocket.port, 3001); // Unchanged
812    }
813
814    #[test]
815    fn test_strip_typescript_types() {
816        let ts_code = r#"
817interface Config {
818    port: number;
819    host: string;
820}
821
822const config: Config = {
823    port: 3000,
824    host: "localhost"
825} as Config;
826"#;
827
828        let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
829        assert!(!stripped.contains("interface"));
830        assert!(!stripped.contains(": Config"));
831        assert!(!stripped.contains("as Config"));
832    }
833}