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)]
11#[serde(default)]
12pub struct AuthConfig {
13 pub jwt: Option<JwtConfig>,
15 pub oauth2: Option<OAuth2Config>,
17 pub basic_auth: Option<BasicAuthConfig>,
19 pub api_key: Option<ApiKeyConfig>,
21 pub require_auth: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct JwtConfig {
28 pub secret: Option<String>,
30 pub rsa_public_key: Option<String>,
32 pub ecdsa_public_key: Option<String>,
34 pub issuer: Option<String>,
36 pub audience: Option<String>,
38 pub algorithms: Vec<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct OAuth2Config {
45 pub client_id: String,
47 pub client_secret: String,
49 pub introspection_url: String,
51 pub auth_url: Option<String>,
53 pub token_url: Option<String>,
55 pub token_type_hint: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct BasicAuthConfig {
62 pub credentials: HashMap<String, String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ApiKeyConfig {
69 pub header_name: String,
71 pub query_name: Option<String>,
73 pub keys: Vec<String>,
75}
76
77impl Default for AuthConfig {
78 fn default() -> Self {
79 Self {
80 jwt: None,
81 oauth2: None,
82 basic_auth: None,
83 api_key: Some(ApiKeyConfig {
84 header_name: "X-API-Key".to_string(),
85 query_name: None,
86 keys: vec![],
87 }),
88 require_auth: false,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct RouteConfig {
96 pub path: String,
98 pub method: String,
100 pub request: Option<RouteRequestConfig>,
102 pub response: RouteResponseConfig,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct RouteRequestConfig {
109 pub validation: Option<RouteValidationConfig>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct RouteResponseConfig {
116 pub status: u16,
118 #[serde(default)]
120 pub headers: HashMap<String, String>,
121 pub body: Option<serde_json::Value>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RouteValidationConfig {
128 pub schema: serde_json::Value,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ProtocolConfig {
135 pub enabled: bool,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ProtocolsConfig {
142 pub http: ProtocolConfig,
144 pub graphql: ProtocolConfig,
146 pub grpc: ProtocolConfig,
148 pub websocket: ProtocolConfig,
150 pub smtp: ProtocolConfig,
152 pub mqtt: ProtocolConfig,
154 pub ftp: ProtocolConfig,
156 pub kafka: ProtocolConfig,
158 pub rabbitmq: ProtocolConfig,
160 pub amqp: ProtocolConfig,
162}
163
164impl Default for ProtocolsConfig {
165 fn default() -> Self {
166 Self {
167 http: ProtocolConfig { enabled: true },
168 graphql: ProtocolConfig { enabled: true },
169 grpc: ProtocolConfig { enabled: true },
170 websocket: ProtocolConfig { enabled: true },
171 smtp: ProtocolConfig { enabled: false },
172 mqtt: ProtocolConfig { enabled: true },
173 ftp: ProtocolConfig { enabled: false },
174 kafka: ProtocolConfig { enabled: false },
175 rabbitmq: ProtocolConfig { enabled: false },
176 amqp: ProtocolConfig { enabled: false },
177 }
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, Default)]
183#[serde(default)]
184pub struct ServerConfig {
185 pub http: HttpConfig,
187 pub websocket: WebSocketConfig,
189 pub grpc: GrpcConfig,
191 pub mqtt: MqttConfig,
193 pub smtp: SmtpConfig,
195 pub ftp: FtpConfig,
197 pub kafka: KafkaConfig,
199 pub amqp: AmqpConfig,
201 pub admin: AdminConfig,
203 pub chaining: ChainingConfig,
205 pub core: CoreConfig,
207 pub logging: LoggingConfig,
209 pub data: DataConfig,
211 pub observability: ObservabilityConfig,
213 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
215 #[serde(default)]
217 pub routes: Vec<RouteConfig>,
218 #[serde(default)]
220 pub protocols: ProtocolsConfig,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct HttpValidationConfig {
228 pub mode: String,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct HttpCorsConfig {
235 pub enabled: bool,
237 #[serde(default)]
239 pub allowed_origins: Vec<String>,
240 #[serde(default)]
242 pub allowed_methods: Vec<String>,
243 #[serde(default)]
245 pub allowed_headers: Vec<String>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(default)]
251pub struct HttpConfig {
252 pub enabled: bool,
254 pub port: u16,
256 pub host: String,
258 pub openapi_spec: Option<String>,
260 pub cors: Option<HttpCorsConfig>,
262 pub request_timeout_secs: u64,
264 pub validation: Option<HttpValidationConfig>,
266 pub aggregate_validation_errors: bool,
268 pub validate_responses: bool,
270 pub response_template_expand: bool,
272 pub validation_status: Option<u16>,
274 pub validation_overrides: std::collections::HashMap<String, String>,
276 pub skip_admin_validation: bool,
278 pub auth: Option<AuthConfig>,
280}
281
282impl Default for HttpConfig {
283 fn default() -> Self {
284 Self {
285 enabled: true,
286 port: 3000,
287 host: "0.0.0.0".to_string(),
288 openapi_spec: None,
289 cors: Some(HttpCorsConfig {
290 enabled: true,
291 allowed_origins: vec!["*".to_string()],
292 allowed_methods: vec![
293 "GET".to_string(),
294 "POST".to_string(),
295 "PUT".to_string(),
296 "DELETE".to_string(),
297 "PATCH".to_string(),
298 "OPTIONS".to_string(),
299 ],
300 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
301 }),
302 request_timeout_secs: 30,
303 validation: Some(HttpValidationConfig {
304 mode: "enforce".to_string(),
305 }),
306 aggregate_validation_errors: true,
307 validate_responses: false,
308 response_template_expand: false,
309 validation_status: None,
310 validation_overrides: std::collections::HashMap::new(),
311 skip_admin_validation: true,
312 auth: None,
313 }
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(default)]
320pub struct WebSocketConfig {
321 pub enabled: bool,
323 pub port: u16,
325 pub host: String,
327 pub replay_file: Option<String>,
329 pub connection_timeout_secs: u64,
331}
332
333impl Default for WebSocketConfig {
334 fn default() -> Self {
335 Self {
336 enabled: true,
337 port: 3001,
338 host: "0.0.0.0".to_string(),
339 replay_file: None,
340 connection_timeout_secs: 300,
341 }
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347#[serde(default)]
348pub struct GrpcConfig {
349 pub enabled: bool,
351 pub port: u16,
353 pub host: String,
355 pub proto_dir: Option<String>,
357 pub tls: Option<TlsConfig>,
359}
360
361impl Default for GrpcConfig {
362 fn default() -> Self {
363 Self {
364 enabled: true,
365 port: 50051,
366 host: "0.0.0.0".to_string(),
367 proto_dir: None,
368 tls: None,
369 }
370 }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct TlsConfig {
376 pub cert_path: String,
378 pub key_path: String,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384#[serde(default)]
385pub struct MqttConfig {
386 pub enabled: bool,
388 pub port: u16,
390 pub host: String,
392 pub max_connections: usize,
394 pub max_packet_size: usize,
396 pub keep_alive_secs: u16,
398 pub fixtures_dir: Option<std::path::PathBuf>,
400 pub enable_retained_messages: bool,
402 pub max_retained_messages: usize,
404}
405
406impl Default for MqttConfig {
407 fn default() -> Self {
408 Self {
409 enabled: false,
410 port: 1883,
411 host: "0.0.0.0".to_string(),
412 max_connections: 1000,
413 max_packet_size: 268435456, keep_alive_secs: 60,
415 fixtures_dir: None,
416 enable_retained_messages: true,
417 max_retained_messages: 10000,
418 }
419 }
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(default)]
425pub struct SmtpConfig {
426 pub enabled: bool,
428 pub port: u16,
430 pub host: String,
432 pub hostname: String,
434 pub fixtures_dir: Option<std::path::PathBuf>,
436 pub timeout_secs: u64,
438 pub max_connections: usize,
440 pub enable_mailbox: bool,
442 pub max_mailbox_messages: usize,
444 pub enable_starttls: bool,
446 pub tls_cert_path: Option<std::path::PathBuf>,
448 pub tls_key_path: Option<std::path::PathBuf>,
450}
451
452impl Default for SmtpConfig {
453 fn default() -> Self {
454 Self {
455 enabled: false,
456 port: 1025,
457 host: "0.0.0.0".to_string(),
458 hostname: "mockforge-smtp".to_string(),
459 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
460 timeout_secs: 300,
461 max_connections: 10,
462 enable_mailbox: true,
463 max_mailbox_messages: 1000,
464 enable_starttls: false,
465 tls_cert_path: None,
466 tls_key_path: None,
467 }
468 }
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
473#[serde(default)]
474pub struct FtpConfig {
475 pub enabled: bool,
477 pub port: u16,
479 pub host: String,
481 pub passive_ports: (u16, u16),
483 pub max_connections: usize,
485 pub timeout_secs: u64,
487 pub allow_anonymous: bool,
489 pub fixtures_dir: Option<std::path::PathBuf>,
491 pub virtual_root: std::path::PathBuf,
493}
494
495impl Default for FtpConfig {
496 fn default() -> Self {
497 Self {
498 enabled: false,
499 port: 2121,
500 host: "0.0.0.0".to_string(),
501 passive_ports: (50000, 51000),
502 max_connections: 100,
503 timeout_secs: 300,
504 allow_anonymous: true,
505 fixtures_dir: None,
506 virtual_root: std::path::PathBuf::from("/mockforge"),
507 }
508 }
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
513#[serde(default)]
514pub struct KafkaConfig {
515 pub enabled: bool,
517 pub port: u16,
519 pub host: String,
521 pub broker_id: i32,
523 pub max_connections: usize,
525 pub log_retention_ms: i64,
527 pub log_segment_bytes: i64,
529 pub fixtures_dir: Option<std::path::PathBuf>,
531 pub auto_create_topics: bool,
533 pub default_partitions: i32,
535 pub default_replication_factor: i16,
537}
538
539impl Default for KafkaConfig {
540 fn default() -> Self {
541 Self {
542 enabled: false,
543 port: 9092, host: "0.0.0.0".to_string(),
545 broker_id: 1,
546 max_connections: 1000,
547 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
550 auto_create_topics: true,
551 default_partitions: 3,
552 default_replication_factor: 1,
553 }
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559#[serde(default)]
560pub struct AmqpConfig {
561 pub enabled: bool,
563 pub port: u16,
565 pub host: String,
567 pub max_connections: usize,
569 pub max_channels_per_connection: u16,
571 pub frame_max: u32,
573 pub heartbeat_interval: u16,
575 pub fixtures_dir: Option<std::path::PathBuf>,
577 pub virtual_hosts: Vec<String>,
579}
580
581impl Default for AmqpConfig {
582 fn default() -> Self {
583 Self {
584 enabled: false,
585 port: 5672, host: "0.0.0.0".to_string(),
587 max_connections: 1000,
588 max_channels_per_connection: 100,
589 frame_max: 131072, heartbeat_interval: 60,
591 fixtures_dir: None,
592 virtual_hosts: vec!["/".to_string()],
593 }
594 }
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
599#[serde(default)]
600pub struct AdminConfig {
601 pub enabled: bool,
603 pub port: u16,
605 pub host: String,
607 pub auth_required: bool,
609 pub username: Option<String>,
611 pub password: Option<String>,
613 pub mount_path: Option<String>,
615 pub api_enabled: bool,
617 pub prometheus_url: String,
619}
620
621impl Default for AdminConfig {
622 fn default() -> Self {
623 Self {
624 enabled: false,
625 port: 9080,
626 host: "127.0.0.1".to_string(),
627 auth_required: false,
628 username: None,
629 password: None,
630 mount_path: None,
631 api_enabled: true,
632 prometheus_url: "http://localhost:9090".to_string(),
633 }
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
639#[serde(default)]
640pub struct LoggingConfig {
641 pub level: String,
643 pub json_format: bool,
645 pub file_path: Option<String>,
647 pub max_file_size_mb: u64,
649 pub max_files: u32,
651}
652
653impl Default for LoggingConfig {
654 fn default() -> Self {
655 Self {
656 level: "info".to_string(),
657 json_format: false,
658 file_path: None,
659 max_file_size_mb: 10,
660 max_files: 5,
661 }
662 }
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
666#[serde(default, rename_all = "camelCase")]
667pub struct ChainingConfig {
668 pub enabled: bool,
670 pub max_chain_length: usize,
672 pub global_timeout_secs: u64,
674 pub enable_parallel_execution: bool,
676}
677
678impl Default for ChainingConfig {
679 fn default() -> Self {
680 Self {
681 enabled: false,
682 max_chain_length: 20,
683 global_timeout_secs: 300,
684 enable_parallel_execution: false,
685 }
686 }
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
691#[serde(default)]
692pub struct DataConfig {
693 pub default_rows: usize,
695 pub default_format: String,
697 pub locale: String,
699 pub templates: HashMap<String, String>,
701 pub rag: RagConfig,
703}
704
705impl Default for DataConfig {
706 fn default() -> Self {
707 Self {
708 default_rows: 100,
709 default_format: "json".to_string(),
710 locale: "en".to_string(),
711 templates: HashMap::new(),
712 rag: RagConfig::default(),
713 }
714 }
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize)]
719#[serde(default)]
720pub struct RagConfig {
721 pub enabled: bool,
723 #[serde(default)]
725 pub provider: String,
726 pub api_endpoint: Option<String>,
728 pub api_key: Option<String>,
730 pub model: Option<String>,
732 #[serde(default = "default_max_tokens")]
734 pub max_tokens: usize,
735 #[serde(default = "default_temperature")]
737 pub temperature: f64,
738 pub context_window: usize,
740 #[serde(default = "default_true")]
742 pub caching: bool,
743 #[serde(default = "default_cache_ttl")]
745 pub cache_ttl_secs: u64,
746 #[serde(default = "default_timeout")]
748 pub timeout_secs: u64,
749 #[serde(default = "default_max_retries")]
751 pub max_retries: usize,
752}
753
754fn default_max_tokens() -> usize {
755 1024
756}
757
758fn default_temperature() -> f64 {
759 0.7
760}
761
762fn default_true() -> bool {
763 true
764}
765
766fn default_cache_ttl() -> u64 {
767 3600
768}
769
770fn default_timeout() -> u64 {
771 30
772}
773
774fn default_max_retries() -> usize {
775 3
776}
777
778impl Default for RagConfig {
779 fn default() -> Self {
780 Self {
781 enabled: false,
782 provider: "openai".to_string(),
783 api_endpoint: None,
784 api_key: None,
785 model: Some("gpt-3.5-turbo".to_string()),
786 max_tokens: default_max_tokens(),
787 temperature: default_temperature(),
788 context_window: 4000,
789 caching: default_true(),
790 cache_ttl_secs: default_cache_ttl(),
791 timeout_secs: default_timeout(),
792 max_retries: default_max_retries(),
793 }
794 }
795}
796
797#[derive(Debug, Clone, Serialize, Deserialize, Default)]
799#[serde(default)]
800pub struct ObservabilityConfig {
801 pub prometheus: PrometheusConfig,
803 pub opentelemetry: Option<OpenTelemetryConfig>,
805 pub recorder: Option<RecorderConfig>,
807 pub chaos: Option<ChaosEngConfig>,
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize)]
813#[serde(default)]
814pub struct PrometheusConfig {
815 pub enabled: bool,
817 pub port: u16,
819 pub host: String,
821 pub path: String,
823}
824
825impl Default for PrometheusConfig {
826 fn default() -> Self {
827 Self {
828 enabled: true,
829 port: 9090,
830 host: "0.0.0.0".to_string(),
831 path: "/metrics".to_string(),
832 }
833 }
834}
835
836#[derive(Debug, Clone, Serialize, Deserialize)]
838#[serde(default)]
839pub struct OpenTelemetryConfig {
840 pub enabled: bool,
842 pub service_name: String,
844 pub environment: String,
846 pub jaeger_endpoint: String,
848 pub otlp_endpoint: Option<String>,
850 pub protocol: String,
852 pub sampling_rate: f64,
854}
855
856impl Default for OpenTelemetryConfig {
857 fn default() -> Self {
858 Self {
859 enabled: false,
860 service_name: "mockforge".to_string(),
861 environment: "development".to_string(),
862 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
863 otlp_endpoint: Some("http://localhost:4317".to_string()),
864 protocol: "grpc".to_string(),
865 sampling_rate: 1.0,
866 }
867 }
868}
869
870#[derive(Debug, Clone, Serialize, Deserialize)]
872#[serde(default)]
873pub struct RecorderConfig {
874 pub enabled: bool,
876 pub database_path: String,
878 pub api_enabled: bool,
880 pub api_port: Option<u16>,
882 pub max_requests: i64,
884 pub retention_days: i64,
886 pub record_http: bool,
888 pub record_grpc: bool,
890 pub record_websocket: bool,
892 pub record_graphql: bool,
894}
895
896impl Default for RecorderConfig {
897 fn default() -> Self {
898 Self {
899 enabled: false,
900 database_path: "./mockforge-recordings.db".to_string(),
901 api_enabled: true,
902 api_port: None,
903 max_requests: 10000,
904 retention_days: 7,
905 record_http: true,
906 record_grpc: true,
907 record_websocket: true,
908 record_graphql: true,
909 }
910 }
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize, Default)]
915#[serde(default)]
916pub struct ChaosEngConfig {
917 pub enabled: bool,
919 pub latency: Option<LatencyInjectionConfig>,
921 pub fault_injection: Option<FaultConfig>,
923 pub rate_limit: Option<RateLimitingConfig>,
925 pub traffic_shaping: Option<NetworkShapingConfig>,
927 pub scenario: Option<String>,
929}
930
931#[derive(Debug, Clone, Serialize, Deserialize)]
933pub struct LatencyInjectionConfig {
934 pub enabled: bool,
935 pub fixed_delay_ms: Option<u64>,
936 pub random_delay_range_ms: Option<(u64, u64)>,
937 pub jitter_percent: f64,
938 pub probability: f64,
939}
940
941#[derive(Debug, Clone, Serialize, Deserialize)]
943pub struct FaultConfig {
944 pub enabled: bool,
945 pub http_errors: Vec<u16>,
946 pub http_error_probability: f64,
947 pub connection_errors: bool,
948 pub connection_error_probability: f64,
949 pub timeout_errors: bool,
950 pub timeout_ms: u64,
951 pub timeout_probability: f64,
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize)]
956pub struct RateLimitingConfig {
957 pub enabled: bool,
958 pub requests_per_second: u32,
959 pub burst_size: u32,
960 pub per_ip: bool,
961 pub per_endpoint: bool,
962}
963
964#[derive(Debug, Clone, Serialize, Deserialize)]
966pub struct NetworkShapingConfig {
967 pub enabled: bool,
968 pub bandwidth_limit_bps: u64,
969 pub packet_loss_percent: f64,
970 pub max_connections: u32,
971}
972
973pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
975 let content = fs::read_to_string(&path)
976 .await
977 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
978
979 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
980 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
981 {
982 serde_yaml::from_str(&content)
983 .map_err(|e| Error::generic(format!("Failed to parse YAML config: {}", e)))?
984 } else {
985 serde_json::from_str(&content)
986 .map_err(|e| Error::generic(format!("Failed to parse JSON config: {}", e)))?
987 };
988
989 Ok(config)
990}
991
992pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
994 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
995 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
996 {
997 serde_yaml::to_string(config)
998 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
999 } else {
1000 serde_json::to_string_pretty(config)
1001 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1002 };
1003
1004 fs::write(path, content)
1005 .await
1006 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1007
1008 Ok(())
1009}
1010
1011pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1013 match load_config(&path).await {
1014 Ok(config) => {
1015 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1016 config
1017 }
1018 Err(e) => {
1019 tracing::warn!(
1020 "Failed to load config from {:?}: {}. Using defaults.",
1021 path.as_ref(),
1022 e
1023 );
1024 ServerConfig::default()
1025 }
1026 }
1027}
1028
1029pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1031 let config = ServerConfig::default();
1032 save_config(path, &config).await?;
1033 Ok(())
1034}
1035
1036pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1038 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1040 if let Ok(port_num) = port.parse() {
1041 config.http.port = port_num;
1042 }
1043 }
1044
1045 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1046 config.http.host = host;
1047 }
1048
1049 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1051 if let Ok(port_num) = port.parse() {
1052 config.websocket.port = port_num;
1053 }
1054 }
1055
1056 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1058 if let Ok(port_num) = port.parse() {
1059 config.grpc.port = port_num;
1060 }
1061 }
1062
1063 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1065 if let Ok(port_num) = port.parse() {
1066 config.smtp.port = port_num;
1067 }
1068 }
1069
1070 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1071 config.smtp.host = host;
1072 }
1073
1074 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1075 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1076 }
1077
1078 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1079 config.smtp.hostname = hostname;
1080 }
1081
1082 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1084 if let Ok(port_num) = port.parse() {
1085 config.admin.port = port_num;
1086 }
1087 }
1088
1089 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1090 config.admin.enabled = true;
1091 }
1092
1093 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1094 if !mount_path.trim().is_empty() {
1095 config.admin.mount_path = Some(mount_path);
1096 }
1097 }
1098
1099 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1100 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1101 config.admin.api_enabled = on;
1102 }
1103
1104 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1105 config.admin.prometheus_url = prometheus_url;
1106 }
1107
1108 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1110 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1111 config.core.latency_enabled = enabled;
1112 }
1113
1114 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1115 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1116 config.core.failures_enabled = enabled;
1117 }
1118
1119 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1120 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1121 config.core.overrides_enabled = enabled;
1122 }
1123
1124 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1125 let enabled =
1126 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1127 config.core.traffic_shaping_enabled = enabled;
1128 }
1129
1130 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1132 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1133 config.core.traffic_shaping.bandwidth.enabled = enabled;
1134 }
1135
1136 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1137 if let Ok(bytes) = max_bytes_per_sec.parse() {
1138 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1139 config.core.traffic_shaping.bandwidth.enabled = true;
1140 }
1141 }
1142
1143 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1144 if let Ok(bytes) = burst_capacity.parse() {
1145 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1146 }
1147 }
1148
1149 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1150 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1151 config.core.traffic_shaping.burst_loss.enabled = enabled;
1152 }
1153
1154 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1155 if let Ok(prob) = burst_probability.parse::<f64>() {
1156 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1157 config.core.traffic_shaping.burst_loss.enabled = true;
1158 }
1159 }
1160
1161 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1162 if let Ok(ms) = burst_duration.parse() {
1163 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1164 }
1165 }
1166
1167 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1168 if let Ok(rate) = loss_rate.parse::<f64>() {
1169 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1170 }
1171 }
1172
1173 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1174 if let Ok(ms) = recovery_time.parse() {
1175 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1176 }
1177 }
1178
1179 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1181 config.logging.level = level;
1182 }
1183
1184 config
1185}
1186
1187pub fn validate_config(config: &ServerConfig) -> Result<()> {
1189 if config.http.port == 0 {
1191 return Err(Error::generic("HTTP port cannot be 0"));
1192 }
1193 if config.websocket.port == 0 {
1194 return Err(Error::generic("WebSocket port cannot be 0"));
1195 }
1196 if config.grpc.port == 0 {
1197 return Err(Error::generic("gRPC port cannot be 0"));
1198 }
1199 if config.admin.port == 0 {
1200 return Err(Error::generic("Admin port cannot be 0"));
1201 }
1202
1203 let ports = [
1205 ("HTTP", config.http.port),
1206 ("WebSocket", config.websocket.port),
1207 ("gRPC", config.grpc.port),
1208 ("Admin", config.admin.port),
1209 ];
1210
1211 for i in 0..ports.len() {
1212 for j in (i + 1)..ports.len() {
1213 if ports[i].1 == ports[j].1 {
1214 return Err(Error::generic(format!(
1215 "Port conflict: {} and {} both use port {}",
1216 ports[i].0, ports[j].0, ports[i].1
1217 )));
1218 }
1219 }
1220 }
1221
1222 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1224 if !valid_levels.contains(&config.logging.level.as_str()) {
1225 return Err(Error::generic(format!(
1226 "Invalid log level: {}. Valid levels: {}",
1227 config.logging.level,
1228 valid_levels.join(", ")
1229 )));
1230 }
1231
1232 Ok(())
1233}
1234
1235#[cfg(test)]
1236mod tests {
1237 use super::*;
1238
1239 #[test]
1240 fn test_default_config() {
1241 let config = ServerConfig::default();
1242 assert_eq!(config.http.port, 3000);
1243 assert_eq!(config.websocket.port, 3001);
1244 assert_eq!(config.grpc.port, 50051);
1245 assert_eq!(config.admin.port, 9080);
1246 }
1247
1248 #[test]
1249 fn test_config_validation() {
1250 let mut config = ServerConfig::default();
1251 assert!(validate_config(&config).is_ok());
1252
1253 config.websocket.port = config.http.port;
1255 assert!(validate_config(&config).is_err());
1256
1257 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
1260 assert!(validate_config(&config).is_err());
1261 }
1262}