1use crate::{Config as CoreConfig, Error, RealityLevel, 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 #[serde(default)]
106 pub fault_injection: Option<RouteFaultInjectionConfig>,
107 #[serde(default)]
109 pub latency: Option<RouteLatencyConfig>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RouteRequestConfig {
115 pub validation: Option<RouteValidationConfig>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct RouteResponseConfig {
122 pub status: u16,
124 #[serde(default)]
126 pub headers: HashMap<String, String>,
127 pub body: Option<serde_json::Value>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RouteValidationConfig {
134 pub schema: serde_json::Value,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct RouteFaultInjectionConfig {
141 pub enabled: bool,
143 pub probability: f64,
145 pub fault_types: Vec<RouteFaultType>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(tag = "type", rename_all = "snake_case")]
152pub enum RouteFaultType {
153 HttpError {
155 status_code: u16,
157 message: Option<String>,
159 },
160 ConnectionError {
162 message: Option<String>,
164 },
165 Timeout {
167 duration_ms: u64,
169 message: Option<String>,
171 },
172 PartialResponse {
174 truncate_percent: f64,
176 },
177 PayloadCorruption {
179 corruption_type: String,
181 },
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct RouteLatencyConfig {
187 pub enabled: bool,
189 pub probability: f64,
191 pub fixed_delay_ms: Option<u64>,
193 pub random_delay_range_ms: Option<(u64, u64)>,
195 pub jitter_percent: f64,
197 #[serde(default)]
199 pub distribution: LatencyDistribution,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
204#[serde(rename_all = "snake_case")]
205pub enum LatencyDistribution {
206 Fixed,
208 Normal {
210 mean_ms: f64,
212 std_dev_ms: f64,
214 },
215 Exponential {
217 lambda: f64,
219 },
220 Uniform,
222}
223
224impl Default for LatencyDistribution {
225 fn default() -> Self {
226 Self::Fixed
227 }
228}
229
230impl Default for RouteFaultInjectionConfig {
231 fn default() -> Self {
232 Self {
233 enabled: false,
234 probability: 0.0,
235 fault_types: Vec::new(),
236 }
237 }
238}
239
240impl Default for RouteLatencyConfig {
241 fn default() -> Self {
242 Self {
243 enabled: false,
244 probability: 1.0,
245 fixed_delay_ms: None,
246 random_delay_range_ms: None,
247 jitter_percent: 0.0,
248 distribution: LatencyDistribution::Fixed,
249 }
250 }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(default)]
256pub struct DeceptiveDeployConfig {
257 pub enabled: bool,
259 pub cors: Option<ProductionCorsConfig>,
261 pub rate_limit: Option<ProductionRateLimitConfig>,
263 #[serde(default)]
265 pub headers: HashMap<String, String>,
266 pub oauth: Option<ProductionOAuthConfig>,
268 pub custom_domain: Option<String>,
270 pub auto_tunnel: bool,
272}
273
274impl Default for DeceptiveDeployConfig {
275 fn default() -> Self {
276 Self {
277 enabled: false,
278 cors: None,
279 rate_limit: None,
280 headers: HashMap::new(),
281 oauth: None,
282 custom_domain: None,
283 auto_tunnel: false,
284 }
285 }
286}
287
288impl DeceptiveDeployConfig {
289 pub fn production_preset() -> Self {
291 let mut headers = HashMap::new();
292 headers.insert("X-API-Version".to_string(), "1.0".to_string());
293 headers.insert("X-Request-ID".to_string(), "{{uuid}}".to_string());
294 headers.insert("X-Powered-By".to_string(), "MockForge".to_string());
295
296 Self {
297 enabled: true,
298 cors: Some(ProductionCorsConfig {
299 allowed_origins: vec!["*".to_string()],
300 allowed_methods: vec![
301 "GET".to_string(),
302 "POST".to_string(),
303 "PUT".to_string(),
304 "DELETE".to_string(),
305 "PATCH".to_string(),
306 "OPTIONS".to_string(),
307 ],
308 allowed_headers: vec!["*".to_string()],
309 allow_credentials: true,
310 }),
311 rate_limit: Some(ProductionRateLimitConfig {
312 requests_per_minute: 1000,
313 burst: 2000,
314 per_ip: true,
315 }),
316 headers,
317 oauth: None, custom_domain: None,
319 auto_tunnel: true,
320 }
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct ProductionCorsConfig {
327 #[serde(default)]
329 pub allowed_origins: Vec<String>,
330 #[serde(default)]
332 pub allowed_methods: Vec<String>,
333 #[serde(default)]
335 pub allowed_headers: Vec<String>,
336 pub allow_credentials: bool,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct ProductionRateLimitConfig {
343 pub requests_per_minute: u32,
345 pub burst: u32,
347 pub per_ip: bool,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ProductionOAuthConfig {
354 pub client_id: String,
356 pub client_secret: String,
358 pub introspection_url: String,
360 pub auth_url: Option<String>,
362 pub token_url: Option<String>,
364 pub token_type_hint: Option<String>,
366}
367
368impl From<ProductionOAuthConfig> for OAuth2Config {
369 fn from(prod: ProductionOAuthConfig) -> Self {
371 OAuth2Config {
372 client_id: prod.client_id,
373 client_secret: prod.client_secret,
374 introspection_url: prod.introspection_url,
375 auth_url: prod.auth_url,
376 token_url: prod.token_url,
377 token_type_hint: prod.token_type_hint,
378 }
379 }
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct ProtocolConfig {
385 pub enabled: bool,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ProtocolsConfig {
392 pub http: ProtocolConfig,
394 pub graphql: ProtocolConfig,
396 pub grpc: ProtocolConfig,
398 pub websocket: ProtocolConfig,
400 pub smtp: ProtocolConfig,
402 pub mqtt: ProtocolConfig,
404 pub ftp: ProtocolConfig,
406 pub kafka: ProtocolConfig,
408 pub rabbitmq: ProtocolConfig,
410 pub amqp: ProtocolConfig,
412 pub tcp: ProtocolConfig,
414}
415
416impl Default for ProtocolsConfig {
417 fn default() -> Self {
418 Self {
419 http: ProtocolConfig { enabled: true },
420 graphql: ProtocolConfig { enabled: true },
421 grpc: ProtocolConfig { enabled: true },
422 websocket: ProtocolConfig { enabled: true },
423 smtp: ProtocolConfig { enabled: false },
424 mqtt: ProtocolConfig { enabled: true },
425 ftp: ProtocolConfig { enabled: false },
426 kafka: ProtocolConfig { enabled: false },
427 rabbitmq: ProtocolConfig { enabled: false },
428 amqp: ProtocolConfig { enabled: false },
429 tcp: ProtocolConfig { enabled: false },
430 }
431 }
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(default)]
441pub struct RealitySliderConfig {
442 pub level: RealityLevel,
444 pub enabled: bool,
446}
447
448impl Default for RealitySliderConfig {
449 fn default() -> Self {
450 Self {
451 level: RealityLevel::ModerateRealism,
452 enabled: true,
453 }
454 }
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, Default)]
459#[serde(default)]
460pub struct ServerConfig {
461 pub http: HttpConfig,
463 pub websocket: WebSocketConfig,
465 pub graphql: GraphQLConfig,
467 pub grpc: GrpcConfig,
469 pub mqtt: MqttConfig,
471 pub smtp: SmtpConfig,
473 pub ftp: FtpConfig,
475 pub kafka: KafkaConfig,
477 pub amqp: AmqpConfig,
479 pub tcp: TcpConfig,
481 pub admin: AdminConfig,
483 pub chaining: ChainingConfig,
485 pub core: CoreConfig,
487 pub logging: LoggingConfig,
489 pub data: DataConfig,
491 #[serde(default)]
493 pub mockai: MockAIConfig,
494 pub observability: ObservabilityConfig,
496 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
498 #[serde(default)]
500 pub routes: Vec<RouteConfig>,
501 #[serde(default)]
503 pub protocols: ProtocolsConfig,
504 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
506 pub profiles: HashMap<String, ProfileConfig>,
507 #[serde(default)]
509 pub deceptive_deploy: DeceptiveDeployConfig,
510 #[serde(default)]
512 pub reality: RealitySliderConfig,
513 #[serde(default)]
515 pub reality_continuum: crate::reality_continuum::ContinuumConfig,
516 #[serde(default)]
518 pub security: SecurityConfig,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, Default)]
523#[serde(default)]
524pub struct ProfileConfig {
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub http: Option<HttpConfig>,
528 #[serde(skip_serializing_if = "Option::is_none")]
530 pub websocket: Option<WebSocketConfig>,
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub graphql: Option<GraphQLConfig>,
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub grpc: Option<GrpcConfig>,
537 #[serde(skip_serializing_if = "Option::is_none")]
539 pub mqtt: Option<MqttConfig>,
540 #[serde(skip_serializing_if = "Option::is_none")]
542 pub smtp: Option<SmtpConfig>,
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub ftp: Option<FtpConfig>,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub kafka: Option<KafkaConfig>,
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub amqp: Option<AmqpConfig>,
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub tcp: Option<TcpConfig>,
555 #[serde(skip_serializing_if = "Option::is_none")]
557 pub admin: Option<AdminConfig>,
558 #[serde(skip_serializing_if = "Option::is_none")]
560 pub chaining: Option<ChainingConfig>,
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub core: Option<CoreConfig>,
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub logging: Option<LoggingConfig>,
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub data: Option<DataConfig>,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub mockai: Option<MockAIConfig>,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub observability: Option<ObservabilityConfig>,
576 #[serde(skip_serializing_if = "Option::is_none")]
578 pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
579 #[serde(skip_serializing_if = "Option::is_none")]
581 pub routes: Option<Vec<RouteConfig>>,
582 #[serde(skip_serializing_if = "Option::is_none")]
584 pub protocols: Option<ProtocolsConfig>,
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub deceptive_deploy: Option<DeceptiveDeployConfig>,
588 #[serde(skip_serializing_if = "Option::is_none")]
590 pub reality: Option<RealitySliderConfig>,
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub security: Option<SecurityConfig>,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct HttpValidationConfig {
604 pub mode: String,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize, Default)]
610pub struct HttpCorsConfig {
611 pub enabled: bool,
613 #[serde(default)]
615 pub allowed_origins: Vec<String>,
616 #[serde(default)]
618 pub allowed_methods: Vec<String>,
619 #[serde(default)]
621 pub allowed_headers: Vec<String>,
622 #[serde(default = "default_cors_allow_credentials")]
625 pub allow_credentials: bool,
626}
627
628fn default_cors_allow_credentials() -> bool {
629 false
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634#[serde(default)]
635pub struct HttpConfig {
636 pub enabled: bool,
638 pub port: u16,
640 pub host: String,
642 pub openapi_spec: Option<String>,
644 pub cors: Option<HttpCorsConfig>,
646 pub request_timeout_secs: u64,
648 pub validation: Option<HttpValidationConfig>,
650 pub aggregate_validation_errors: bool,
652 pub validate_responses: bool,
654 pub response_template_expand: bool,
656 pub validation_status: Option<u16>,
658 pub validation_overrides: std::collections::HashMap<String, String>,
660 pub skip_admin_validation: bool,
662 pub auth: Option<AuthConfig>,
664 #[serde(skip_serializing_if = "Option::is_none")]
666 pub tls: Option<HttpTlsConfig>,
667}
668
669impl Default for HttpConfig {
670 fn default() -> Self {
671 Self {
672 enabled: true,
673 port: 3000,
674 host: "0.0.0.0".to_string(),
675 openapi_spec: None,
676 cors: Some(HttpCorsConfig {
677 enabled: true,
678 allowed_origins: vec!["*".to_string()],
679 allowed_methods: vec![
680 "GET".to_string(),
681 "POST".to_string(),
682 "PUT".to_string(),
683 "DELETE".to_string(),
684 "PATCH".to_string(),
685 "OPTIONS".to_string(),
686 ],
687 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
688 allow_credentials: false, }),
690 request_timeout_secs: 30,
691 validation: Some(HttpValidationConfig {
692 mode: "enforce".to_string(),
693 }),
694 aggregate_validation_errors: true,
695 validate_responses: false,
696 response_template_expand: false,
697 validation_status: None,
698 validation_overrides: std::collections::HashMap::new(),
699 skip_admin_validation: true,
700 auth: None,
701 tls: None,
702 }
703 }
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct HttpTlsConfig {
709 pub enabled: bool,
711 pub cert_file: String,
713 pub key_file: String,
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub ca_file: Option<String>,
718 #[serde(default = "default_tls_min_version")]
720 pub min_version: String,
721 #[serde(default, skip_serializing_if = "Vec::is_empty")]
723 pub cipher_suites: Vec<String>,
724 #[serde(default)]
726 pub require_client_cert: bool,
727}
728
729fn default_tls_min_version() -> String {
730 "1.2".to_string()
731}
732
733impl Default for HttpTlsConfig {
734 fn default() -> Self {
735 Self {
736 enabled: true,
737 cert_file: String::new(),
738 key_file: String::new(),
739 ca_file: None,
740 min_version: "1.2".to_string(),
741 cipher_suites: Vec::new(),
742 require_client_cert: false,
743 }
744 }
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize)]
749#[serde(default)]
750pub struct WebSocketConfig {
751 pub enabled: bool,
753 pub port: u16,
755 pub host: String,
757 pub replay_file: Option<String>,
759 pub connection_timeout_secs: u64,
761}
762
763impl Default for WebSocketConfig {
764 fn default() -> Self {
765 Self {
766 enabled: true,
767 port: 3001,
768 host: "0.0.0.0".to_string(),
769 replay_file: None,
770 connection_timeout_secs: 300,
771 }
772 }
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize)]
777#[serde(default)]
778pub struct GrpcConfig {
779 pub enabled: bool,
781 pub port: u16,
783 pub host: String,
785 pub proto_dir: Option<String>,
787 pub tls: Option<TlsConfig>,
789}
790
791impl Default for GrpcConfig {
792 fn default() -> Self {
793 Self {
794 enabled: true,
795 port: 50051,
796 host: "0.0.0.0".to_string(),
797 proto_dir: None,
798 tls: None,
799 }
800 }
801}
802
803#[derive(Debug, Clone, Serialize, Deserialize)]
805#[serde(default)]
806pub struct GraphQLConfig {
807 pub enabled: bool,
809 pub port: u16,
811 pub host: String,
813 pub schema_path: Option<String>,
815 pub handlers_dir: Option<String>,
817 pub playground_enabled: bool,
819 pub upstream_url: Option<String>,
821 pub introspection_enabled: bool,
823}
824
825impl Default for GraphQLConfig {
826 fn default() -> Self {
827 Self {
828 enabled: true,
829 port: 4000,
830 host: "0.0.0.0".to_string(),
831 schema_path: None,
832 handlers_dir: None,
833 playground_enabled: true,
834 upstream_url: None,
835 introspection_enabled: true,
836 }
837 }
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct TlsConfig {
843 pub cert_path: String,
845 pub key_path: String,
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
851#[serde(default)]
852pub struct MqttConfig {
853 pub enabled: bool,
855 pub port: u16,
857 pub host: String,
859 pub max_connections: usize,
861 pub max_packet_size: usize,
863 pub keep_alive_secs: u16,
865 pub fixtures_dir: Option<std::path::PathBuf>,
867 pub enable_retained_messages: bool,
869 pub max_retained_messages: usize,
871}
872
873impl Default for MqttConfig {
874 fn default() -> Self {
875 Self {
876 enabled: false,
877 port: 1883,
878 host: "0.0.0.0".to_string(),
879 max_connections: 1000,
880 max_packet_size: 268435456, keep_alive_secs: 60,
882 fixtures_dir: None,
883 enable_retained_messages: true,
884 max_retained_messages: 10000,
885 }
886 }
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize)]
891#[serde(default)]
892pub struct SmtpConfig {
893 pub enabled: bool,
895 pub port: u16,
897 pub host: String,
899 pub hostname: String,
901 pub fixtures_dir: Option<std::path::PathBuf>,
903 pub timeout_secs: u64,
905 pub max_connections: usize,
907 pub enable_mailbox: bool,
909 pub max_mailbox_messages: usize,
911 pub enable_starttls: bool,
913 pub tls_cert_path: Option<std::path::PathBuf>,
915 pub tls_key_path: Option<std::path::PathBuf>,
917}
918
919impl Default for SmtpConfig {
920 fn default() -> Self {
921 Self {
922 enabled: false,
923 port: 1025,
924 host: "0.0.0.0".to_string(),
925 hostname: "mockforge-smtp".to_string(),
926 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
927 timeout_secs: 300,
928 max_connections: 10,
929 enable_mailbox: true,
930 max_mailbox_messages: 1000,
931 enable_starttls: false,
932 tls_cert_path: None,
933 tls_key_path: None,
934 }
935 }
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
940#[serde(default)]
941pub struct FtpConfig {
942 pub enabled: bool,
944 pub port: u16,
946 pub host: String,
948 pub passive_ports: (u16, u16),
950 pub max_connections: usize,
952 pub timeout_secs: u64,
954 pub allow_anonymous: bool,
956 pub fixtures_dir: Option<std::path::PathBuf>,
958 pub virtual_root: std::path::PathBuf,
960}
961
962impl Default for FtpConfig {
963 fn default() -> Self {
964 Self {
965 enabled: false,
966 port: 2121,
967 host: "0.0.0.0".to_string(),
968 passive_ports: (50000, 51000),
969 max_connections: 100,
970 timeout_secs: 300,
971 allow_anonymous: true,
972 fixtures_dir: None,
973 virtual_root: std::path::PathBuf::from("/mockforge"),
974 }
975 }
976}
977
978#[derive(Debug, Clone, Serialize, Deserialize)]
980#[serde(default)]
981pub struct KafkaConfig {
982 pub enabled: bool,
984 pub port: u16,
986 pub host: String,
988 pub broker_id: i32,
990 pub max_connections: usize,
992 pub log_retention_ms: i64,
994 pub log_segment_bytes: i64,
996 pub fixtures_dir: Option<std::path::PathBuf>,
998 pub auto_create_topics: bool,
1000 pub default_partitions: i32,
1002 pub default_replication_factor: i16,
1004}
1005
1006impl Default for KafkaConfig {
1007 fn default() -> Self {
1008 Self {
1009 enabled: false,
1010 port: 9092, host: "0.0.0.0".to_string(),
1012 broker_id: 1,
1013 max_connections: 1000,
1014 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
1017 auto_create_topics: true,
1018 default_partitions: 3,
1019 default_replication_factor: 1,
1020 }
1021 }
1022}
1023
1024#[derive(Debug, Clone, Serialize, Deserialize)]
1026#[serde(default)]
1027pub struct AmqpConfig {
1028 pub enabled: bool,
1030 pub port: u16,
1032 pub host: String,
1034 pub max_connections: usize,
1036 pub max_channels_per_connection: u16,
1038 pub frame_max: u32,
1040 pub heartbeat_interval: u16,
1042 pub fixtures_dir: Option<std::path::PathBuf>,
1044 pub virtual_hosts: Vec<String>,
1046}
1047
1048impl Default for AmqpConfig {
1049 fn default() -> Self {
1050 Self {
1051 enabled: false,
1052 port: 5672, host: "0.0.0.0".to_string(),
1054 max_connections: 1000,
1055 max_channels_per_connection: 100,
1056 frame_max: 131072, heartbeat_interval: 60,
1058 fixtures_dir: None,
1059 virtual_hosts: vec!["/".to_string()],
1060 }
1061 }
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize)]
1066#[serde(default)]
1067pub struct TcpConfig {
1068 pub enabled: bool,
1070 pub port: u16,
1072 pub host: String,
1074 pub max_connections: usize,
1076 pub timeout_secs: u64,
1078 pub fixtures_dir: Option<std::path::PathBuf>,
1080 pub echo_mode: bool,
1082 pub enable_tls: bool,
1084 pub tls_cert_path: Option<std::path::PathBuf>,
1086 pub tls_key_path: Option<std::path::PathBuf>,
1088}
1089
1090impl Default for TcpConfig {
1091 fn default() -> Self {
1092 Self {
1093 enabled: false,
1094 port: 9999,
1095 host: "0.0.0.0".to_string(),
1096 max_connections: 1000,
1097 timeout_secs: 300,
1098 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/tcp")),
1099 echo_mode: true,
1100 enable_tls: false,
1101 tls_cert_path: None,
1102 tls_key_path: None,
1103 }
1104 }
1105}
1106
1107#[derive(Debug, Clone, Serialize, Deserialize)]
1109#[serde(default)]
1110pub struct AdminConfig {
1111 pub enabled: bool,
1113 pub port: u16,
1115 pub host: String,
1117 pub auth_required: bool,
1119 pub username: Option<String>,
1121 pub password: Option<String>,
1123 pub mount_path: Option<String>,
1125 pub api_enabled: bool,
1127 pub prometheus_url: String,
1129}
1130
1131impl Default for AdminConfig {
1132 fn default() -> Self {
1133 let default_host = if std::env::var("DOCKER_CONTAINER").is_ok()
1136 || std::env::var("container").is_ok()
1137 || std::path::Path::new("/.dockerenv").exists()
1138 {
1139 "0.0.0.0".to_string()
1140 } else {
1141 "127.0.0.1".to_string()
1142 };
1143
1144 Self {
1145 enabled: false,
1146 port: 9080,
1147 host: default_host,
1148 auth_required: false,
1149 username: None,
1150 password: None,
1151 mount_path: None,
1152 api_enabled: true,
1153 prometheus_url: "http://localhost:9090".to_string(),
1154 }
1155 }
1156}
1157
1158#[derive(Debug, Clone, Serialize, Deserialize)]
1160#[serde(default)]
1161pub struct LoggingConfig {
1162 pub level: String,
1164 pub json_format: bool,
1166 pub file_path: Option<String>,
1168 pub max_file_size_mb: u64,
1170 pub max_files: u32,
1172}
1173
1174impl Default for LoggingConfig {
1175 fn default() -> Self {
1176 Self {
1177 level: "info".to_string(),
1178 json_format: false,
1179 file_path: None,
1180 max_file_size_mb: 10,
1181 max_files: 5,
1182 }
1183 }
1184}
1185
1186#[derive(Debug, Clone, Serialize, Deserialize)]
1188#[serde(default, rename_all = "camelCase")]
1189pub struct ChainingConfig {
1190 pub enabled: bool,
1192 pub max_chain_length: usize,
1194 pub global_timeout_secs: u64,
1196 pub enable_parallel_execution: bool,
1198}
1199
1200impl Default for ChainingConfig {
1201 fn default() -> Self {
1202 Self {
1203 enabled: false,
1204 max_chain_length: 20,
1205 global_timeout_secs: 300,
1206 enable_parallel_execution: false,
1207 }
1208 }
1209}
1210
1211#[derive(Debug, Clone, Serialize, Deserialize)]
1213#[serde(default)]
1214pub struct DataConfig {
1215 pub default_rows: usize,
1217 pub default_format: String,
1219 pub locale: String,
1221 pub templates: HashMap<String, String>,
1223 pub rag: RagConfig,
1225 #[serde(skip_serializing_if = "Option::is_none")]
1227 pub persona_domain: Option<String>,
1228 #[serde(default = "default_false")]
1230 pub persona_consistency_enabled: bool,
1231 #[serde(skip_serializing_if = "Option::is_none")]
1233 pub persona_registry: Option<PersonaRegistryConfig>,
1234}
1235
1236impl Default for DataConfig {
1237 fn default() -> Self {
1238 Self {
1239 default_rows: 100,
1240 default_format: "json".to_string(),
1241 locale: "en".to_string(),
1242 templates: HashMap::new(),
1243 rag: RagConfig::default(),
1244 persona_domain: None,
1245 persona_consistency_enabled: false,
1246 persona_registry: None,
1247 }
1248 }
1249}
1250
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1253#[serde(default)]
1254pub struct RagConfig {
1255 pub enabled: bool,
1257 #[serde(default)]
1259 pub provider: String,
1260 pub api_endpoint: Option<String>,
1262 pub api_key: Option<String>,
1264 pub model: Option<String>,
1266 #[serde(default = "default_max_tokens")]
1268 pub max_tokens: usize,
1269 #[serde(default = "default_temperature")]
1271 pub temperature: f64,
1272 pub context_window: usize,
1274 #[serde(default = "default_true")]
1276 pub caching: bool,
1277 #[serde(default = "default_cache_ttl")]
1279 pub cache_ttl_secs: u64,
1280 #[serde(default = "default_timeout")]
1282 pub timeout_secs: u64,
1283 #[serde(default = "default_max_retries")]
1285 pub max_retries: usize,
1286}
1287
1288fn default_max_tokens() -> usize {
1289 1024
1290}
1291
1292fn default_temperature() -> f64 {
1293 0.7
1294}
1295
1296fn default_true() -> bool {
1297 true
1298}
1299
1300fn default_cache_ttl() -> u64 {
1301 3600
1302}
1303
1304fn default_timeout() -> u64 {
1305 30
1306}
1307
1308fn default_max_retries() -> usize {
1309 3
1310}
1311
1312fn default_false() -> bool {
1313 false
1314}
1315
1316impl Default for RagConfig {
1317 fn default() -> Self {
1318 Self {
1319 enabled: false,
1320 provider: "openai".to_string(),
1321 api_endpoint: None,
1322 api_key: None,
1323 model: Some("gpt-3.5-turbo".to_string()),
1324 max_tokens: default_max_tokens(),
1325 temperature: default_temperature(),
1326 context_window: 4000,
1327 caching: default_true(),
1328 cache_ttl_secs: default_cache_ttl(),
1329 timeout_secs: default_timeout(),
1330 max_retries: default_max_retries(),
1331 }
1332 }
1333}
1334
1335#[derive(Debug, Clone, Serialize, Deserialize)]
1337#[serde(default)]
1338pub struct PersonaRegistryConfig {
1339 #[serde(default = "default_false")]
1341 pub persistent: bool,
1342 #[serde(skip_serializing_if = "Option::is_none")]
1344 pub storage_path: Option<String>,
1345 #[serde(default)]
1347 pub default_traits: HashMap<String, String>,
1348}
1349
1350impl Default for PersonaRegistryConfig {
1351 fn default() -> Self {
1352 Self {
1353 persistent: false,
1354 storage_path: None,
1355 default_traits: HashMap::new(),
1356 }
1357 }
1358}
1359
1360#[derive(Debug, Clone, Serialize, Deserialize)]
1362#[serde(default)]
1363pub struct MockAIConfig {
1364 pub enabled: bool,
1366 pub intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig,
1368 pub auto_learn: bool,
1370 pub mutation_detection: bool,
1372 pub ai_validation_errors: bool,
1374 pub intelligent_pagination: bool,
1376 #[serde(default)]
1378 pub enabled_endpoints: Vec<String>,
1379}
1380
1381impl Default for MockAIConfig {
1382 fn default() -> Self {
1383 Self {
1384 enabled: false,
1385 intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig::default(),
1386 auto_learn: true,
1387 mutation_detection: true,
1388 ai_validation_errors: true,
1389 intelligent_pagination: true,
1390 enabled_endpoints: Vec::new(),
1391 }
1392 }
1393}
1394
1395#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1397#[serde(default)]
1398pub struct ObservabilityConfig {
1399 pub prometheus: PrometheusConfig,
1401 pub opentelemetry: Option<OpenTelemetryConfig>,
1403 pub recorder: Option<RecorderConfig>,
1405 pub chaos: Option<ChaosEngConfig>,
1407}
1408
1409#[derive(Debug, Clone, Serialize, Deserialize)]
1411#[serde(default)]
1412pub struct SecurityConfig {
1413 pub monitoring: SecurityMonitoringConfig,
1415}
1416
1417impl Default for SecurityConfig {
1418 fn default() -> Self {
1419 Self {
1420 monitoring: SecurityMonitoringConfig::default(),
1421 }
1422 }
1423}
1424
1425#[derive(Debug, Clone, Serialize, Deserialize)]
1427#[serde(default)]
1428pub struct SecurityMonitoringConfig {
1429 pub siem: crate::security::siem::SiemConfig,
1431 pub access_review: crate::security::access_review::AccessReviewConfig,
1433 pub privileged_access: crate::security::privileged_access::PrivilegedAccessConfig,
1435 pub change_management: crate::security::change_management::ChangeManagementConfig,
1437 pub compliance_dashboard: crate::security::compliance_dashboard::ComplianceDashboardConfig,
1439 pub risk_assessment: crate::security::risk_assessment::RiskAssessmentConfig,
1441}
1442
1443impl Default for SecurityMonitoringConfig {
1444 fn default() -> Self {
1445 Self {
1446 siem: crate::security::siem::SiemConfig::default(),
1447 access_review: crate::security::access_review::AccessReviewConfig::default(),
1448 privileged_access: crate::security::privileged_access::PrivilegedAccessConfig::default(),
1449 change_management: crate::security::change_management::ChangeManagementConfig::default(),
1450 compliance_dashboard: crate::security::compliance_dashboard::ComplianceDashboardConfig::default(),
1451 risk_assessment: crate::security::risk_assessment::RiskAssessmentConfig::default(),
1452 }
1453 }
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1458#[serde(default)]
1459pub struct PrometheusConfig {
1460 pub enabled: bool,
1462 pub port: u16,
1464 pub host: String,
1466 pub path: String,
1468}
1469
1470impl Default for PrometheusConfig {
1471 fn default() -> Self {
1472 Self {
1473 enabled: true,
1474 port: 9090,
1475 host: "0.0.0.0".to_string(),
1476 path: "/metrics".to_string(),
1477 }
1478 }
1479}
1480
1481#[derive(Debug, Clone, Serialize, Deserialize)]
1483#[serde(default)]
1484pub struct OpenTelemetryConfig {
1485 pub enabled: bool,
1487 pub service_name: String,
1489 pub environment: String,
1491 pub jaeger_endpoint: String,
1493 pub otlp_endpoint: Option<String>,
1495 pub protocol: String,
1497 pub sampling_rate: f64,
1499}
1500
1501impl Default for OpenTelemetryConfig {
1502 fn default() -> Self {
1503 Self {
1504 enabled: false,
1505 service_name: "mockforge".to_string(),
1506 environment: "development".to_string(),
1507 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
1508 otlp_endpoint: Some("http://localhost:4317".to_string()),
1509 protocol: "grpc".to_string(),
1510 sampling_rate: 1.0,
1511 }
1512 }
1513}
1514
1515#[derive(Debug, Clone, Serialize, Deserialize)]
1517#[serde(default)]
1518pub struct RecorderConfig {
1519 pub enabled: bool,
1521 pub database_path: String,
1523 pub api_enabled: bool,
1525 pub api_port: Option<u16>,
1527 pub max_requests: i64,
1529 pub retention_days: i64,
1531 pub record_http: bool,
1533 pub record_grpc: bool,
1535 pub record_websocket: bool,
1537 pub record_graphql: bool,
1539 #[serde(default = "default_true")]
1542 pub record_proxy: bool,
1543}
1544
1545impl Default for RecorderConfig {
1546 fn default() -> Self {
1547 Self {
1548 enabled: false,
1549 database_path: "./mockforge-recordings.db".to_string(),
1550 api_enabled: true,
1551 api_port: None,
1552 max_requests: 10000,
1553 retention_days: 7,
1554 record_http: true,
1555 record_grpc: true,
1556 record_websocket: true,
1557 record_graphql: true,
1558 record_proxy: true,
1559 }
1560 }
1561}
1562
1563#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1565#[serde(default)]
1566pub struct ChaosEngConfig {
1567 pub enabled: bool,
1569 pub latency: Option<LatencyInjectionConfig>,
1571 pub fault_injection: Option<FaultConfig>,
1573 pub rate_limit: Option<RateLimitingConfig>,
1575 pub traffic_shaping: Option<NetworkShapingConfig>,
1577 pub scenario: Option<String>,
1579}
1580
1581#[derive(Debug, Clone, Serialize, Deserialize)]
1583pub struct LatencyInjectionConfig {
1584 pub enabled: bool,
1586 pub fixed_delay_ms: Option<u64>,
1588 pub random_delay_range_ms: Option<(u64, u64)>,
1590 pub jitter_percent: f64,
1592 pub probability: f64,
1594}
1595
1596#[derive(Debug, Clone, Serialize, Deserialize)]
1598pub struct FaultConfig {
1599 pub enabled: bool,
1601 pub http_errors: Vec<u16>,
1603 pub http_error_probability: f64,
1605 pub connection_errors: bool,
1607 pub connection_error_probability: f64,
1609 pub timeout_errors: bool,
1611 pub timeout_ms: u64,
1613 pub timeout_probability: f64,
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1619pub struct RateLimitingConfig {
1620 pub enabled: bool,
1622 pub requests_per_second: u32,
1624 pub burst_size: u32,
1626 pub per_ip: bool,
1628 pub per_endpoint: bool,
1630}
1631
1632#[derive(Debug, Clone, Serialize, Deserialize)]
1634pub struct NetworkShapingConfig {
1635 pub enabled: bool,
1637 pub bandwidth_limit_bps: u64,
1639 pub packet_loss_percent: f64,
1641 pub max_connections: u32,
1643}
1644
1645pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1647 let content = fs::read_to_string(&path)
1648 .await
1649 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1650
1651 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1653 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1654 {
1655 serde_yaml::from_str(&content).map_err(|e| {
1656 let error_msg = e.to_string();
1658 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
1659
1660 if error_msg.contains("missing field") {
1662 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1663 full_msg.push_str(
1664 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1665 );
1666 full_msg.push_str("\n See config.template.yaml for all available options.");
1667 } else if error_msg.contains("unknown field") {
1668 full_msg.push_str("\n\n💡 Check for typos in field names.");
1669 full_msg.push_str("\n See config.template.yaml for valid field names.");
1670 }
1671
1672 Error::generic(full_msg)
1673 })?
1674 } else {
1675 serde_json::from_str(&content).map_err(|e| {
1676 let error_msg = e.to_string();
1678 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
1679
1680 if error_msg.contains("missing field") {
1682 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1683 full_msg.push_str(
1684 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1685 );
1686 full_msg.push_str("\n See config.template.yaml for all available options.");
1687 } else if error_msg.contains("unknown field") {
1688 full_msg.push_str("\n\n💡 Check for typos in field names.");
1689 full_msg.push_str("\n See config.template.yaml for valid field names.");
1690 }
1691
1692 Error::generic(full_msg)
1693 })?
1694 };
1695
1696 Ok(config)
1697}
1698
1699pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1701 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1702 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1703 {
1704 serde_yaml::to_string(config)
1705 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1706 } else {
1707 serde_json::to_string_pretty(config)
1708 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1709 };
1710
1711 fs::write(path, content)
1712 .await
1713 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1714
1715 Ok(())
1716}
1717
1718pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1720 match load_config(&path).await {
1721 Ok(config) => {
1722 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1723 config
1724 }
1725 Err(e) => {
1726 tracing::warn!(
1727 "Failed to load config from {:?}: {}. Using defaults.",
1728 path.as_ref(),
1729 e
1730 );
1731 ServerConfig::default()
1732 }
1733 }
1734}
1735
1736pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1738 let config = ServerConfig::default();
1739 save_config(path, &config).await?;
1740 Ok(())
1741}
1742
1743pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1745 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1747 if let Ok(port_num) = port.parse() {
1748 config.http.port = port_num;
1749 }
1750 }
1751
1752 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1753 config.http.host = host;
1754 }
1755
1756 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1758 if let Ok(port_num) = port.parse() {
1759 config.websocket.port = port_num;
1760 }
1761 }
1762
1763 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1765 if let Ok(port_num) = port.parse() {
1766 config.grpc.port = port_num;
1767 }
1768 }
1769
1770 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1772 if let Ok(port_num) = port.parse() {
1773 config.smtp.port = port_num;
1774 }
1775 }
1776
1777 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1778 config.smtp.host = host;
1779 }
1780
1781 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1782 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1783 }
1784
1785 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1786 config.smtp.hostname = hostname;
1787 }
1788
1789 if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
1791 if let Ok(port_num) = port.parse() {
1792 config.tcp.port = port_num;
1793 }
1794 }
1795
1796 if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
1797 config.tcp.host = host;
1798 }
1799
1800 if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
1801 config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1802 }
1803
1804 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1806 if let Ok(port_num) = port.parse() {
1807 config.admin.port = port_num;
1808 }
1809 }
1810
1811 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1812 config.admin.enabled = true;
1813 }
1814
1815 if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
1817 config.admin.host = host;
1818 }
1819
1820 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1821 if !mount_path.trim().is_empty() {
1822 config.admin.mount_path = Some(mount_path);
1823 }
1824 }
1825
1826 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1827 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1828 config.admin.api_enabled = on;
1829 }
1830
1831 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1832 config.admin.prometheus_url = prometheus_url;
1833 }
1834
1835 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1837 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1838 config.core.latency_enabled = enabled;
1839 }
1840
1841 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1842 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1843 config.core.failures_enabled = enabled;
1844 }
1845
1846 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1847 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1848 config.core.overrides_enabled = enabled;
1849 }
1850
1851 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1852 let enabled =
1853 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1854 config.core.traffic_shaping_enabled = enabled;
1855 }
1856
1857 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1859 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1860 config.core.traffic_shaping.bandwidth.enabled = enabled;
1861 }
1862
1863 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1864 if let Ok(bytes) = max_bytes_per_sec.parse() {
1865 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1866 config.core.traffic_shaping.bandwidth.enabled = true;
1867 }
1868 }
1869
1870 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1871 if let Ok(bytes) = burst_capacity.parse() {
1872 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1873 }
1874 }
1875
1876 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1877 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1878 config.core.traffic_shaping.burst_loss.enabled = enabled;
1879 }
1880
1881 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1882 if let Ok(prob) = burst_probability.parse::<f64>() {
1883 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1884 config.core.traffic_shaping.burst_loss.enabled = true;
1885 }
1886 }
1887
1888 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1889 if let Ok(ms) = burst_duration.parse() {
1890 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1891 }
1892 }
1893
1894 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1895 if let Ok(rate) = loss_rate.parse::<f64>() {
1896 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1897 }
1898 }
1899
1900 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1901 if let Ok(ms) = recovery_time.parse() {
1902 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1903 }
1904 }
1905
1906 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1908 config.logging.level = level;
1909 }
1910
1911 config
1912}
1913
1914pub fn validate_config(config: &ServerConfig) -> Result<()> {
1916 if config.http.port == 0 {
1918 return Err(Error::generic("HTTP port cannot be 0"));
1919 }
1920 if config.websocket.port == 0 {
1921 return Err(Error::generic("WebSocket port cannot be 0"));
1922 }
1923 if config.grpc.port == 0 {
1924 return Err(Error::generic("gRPC port cannot be 0"));
1925 }
1926 if config.admin.port == 0 {
1927 return Err(Error::generic("Admin port cannot be 0"));
1928 }
1929
1930 let ports = [
1932 ("HTTP", config.http.port),
1933 ("WebSocket", config.websocket.port),
1934 ("gRPC", config.grpc.port),
1935 ("Admin", config.admin.port),
1936 ];
1937
1938 for i in 0..ports.len() {
1939 for j in (i + 1)..ports.len() {
1940 if ports[i].1 == ports[j].1 {
1941 return Err(Error::generic(format!(
1942 "Port conflict: {} and {} both use port {}",
1943 ports[i].0, ports[j].0, ports[i].1
1944 )));
1945 }
1946 }
1947 }
1948
1949 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1951 if !valid_levels.contains(&config.logging.level.as_str()) {
1952 return Err(Error::generic(format!(
1953 "Invalid log level: {}. Valid levels: {}",
1954 config.logging.level,
1955 valid_levels.join(", ")
1956 )));
1957 }
1958
1959 Ok(())
1960}
1961
1962pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1964 macro_rules! merge_field {
1966 ($field:ident) => {
1967 if let Some(override_val) = profile.$field {
1968 base.$field = override_val;
1969 }
1970 };
1971 }
1972
1973 merge_field!(http);
1974 merge_field!(websocket);
1975 merge_field!(graphql);
1976 merge_field!(grpc);
1977 merge_field!(mqtt);
1978 merge_field!(smtp);
1979 merge_field!(ftp);
1980 merge_field!(kafka);
1981 merge_field!(amqp);
1982 merge_field!(tcp);
1983 merge_field!(admin);
1984 merge_field!(chaining);
1985 merge_field!(core);
1986 merge_field!(logging);
1987 merge_field!(data);
1988 merge_field!(mockai);
1989 merge_field!(observability);
1990 merge_field!(multi_tenant);
1991 merge_field!(routes);
1992 merge_field!(protocols);
1993
1994 base
1995}
1996
1997pub async fn load_config_with_profile<P: AsRef<Path>>(
1999 path: P,
2000 profile_name: Option<&str>,
2001) -> Result<ServerConfig> {
2002 let mut config = load_config_auto(&path).await?;
2004
2005 if let Some(profile) = profile_name {
2007 if let Some(profile_config) = config.profiles.remove(profile) {
2008 tracing::info!("Applying profile: {}", profile);
2009 config = apply_profile(config, profile_config);
2010 } else {
2011 return Err(Error::generic(format!(
2012 "Profile '{}' not found in configuration. Available profiles: {}",
2013 profile,
2014 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
2015 )));
2016 }
2017 }
2018
2019 config.profiles.clear();
2021
2022 Ok(config)
2023}
2024
2025pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
2027 use rquickjs::{Context, Runtime};
2028
2029 let content = fs::read_to_string(&path)
2030 .await
2031 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
2032
2033 let runtime = Runtime::new()
2035 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
2036 let context = Context::full(&runtime)
2037 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
2038
2039 context.with(|ctx| {
2040 let js_content = if path
2043 .as_ref()
2044 .extension()
2045 .and_then(|s| s.to_str())
2046 .map(|ext| ext == "ts")
2047 .unwrap_or(false)
2048 {
2049 strip_typescript_types(&content)?
2050 } else {
2051 content
2052 };
2053
2054 let result: rquickjs::Value = ctx
2056 .eval(js_content.as_bytes())
2057 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
2058
2059 let json_str: String = ctx
2061 .json_stringify(result)
2062 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
2063 .ok_or_else(|| Error::generic("JS config returned undefined"))?
2064 .get()
2065 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
2066
2067 serde_json::from_str(&json_str).map_err(|e| {
2069 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
2070 })
2071 })
2072}
2073
2074fn strip_typescript_types(content: &str) -> Result<String> {
2081 use regex::Regex;
2082
2083 let mut result = content.to_string();
2084
2085 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
2091 .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
2092 result = interface_re.replace_all(&result, "").to_string();
2093
2094 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
2096 .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
2097 result = type_alias_re.replace_all(&result, "").to_string();
2098
2099 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
2101 .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
2102 result = type_annotation_re.replace_all(&result, "").to_string();
2103
2104 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
2106 .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
2107 result = type_import_re.replace_all(&result, "").to_string();
2108
2109 let as_type_re = Regex::new(r"\s+as\s+\w+")
2111 .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
2112 result = as_type_re.replace_all(&result, "").to_string();
2113
2114 Ok(result)
2115}
2116
2117pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
2119 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
2120
2121 match ext {
2122 "ts" | "js" => load_config_from_js(&path).await,
2123 "yaml" | "yml" | "json" => load_config(&path).await,
2124 _ => Err(Error::generic(format!(
2125 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
2126 ext
2127 ))),
2128 }
2129}
2130
2131pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
2133 let current_dir = std::env::current_dir()
2134 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
2135
2136 let config_names = vec![
2137 "mockforge.config.ts",
2138 "mockforge.config.js",
2139 "mockforge.yaml",
2140 "mockforge.yml",
2141 ".mockforge.yaml",
2142 ".mockforge.yml",
2143 ];
2144
2145 for name in &config_names {
2147 let path = current_dir.join(name);
2148 if tokio::fs::metadata(&path).await.is_ok() {
2149 return Ok(path);
2150 }
2151 }
2152
2153 let mut dir = current_dir.clone();
2155 for _ in 0..5 {
2156 if let Some(parent) = dir.parent() {
2157 for name in &config_names {
2158 let path = parent.join(name);
2159 if tokio::fs::metadata(&path).await.is_ok() {
2160 return Ok(path);
2161 }
2162 }
2163 dir = parent.to_path_buf();
2164 } else {
2165 break;
2166 }
2167 }
2168
2169 Err(Error::generic(
2170 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
2171 ))
2172}
2173
2174#[cfg(test)]
2175mod tests {
2176 use super::*;
2177
2178 #[test]
2179 fn test_default_config() {
2180 let config = ServerConfig::default();
2181 assert_eq!(config.http.port, 3000);
2182 assert_eq!(config.websocket.port, 3001);
2183 assert_eq!(config.grpc.port, 50051);
2184 assert_eq!(config.admin.port, 9080);
2185 }
2186
2187 #[test]
2188 fn test_config_validation() {
2189 let mut config = ServerConfig::default();
2190 assert!(validate_config(&config).is_ok());
2191
2192 config.websocket.port = config.http.port;
2194 assert!(validate_config(&config).is_err());
2195
2196 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
2199 assert!(validate_config(&config).is_err());
2200 }
2201
2202 #[test]
2203 fn test_apply_profile() {
2204 let mut base = ServerConfig::default();
2205 assert_eq!(base.http.port, 3000);
2206
2207 let mut profile = ProfileConfig::default();
2208 profile.http = Some(HttpConfig {
2209 port: 8080,
2210 ..Default::default()
2211 });
2212 profile.logging = Some(LoggingConfig {
2213 level: "debug".to_string(),
2214 ..Default::default()
2215 });
2216
2217 let merged = apply_profile(base, profile);
2218 assert_eq!(merged.http.port, 8080);
2219 assert_eq!(merged.logging.level, "debug");
2220 assert_eq!(merged.websocket.port, 3001); }
2222
2223 #[test]
2224 fn test_strip_typescript_types() {
2225 let ts_code = r#"
2226interface Config {
2227 port: number;
2228 host: string;
2229}
2230
2231const config: Config = {
2232 port: 3000,
2233 host: "localhost"
2234} as Config;
2235"#;
2236
2237 let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
2238 assert!(!stripped.contains("interface"));
2239 assert!(!stripped.contains(": Config"));
2240 assert!(!stripped.contains("as Config"));
2241 }
2242}