1use crate::{Config as CoreConfig, Error, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7use tokio::fs;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuthConfig {
12 pub jwt: Option<JwtConfig>,
14 pub oauth2: Option<OAuth2Config>,
16 pub basic_auth: Option<BasicAuthConfig>,
18 pub api_key: Option<ApiKeyConfig>,
20 pub require_auth: bool,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct JwtConfig {
27 pub secret: Option<String>,
29 pub rsa_public_key: Option<String>,
31 pub ecdsa_public_key: Option<String>,
33 pub issuer: Option<String>,
35 pub audience: Option<String>,
37 pub algorithms: Vec<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct OAuth2Config {
44 pub client_id: String,
46 pub client_secret: String,
48 pub introspection_url: String,
50 pub auth_url: Option<String>,
52 pub token_url: Option<String>,
54 pub token_type_hint: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct BasicAuthConfig {
61 pub credentials: HashMap<String, String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ApiKeyConfig {
68 pub header_name: String,
70 pub query_name: Option<String>,
72 pub keys: Vec<String>,
74}
75
76impl Default for AuthConfig {
77 fn default() -> Self {
78 Self {
79 jwt: None,
80 oauth2: None,
81 basic_auth: None,
82 api_key: Some(ApiKeyConfig {
83 header_name: "X-API-Key".to_string(),
84 query_name: None,
85 keys: vec![],
86 }),
87 require_auth: false,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct RouteConfig {
95 pub path: String,
97 pub method: String,
99 pub request: Option<RouteRequestConfig>,
101 pub response: RouteResponseConfig,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct RouteRequestConfig {
108 pub validation: Option<RouteValidationConfig>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RouteResponseConfig {
115 pub status: u16,
117 #[serde(default)]
119 pub headers: HashMap<String, String>,
120 pub body: Option<serde_json::Value>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RouteValidationConfig {
127 pub schema: serde_json::Value,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ProtocolConfig {
134 pub enabled: bool,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ProtocolsConfig {
141 pub http: ProtocolConfig,
143 pub graphql: ProtocolConfig,
145 pub grpc: ProtocolConfig,
147 pub websocket: ProtocolConfig,
149 pub smtp: ProtocolConfig,
151 pub mqtt: ProtocolConfig,
153 pub ftp: ProtocolConfig,
155 pub kafka: ProtocolConfig,
157 pub rabbitmq: ProtocolConfig,
159 pub amqp: ProtocolConfig,
161}
162
163impl Default for ProtocolsConfig {
164 fn default() -> Self {
165 Self {
166 http: ProtocolConfig { enabled: true },
167 graphql: ProtocolConfig { enabled: true },
168 grpc: ProtocolConfig { enabled: true },
169 websocket: ProtocolConfig { enabled: true },
170 smtp: ProtocolConfig { enabled: false },
171 mqtt: ProtocolConfig { enabled: true },
172 ftp: ProtocolConfig { enabled: false },
173 kafka: ProtocolConfig { enabled: false },
174 rabbitmq: ProtocolConfig { enabled: false },
175 amqp: ProtocolConfig { enabled: false },
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, Default)]
182#[serde(default)]
183pub struct ServerConfig {
184 pub http: HttpConfig,
186 pub websocket: WebSocketConfig,
188 pub grpc: GrpcConfig,
190 pub mqtt: MqttConfig,
192 pub smtp: SmtpConfig,
194 pub ftp: FtpConfig,
196 pub kafka: KafkaConfig,
198 pub amqp: AmqpConfig,
200 pub admin: AdminConfig,
202 pub chaining: ChainingConfig,
204 pub core: CoreConfig,
206 pub logging: LoggingConfig,
208 pub data: DataConfig,
210 pub observability: ObservabilityConfig,
212 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
214 #[serde(default)]
216 pub routes: Vec<RouteConfig>,
217 #[serde(default)]
219 pub protocols: ProtocolsConfig,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct HttpValidationConfig {
227 pub mode: String,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct HttpCorsConfig {
234 pub enabled: bool,
236 #[serde(default)]
238 pub allowed_origins: Vec<String>,
239 #[serde(default)]
241 pub allowed_methods: Vec<String>,
242 #[serde(default)]
244 pub allowed_headers: Vec<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct HttpConfig {
250 pub enabled: bool,
252 pub port: u16,
254 pub host: String,
256 pub openapi_spec: Option<String>,
258 pub cors: Option<HttpCorsConfig>,
260 pub request_timeout_secs: u64,
262 pub validation: Option<HttpValidationConfig>,
264 pub aggregate_validation_errors: bool,
266 pub validate_responses: bool,
268 pub response_template_expand: bool,
270 pub validation_status: Option<u16>,
272 pub validation_overrides: std::collections::HashMap<String, String>,
274 pub skip_admin_validation: bool,
276 pub auth: Option<AuthConfig>,
278}
279
280impl Default for HttpConfig {
281 fn default() -> Self {
282 Self {
283 enabled: true,
284 port: 3000,
285 host: "0.0.0.0".to_string(),
286 openapi_spec: None,
287 cors: Some(HttpCorsConfig {
288 enabled: true,
289 allowed_origins: vec!["*".to_string()],
290 allowed_methods: vec![
291 "GET".to_string(),
292 "POST".to_string(),
293 "PUT".to_string(),
294 "DELETE".to_string(),
295 "PATCH".to_string(),
296 "OPTIONS".to_string(),
297 ],
298 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
299 }),
300 request_timeout_secs: 30,
301 validation: Some(HttpValidationConfig {
302 mode: "enforce".to_string(),
303 }),
304 aggregate_validation_errors: true,
305 validate_responses: false,
306 response_template_expand: false,
307 validation_status: None,
308 validation_overrides: std::collections::HashMap::new(),
309 skip_admin_validation: true,
310 auth: None,
311 }
312 }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct WebSocketConfig {
318 pub enabled: bool,
320 pub port: u16,
322 pub host: String,
324 pub replay_file: Option<String>,
326 pub connection_timeout_secs: u64,
328}
329
330impl Default for WebSocketConfig {
331 fn default() -> Self {
332 Self {
333 enabled: true,
334 port: 3001,
335 host: "0.0.0.0".to_string(),
336 replay_file: None,
337 connection_timeout_secs: 300,
338 }
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct GrpcConfig {
345 pub enabled: bool,
347 pub port: u16,
349 pub host: String,
351 pub proto_dir: Option<String>,
353 pub tls: Option<TlsConfig>,
355}
356
357impl Default for GrpcConfig {
358 fn default() -> Self {
359 Self {
360 enabled: true,
361 port: 50051,
362 host: "0.0.0.0".to_string(),
363 proto_dir: None,
364 tls: None,
365 }
366 }
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct TlsConfig {
372 pub cert_path: String,
374 pub key_path: String,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct MqttConfig {
381 pub enabled: bool,
383 pub port: u16,
385 pub host: String,
387 pub max_connections: usize,
389 pub max_packet_size: usize,
391 pub keep_alive_secs: u16,
393 pub fixtures_dir: Option<std::path::PathBuf>,
395 pub enable_retained_messages: bool,
397 pub max_retained_messages: usize,
399}
400
401impl Default for MqttConfig {
402 fn default() -> Self {
403 Self {
404 enabled: false,
405 port: 1883,
406 host: "0.0.0.0".to_string(),
407 max_connections: 1000,
408 max_packet_size: 268435456, keep_alive_secs: 60,
410 fixtures_dir: None,
411 enable_retained_messages: true,
412 max_retained_messages: 10000,
413 }
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct SmtpConfig {
420 pub enabled: bool,
422 pub port: u16,
424 pub host: String,
426 pub hostname: String,
428 pub fixtures_dir: Option<std::path::PathBuf>,
430 pub timeout_secs: u64,
432 pub max_connections: usize,
434 pub enable_mailbox: bool,
436 pub max_mailbox_messages: usize,
438 pub enable_starttls: bool,
440 pub tls_cert_path: Option<std::path::PathBuf>,
442 pub tls_key_path: Option<std::path::PathBuf>,
444}
445
446impl Default for SmtpConfig {
447 fn default() -> Self {
448 Self {
449 enabled: false,
450 port: 1025,
451 host: "0.0.0.0".to_string(),
452 hostname: "mockforge-smtp".to_string(),
453 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
454 timeout_secs: 300,
455 max_connections: 10,
456 enable_mailbox: true,
457 max_mailbox_messages: 1000,
458 enable_starttls: false,
459 tls_cert_path: None,
460 tls_key_path: None,
461 }
462 }
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct FtpConfig {
468 pub enabled: bool,
470 pub port: u16,
472 pub host: String,
474 pub passive_ports: (u16, u16),
476 pub max_connections: usize,
478 pub timeout_secs: u64,
480 pub allow_anonymous: bool,
482 pub fixtures_dir: Option<std::path::PathBuf>,
484 pub virtual_root: std::path::PathBuf,
486}
487
488impl Default for FtpConfig {
489 fn default() -> Self {
490 Self {
491 enabled: false,
492 port: 2121,
493 host: "0.0.0.0".to_string(),
494 passive_ports: (50000, 51000),
495 max_connections: 100,
496 timeout_secs: 300,
497 allow_anonymous: true,
498 fixtures_dir: None,
499 virtual_root: std::path::PathBuf::from("/mockforge"),
500 }
501 }
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct KafkaConfig {
507 pub enabled: bool,
509 pub port: u16,
511 pub host: String,
513 pub broker_id: i32,
515 pub max_connections: usize,
517 pub log_retention_ms: i64,
519 pub log_segment_bytes: i64,
521 pub fixtures_dir: Option<std::path::PathBuf>,
523 pub auto_create_topics: bool,
525 pub default_partitions: i32,
527 pub default_replication_factor: i16,
529}
530
531impl Default for KafkaConfig {
532 fn default() -> Self {
533 Self {
534 enabled: false,
535 port: 9092, host: "0.0.0.0".to_string(),
537 broker_id: 1,
538 max_connections: 1000,
539 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
542 auto_create_topics: true,
543 default_partitions: 3,
544 default_replication_factor: 1,
545 }
546 }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct AmqpConfig {
552 pub enabled: bool,
554 pub port: u16,
556 pub host: String,
558 pub max_connections: usize,
560 pub max_channels_per_connection: u16,
562 pub frame_max: u32,
564 pub heartbeat_interval: u16,
566 pub fixtures_dir: Option<std::path::PathBuf>,
568 pub virtual_hosts: Vec<String>,
570}
571
572impl Default for AmqpConfig {
573 fn default() -> Self {
574 Self {
575 enabled: false,
576 port: 5672, host: "0.0.0.0".to_string(),
578 max_connections: 1000,
579 max_channels_per_connection: 100,
580 frame_max: 131072, heartbeat_interval: 60,
582 fixtures_dir: None,
583 virtual_hosts: vec!["/".to_string()],
584 }
585 }
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct AdminConfig {
591 pub enabled: bool,
593 pub port: u16,
595 pub host: String,
597 pub auth_required: bool,
599 pub username: Option<String>,
601 pub password: Option<String>,
603 pub mount_path: Option<String>,
605 pub api_enabled: bool,
607 pub prometheus_url: String,
609}
610
611impl Default for AdminConfig {
612 fn default() -> Self {
613 Self {
614 enabled: false,
615 port: 9080,
616 host: "127.0.0.1".to_string(),
617 auth_required: false,
618 username: None,
619 password: None,
620 mount_path: None,
621 api_enabled: true,
622 prometheus_url: "http://localhost:9090".to_string(),
623 }
624 }
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct LoggingConfig {
630 pub level: String,
632 pub json_format: bool,
634 pub file_path: Option<String>,
636 pub max_file_size_mb: u64,
638 pub max_files: u32,
640}
641
642impl Default for LoggingConfig {
643 fn default() -> Self {
644 Self {
645 level: "info".to_string(),
646 json_format: false,
647 file_path: None,
648 max_file_size_mb: 10,
649 max_files: 5,
650 }
651 }
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize)]
655#[serde(rename_all = "camelCase")]
656pub struct ChainingConfig {
657 pub enabled: bool,
659 pub max_chain_length: usize,
661 pub global_timeout_secs: u64,
663 pub enable_parallel_execution: bool,
665}
666
667impl Default for ChainingConfig {
668 fn default() -> Self {
669 Self {
670 enabled: false,
671 max_chain_length: 20,
672 global_timeout_secs: 300,
673 enable_parallel_execution: false,
674 }
675 }
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize)]
680pub struct DataConfig {
681 pub default_rows: usize,
683 pub default_format: String,
685 pub locale: String,
687 pub templates: HashMap<String, String>,
689 pub rag: RagConfig,
691}
692
693impl Default for DataConfig {
694 fn default() -> Self {
695 Self {
696 default_rows: 100,
697 default_format: "json".to_string(),
698 locale: "en".to_string(),
699 templates: HashMap::new(),
700 rag: RagConfig::default(),
701 }
702 }
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
707#[serde(default)]
708pub struct RagConfig {
709 pub enabled: bool,
711 #[serde(default)]
713 pub provider: String,
714 pub api_endpoint: Option<String>,
716 pub api_key: Option<String>,
718 pub model: Option<String>,
720 #[serde(default = "default_max_tokens")]
722 pub max_tokens: usize,
723 #[serde(default = "default_temperature")]
725 pub temperature: f64,
726 pub context_window: usize,
728 #[serde(default = "default_true")]
730 pub caching: bool,
731 #[serde(default = "default_cache_ttl")]
733 pub cache_ttl_secs: u64,
734 #[serde(default = "default_timeout")]
736 pub timeout_secs: u64,
737 #[serde(default = "default_max_retries")]
739 pub max_retries: usize,
740}
741
742fn default_max_tokens() -> usize {
743 1024
744}
745
746fn default_temperature() -> f64 {
747 0.7
748}
749
750fn default_true() -> bool {
751 true
752}
753
754fn default_cache_ttl() -> u64 {
755 3600
756}
757
758fn default_timeout() -> u64 {
759 30
760}
761
762fn default_max_retries() -> usize {
763 3
764}
765
766impl Default for RagConfig {
767 fn default() -> Self {
768 Self {
769 enabled: false,
770 provider: "openai".to_string(),
771 api_endpoint: None,
772 api_key: None,
773 model: Some("gpt-3.5-turbo".to_string()),
774 max_tokens: default_max_tokens(),
775 temperature: default_temperature(),
776 context_window: 4000,
777 caching: default_true(),
778 cache_ttl_secs: default_cache_ttl(),
779 timeout_secs: default_timeout(),
780 max_retries: default_max_retries(),
781 }
782 }
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize, Default)]
787pub struct ObservabilityConfig {
788 pub prometheus: PrometheusConfig,
790 pub opentelemetry: Option<OpenTelemetryConfig>,
792 pub recorder: Option<RecorderConfig>,
794 pub chaos: Option<ChaosEngConfig>,
796}
797
798#[derive(Debug, Clone, Serialize, Deserialize)]
800pub struct PrometheusConfig {
801 pub enabled: bool,
803 pub port: u16,
805 pub host: String,
807 pub path: String,
809}
810
811impl Default for PrometheusConfig {
812 fn default() -> Self {
813 Self {
814 enabled: true,
815 port: 9090,
816 host: "0.0.0.0".to_string(),
817 path: "/metrics".to_string(),
818 }
819 }
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
824pub struct OpenTelemetryConfig {
825 pub enabled: bool,
827 pub service_name: String,
829 pub environment: String,
831 pub jaeger_endpoint: String,
833 pub otlp_endpoint: Option<String>,
835 pub protocol: String,
837 pub sampling_rate: f64,
839}
840
841impl Default for OpenTelemetryConfig {
842 fn default() -> Self {
843 Self {
844 enabled: false,
845 service_name: "mockforge".to_string(),
846 environment: "development".to_string(),
847 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
848 otlp_endpoint: Some("http://localhost:4317".to_string()),
849 protocol: "grpc".to_string(),
850 sampling_rate: 1.0,
851 }
852 }
853}
854
855#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct RecorderConfig {
858 pub enabled: bool,
860 pub database_path: String,
862 pub api_enabled: bool,
864 pub api_port: Option<u16>,
866 pub max_requests: i64,
868 pub retention_days: i64,
870 pub record_http: bool,
872 pub record_grpc: bool,
874 pub record_websocket: bool,
876 pub record_graphql: bool,
878}
879
880impl Default for RecorderConfig {
881 fn default() -> Self {
882 Self {
883 enabled: false,
884 database_path: "./mockforge-recordings.db".to_string(),
885 api_enabled: true,
886 api_port: None,
887 max_requests: 10000,
888 retention_days: 7,
889 record_http: true,
890 record_grpc: true,
891 record_websocket: true,
892 record_graphql: true,
893 }
894 }
895}
896
897#[derive(Debug, Clone, Serialize, Deserialize, Default)]
899pub struct ChaosEngConfig {
900 pub enabled: bool,
902 pub latency: Option<LatencyInjectionConfig>,
904 pub fault_injection: Option<FaultConfig>,
906 pub rate_limit: Option<RateLimitingConfig>,
908 pub traffic_shaping: Option<NetworkShapingConfig>,
910 pub scenario: Option<String>,
912}
913
914#[derive(Debug, Clone, Serialize, Deserialize)]
916pub struct LatencyInjectionConfig {
917 pub enabled: bool,
918 pub fixed_delay_ms: Option<u64>,
919 pub random_delay_range_ms: Option<(u64, u64)>,
920 pub jitter_percent: f64,
921 pub probability: f64,
922}
923
924#[derive(Debug, Clone, Serialize, Deserialize)]
926pub struct FaultConfig {
927 pub enabled: bool,
928 pub http_errors: Vec<u16>,
929 pub http_error_probability: f64,
930 pub connection_errors: bool,
931 pub connection_error_probability: f64,
932 pub timeout_errors: bool,
933 pub timeout_ms: u64,
934 pub timeout_probability: f64,
935}
936
937#[derive(Debug, Clone, Serialize, Deserialize)]
939pub struct RateLimitingConfig {
940 pub enabled: bool,
941 pub requests_per_second: u32,
942 pub burst_size: u32,
943 pub per_ip: bool,
944 pub per_endpoint: bool,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
949pub struct NetworkShapingConfig {
950 pub enabled: bool,
951 pub bandwidth_limit_bps: u64,
952 pub packet_loss_percent: f64,
953 pub max_connections: u32,
954}
955
956pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
958 let content = fs::read_to_string(&path)
959 .await
960 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
961
962 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
963 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
964 {
965 serde_yaml::from_str(&content)
966 .map_err(|e| Error::generic(format!("Failed to parse YAML config: {}", e)))?
967 } else {
968 serde_json::from_str(&content)
969 .map_err(|e| Error::generic(format!("Failed to parse JSON config: {}", e)))?
970 };
971
972 Ok(config)
973}
974
975pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
977 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
978 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
979 {
980 serde_yaml::to_string(config)
981 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
982 } else {
983 serde_json::to_string_pretty(config)
984 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
985 };
986
987 fs::write(path, content)
988 .await
989 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
990
991 Ok(())
992}
993
994pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
996 match load_config(&path).await {
997 Ok(config) => {
998 tracing::info!("Loaded configuration from {:?}", path.as_ref());
999 config
1000 }
1001 Err(e) => {
1002 tracing::warn!(
1003 "Failed to load config from {:?}: {}. Using defaults.",
1004 path.as_ref(),
1005 e
1006 );
1007 ServerConfig::default()
1008 }
1009 }
1010}
1011
1012pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1014 let config = ServerConfig::default();
1015 save_config(path, &config).await?;
1016 Ok(())
1017}
1018
1019pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1021 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1023 if let Ok(port_num) = port.parse() {
1024 config.http.port = port_num;
1025 }
1026 }
1027
1028 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1029 config.http.host = host;
1030 }
1031
1032 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1034 if let Ok(port_num) = port.parse() {
1035 config.websocket.port = port_num;
1036 }
1037 }
1038
1039 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1041 if let Ok(port_num) = port.parse() {
1042 config.grpc.port = port_num;
1043 }
1044 }
1045
1046 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1048 if let Ok(port_num) = port.parse() {
1049 config.smtp.port = port_num;
1050 }
1051 }
1052
1053 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1054 config.smtp.host = host;
1055 }
1056
1057 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1058 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1059 }
1060
1061 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1062 config.smtp.hostname = hostname;
1063 }
1064
1065 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1067 if let Ok(port_num) = port.parse() {
1068 config.admin.port = port_num;
1069 }
1070 }
1071
1072 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1073 config.admin.enabled = true;
1074 }
1075
1076 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1077 if !mount_path.trim().is_empty() {
1078 config.admin.mount_path = Some(mount_path);
1079 }
1080 }
1081
1082 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1083 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1084 config.admin.api_enabled = on;
1085 }
1086
1087 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1088 config.admin.prometheus_url = prometheus_url;
1089 }
1090
1091 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1093 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1094 config.core.latency_enabled = enabled;
1095 }
1096
1097 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1098 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1099 config.core.failures_enabled = enabled;
1100 }
1101
1102 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1103 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1104 config.core.overrides_enabled = enabled;
1105 }
1106
1107 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1108 let enabled =
1109 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1110 config.core.traffic_shaping_enabled = enabled;
1111 }
1112
1113 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1115 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1116 config.core.traffic_shaping.bandwidth.enabled = enabled;
1117 }
1118
1119 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1120 if let Ok(bytes) = max_bytes_per_sec.parse() {
1121 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1122 config.core.traffic_shaping.bandwidth.enabled = true;
1123 }
1124 }
1125
1126 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1127 if let Ok(bytes) = burst_capacity.parse() {
1128 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1129 }
1130 }
1131
1132 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1133 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1134 config.core.traffic_shaping.burst_loss.enabled = enabled;
1135 }
1136
1137 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1138 if let Ok(prob) = burst_probability.parse::<f64>() {
1139 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1140 config.core.traffic_shaping.burst_loss.enabled = true;
1141 }
1142 }
1143
1144 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1145 if let Ok(ms) = burst_duration.parse() {
1146 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1147 }
1148 }
1149
1150 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1151 if let Ok(rate) = loss_rate.parse::<f64>() {
1152 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1153 }
1154 }
1155
1156 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1157 if let Ok(ms) = recovery_time.parse() {
1158 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1159 }
1160 }
1161
1162 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1164 config.logging.level = level;
1165 }
1166
1167 config
1168}
1169
1170pub fn validate_config(config: &ServerConfig) -> Result<()> {
1172 if config.http.port == 0 {
1174 return Err(Error::generic("HTTP port cannot be 0"));
1175 }
1176 if config.websocket.port == 0 {
1177 return Err(Error::generic("WebSocket port cannot be 0"));
1178 }
1179 if config.grpc.port == 0 {
1180 return Err(Error::generic("gRPC port cannot be 0"));
1181 }
1182 if config.admin.port == 0 {
1183 return Err(Error::generic("Admin port cannot be 0"));
1184 }
1185
1186 let ports = [
1188 ("HTTP", config.http.port),
1189 ("WebSocket", config.websocket.port),
1190 ("gRPC", config.grpc.port),
1191 ("Admin", config.admin.port),
1192 ];
1193
1194 for i in 0..ports.len() {
1195 for j in (i + 1)..ports.len() {
1196 if ports[i].1 == ports[j].1 {
1197 return Err(Error::generic(format!(
1198 "Port conflict: {} and {} both use port {}",
1199 ports[i].0, ports[j].0, ports[i].1
1200 )));
1201 }
1202 }
1203 }
1204
1205 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1207 if !valid_levels.contains(&config.logging.level.as_str()) {
1208 return Err(Error::generic(format!(
1209 "Invalid log level: {}. Valid levels: {}",
1210 config.logging.level,
1211 valid_levels.join(", ")
1212 )));
1213 }
1214
1215 Ok(())
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use super::*;
1221
1222 #[test]
1223 fn test_default_config() {
1224 let config = ServerConfig::default();
1225 assert_eq!(config.http.port, 3000);
1226 assert_eq!(config.websocket.port, 3001);
1227 assert_eq!(config.grpc.port, 50051);
1228 assert_eq!(config.admin.port, 9080);
1229 }
1230
1231 #[test]
1232 fn test_config_validation() {
1233 let mut config = ServerConfig::default();
1234 assert!(validate_config(&config).is_ok());
1235
1236 config.websocket.port = config.http.port;
1238 assert!(validate_config(&config).is_err());
1239
1240 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
1243 assert!(validate_config(&config).is_err());
1244 }
1245}