1mod 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#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(default)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29pub struct RealitySliderConfig {
30 pub level: RealityLevel,
32 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[serde(default)]
49pub struct ServerConfig {
50 pub http: HttpConfig,
52 pub websocket: WebSocketConfig,
54 pub graphql: GraphQLConfig,
56 pub grpc: GrpcConfig,
58 pub mqtt: MqttConfig,
60 pub smtp: SmtpConfig,
62 pub ftp: FtpConfig,
64 pub kafka: KafkaConfig,
66 pub amqp: AmqpConfig,
68 pub tcp: TcpConfig,
70 pub admin: AdminConfig,
72 pub chaining: ChainingConfig,
74 pub core: CoreConfig,
76 pub logging: LoggingConfig,
78 pub data: DataConfig,
80 #[serde(default)]
82 pub mockai: MockAIConfig,
83 pub observability: ObservabilityConfig,
85 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
87 #[serde(default)]
89 pub routes: Vec<RouteConfig>,
90 #[serde(default)]
92 pub protocols: ProtocolsConfig,
93 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
95 pub profiles: HashMap<String, ProfileConfig>,
96 #[serde(default)]
98 pub deceptive_deploy: DeceptiveDeployConfig,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub behavioral_cloning: Option<BehavioralCloningConfig>,
102 #[serde(default)]
104 pub reality: RealitySliderConfig,
105 #[serde(default)]
107 pub reality_continuum: crate::reality_continuum::ContinuumConfig,
108 #[serde(default)]
110 pub security: SecurityConfig,
111 #[serde(default)]
113 pub drift_budget: crate::contract_drift::DriftBudgetConfig,
114 #[serde(default)]
116 pub incidents: IncidentConfig,
117 #[serde(default)]
119 pub pr_generation: crate::pr_generation::PRGenerationConfig,
120 #[serde(default)]
122 pub consumer_contracts: ConsumerContractsConfig,
123 #[serde(default)]
125 pub contracts: ContractsConfig,
126 #[serde(default)]
128 pub behavioral_economics: BehavioralEconomicsConfig,
129 #[serde(default)]
131 pub drift_learning: DriftLearningConfig,
132 #[serde(default)]
134 pub org_ai_controls: crate::ai_studio::org_controls::OrgAiControlsConfig,
135 #[serde(default)]
137 pub performance: PerformanceConfig,
138 #[serde(default)]
140 pub plugins: PluginResourceConfig,
141 #[serde(default)]
143 pub hot_reload: ConfigHotReloadConfig,
144 #[serde(default)]
146 pub secrets: SecretBackendConfig,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, Default)]
151#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[serde(default)]
153pub struct ProfileConfig {
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub http: Option<HttpConfig>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub websocket: Option<WebSocketConfig>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub graphql: Option<GraphQLConfig>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub grpc: Option<GrpcConfig>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub mqtt: Option<MqttConfig>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub smtp: Option<SmtpConfig>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub ftp: Option<FtpConfig>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub kafka: Option<KafkaConfig>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub amqp: Option<AmqpConfig>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub tcp: Option<TcpConfig>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub admin: Option<AdminConfig>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub chaining: Option<ChainingConfig>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub core: Option<CoreConfig>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub logging: Option<LoggingConfig>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub data: Option<DataConfig>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub mockai: Option<MockAIConfig>,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub observability: Option<ObservabilityConfig>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub routes: Option<Vec<RouteConfig>>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub protocols: Option<ProtocolsConfig>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub deceptive_deploy: Option<DeceptiveDeployConfig>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub reality: Option<RealitySliderConfig>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub security: Option<SecurityConfig>,
226}
227
228impl ServerConfig {
229 pub fn minimal() -> Self {
231 Self::default()
232 }
233
234 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 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 #[must_use]
254 pub fn with_http_port(mut self, port: u16) -> Self {
255 self.http.port = port;
256 self
257 }
258
259 #[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 #[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 #[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 #[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 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 pub fn has_enterprise_features(&self) -> bool {
303 self.multi_tenant.enabled || self.security.monitoring.siem.enabled
304 }
305}
306
307pub 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 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 let error_msg = e.to_string();
320 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
321
322 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 let error_msg = e.to_string();
342 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
343
344 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
365pub 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
384pub 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
402pub 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
409pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
411 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
682 config.logging.level = level;
683 }
684
685 config
686}
687
688pub fn validate_config(config: &ServerConfig) -> Result<()> {
690 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 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 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
736pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
738 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
771pub async fn load_config_with_profile<P: AsRef<Path>>(
773 path: P,
774 profile_name: Option<&str>,
775) -> Result<ServerConfig> {
776 let mut config = load_config_auto(&path).await?;
778
779 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 config.profiles.clear();
795
796 Ok(config)
797}
798
799#[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 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 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 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 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 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#[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 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 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 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 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 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
892pub 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
918pub 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 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 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 config.websocket.port = config.http.port;
981 assert!(validate_config(&config).is_err());
982
983 config.websocket.port = 3001; 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); }
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}