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}
517
518#[derive(Debug, Clone, Serialize, Deserialize, Default)]
520#[serde(default)]
521pub struct ProfileConfig {
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub http: Option<HttpConfig>,
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub websocket: Option<WebSocketConfig>,
528 #[serde(skip_serializing_if = "Option::is_none")]
530 pub graphql: Option<GraphQLConfig>,
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub grpc: Option<GrpcConfig>,
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub mqtt: Option<MqttConfig>,
537 #[serde(skip_serializing_if = "Option::is_none")]
539 pub smtp: Option<SmtpConfig>,
540 #[serde(skip_serializing_if = "Option::is_none")]
542 pub ftp: Option<FtpConfig>,
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub kafka: Option<KafkaConfig>,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub amqp: Option<AmqpConfig>,
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub tcp: Option<TcpConfig>,
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub admin: Option<AdminConfig>,
555 #[serde(skip_serializing_if = "Option::is_none")]
557 pub chaining: Option<ChainingConfig>,
558 #[serde(skip_serializing_if = "Option::is_none")]
560 pub core: Option<CoreConfig>,
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub logging: Option<LoggingConfig>,
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub data: Option<DataConfig>,
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub mockai: Option<MockAIConfig>,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub observability: Option<ObservabilityConfig>,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
576 #[serde(skip_serializing_if = "Option::is_none")]
578 pub routes: Option<Vec<RouteConfig>>,
579 #[serde(skip_serializing_if = "Option::is_none")]
581 pub protocols: Option<ProtocolsConfig>,
582 #[serde(skip_serializing_if = "Option::is_none")]
584 pub deceptive_deploy: Option<DeceptiveDeployConfig>,
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub reality: Option<RealitySliderConfig>,
588 #[serde(skip_serializing_if = "Option::is_none")]
590 pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct HttpValidationConfig {
598 pub mode: String,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct HttpCorsConfig {
605 pub enabled: bool,
607 #[serde(default)]
609 pub allowed_origins: Vec<String>,
610 #[serde(default)]
612 pub allowed_methods: Vec<String>,
613 #[serde(default)]
615 pub allowed_headers: Vec<String>,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
620#[serde(default)]
621pub struct HttpConfig {
622 pub enabled: bool,
624 pub port: u16,
626 pub host: String,
628 pub openapi_spec: Option<String>,
630 pub cors: Option<HttpCorsConfig>,
632 pub request_timeout_secs: u64,
634 pub validation: Option<HttpValidationConfig>,
636 pub aggregate_validation_errors: bool,
638 pub validate_responses: bool,
640 pub response_template_expand: bool,
642 pub validation_status: Option<u16>,
644 pub validation_overrides: std::collections::HashMap<String, String>,
646 pub skip_admin_validation: bool,
648 pub auth: Option<AuthConfig>,
650 #[serde(skip_serializing_if = "Option::is_none")]
652 pub tls: Option<HttpTlsConfig>,
653}
654
655impl Default for HttpConfig {
656 fn default() -> Self {
657 Self {
658 enabled: true,
659 port: 3000,
660 host: "0.0.0.0".to_string(),
661 openapi_spec: None,
662 cors: Some(HttpCorsConfig {
663 enabled: true,
664 allowed_origins: vec!["*".to_string()],
665 allowed_methods: vec![
666 "GET".to_string(),
667 "POST".to_string(),
668 "PUT".to_string(),
669 "DELETE".to_string(),
670 "PATCH".to_string(),
671 "OPTIONS".to_string(),
672 ],
673 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
674 }),
675 request_timeout_secs: 30,
676 validation: Some(HttpValidationConfig {
677 mode: "enforce".to_string(),
678 }),
679 aggregate_validation_errors: true,
680 validate_responses: false,
681 response_template_expand: false,
682 validation_status: None,
683 validation_overrides: std::collections::HashMap::new(),
684 skip_admin_validation: true,
685 auth: None,
686 tls: None,
687 }
688 }
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct HttpTlsConfig {
694 pub enabled: bool,
696 pub cert_file: String,
698 pub key_file: String,
700 #[serde(skip_serializing_if = "Option::is_none")]
702 pub ca_file: Option<String>,
703 #[serde(default = "default_tls_min_version")]
705 pub min_version: String,
706 #[serde(default, skip_serializing_if = "Vec::is_empty")]
708 pub cipher_suites: Vec<String>,
709 #[serde(default)]
711 pub require_client_cert: bool,
712}
713
714fn default_tls_min_version() -> String {
715 "1.2".to_string()
716}
717
718impl Default for HttpTlsConfig {
719 fn default() -> Self {
720 Self {
721 enabled: true,
722 cert_file: String::new(),
723 key_file: String::new(),
724 ca_file: None,
725 min_version: "1.2".to_string(),
726 cipher_suites: Vec::new(),
727 require_client_cert: false,
728 }
729 }
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
734#[serde(default)]
735pub struct WebSocketConfig {
736 pub enabled: bool,
738 pub port: u16,
740 pub host: String,
742 pub replay_file: Option<String>,
744 pub connection_timeout_secs: u64,
746}
747
748impl Default for WebSocketConfig {
749 fn default() -> Self {
750 Self {
751 enabled: true,
752 port: 3001,
753 host: "0.0.0.0".to_string(),
754 replay_file: None,
755 connection_timeout_secs: 300,
756 }
757 }
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize)]
762#[serde(default)]
763pub struct GrpcConfig {
764 pub enabled: bool,
766 pub port: u16,
768 pub host: String,
770 pub proto_dir: Option<String>,
772 pub tls: Option<TlsConfig>,
774}
775
776impl Default for GrpcConfig {
777 fn default() -> Self {
778 Self {
779 enabled: true,
780 port: 50051,
781 host: "0.0.0.0".to_string(),
782 proto_dir: None,
783 tls: None,
784 }
785 }
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize)]
790#[serde(default)]
791pub struct GraphQLConfig {
792 pub enabled: bool,
794 pub port: u16,
796 pub host: String,
798 pub schema_path: Option<String>,
800 pub handlers_dir: Option<String>,
802 pub playground_enabled: bool,
804 pub upstream_url: Option<String>,
806 pub introspection_enabled: bool,
808}
809
810impl Default for GraphQLConfig {
811 fn default() -> Self {
812 Self {
813 enabled: true,
814 port: 4000,
815 host: "0.0.0.0".to_string(),
816 schema_path: None,
817 handlers_dir: None,
818 playground_enabled: true,
819 upstream_url: None,
820 introspection_enabled: true,
821 }
822 }
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize)]
827pub struct TlsConfig {
828 pub cert_path: String,
830 pub key_path: String,
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
836#[serde(default)]
837pub struct MqttConfig {
838 pub enabled: bool,
840 pub port: u16,
842 pub host: String,
844 pub max_connections: usize,
846 pub max_packet_size: usize,
848 pub keep_alive_secs: u16,
850 pub fixtures_dir: Option<std::path::PathBuf>,
852 pub enable_retained_messages: bool,
854 pub max_retained_messages: usize,
856}
857
858impl Default for MqttConfig {
859 fn default() -> Self {
860 Self {
861 enabled: false,
862 port: 1883,
863 host: "0.0.0.0".to_string(),
864 max_connections: 1000,
865 max_packet_size: 268435456, keep_alive_secs: 60,
867 fixtures_dir: None,
868 enable_retained_messages: true,
869 max_retained_messages: 10000,
870 }
871 }
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize)]
876#[serde(default)]
877pub struct SmtpConfig {
878 pub enabled: bool,
880 pub port: u16,
882 pub host: String,
884 pub hostname: String,
886 pub fixtures_dir: Option<std::path::PathBuf>,
888 pub timeout_secs: u64,
890 pub max_connections: usize,
892 pub enable_mailbox: bool,
894 pub max_mailbox_messages: usize,
896 pub enable_starttls: bool,
898 pub tls_cert_path: Option<std::path::PathBuf>,
900 pub tls_key_path: Option<std::path::PathBuf>,
902}
903
904impl Default for SmtpConfig {
905 fn default() -> Self {
906 Self {
907 enabled: false,
908 port: 1025,
909 host: "0.0.0.0".to_string(),
910 hostname: "mockforge-smtp".to_string(),
911 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
912 timeout_secs: 300,
913 max_connections: 10,
914 enable_mailbox: true,
915 max_mailbox_messages: 1000,
916 enable_starttls: false,
917 tls_cert_path: None,
918 tls_key_path: None,
919 }
920 }
921}
922
923#[derive(Debug, Clone, Serialize, Deserialize)]
925#[serde(default)]
926pub struct FtpConfig {
927 pub enabled: bool,
929 pub port: u16,
931 pub host: String,
933 pub passive_ports: (u16, u16),
935 pub max_connections: usize,
937 pub timeout_secs: u64,
939 pub allow_anonymous: bool,
941 pub fixtures_dir: Option<std::path::PathBuf>,
943 pub virtual_root: std::path::PathBuf,
945}
946
947impl Default for FtpConfig {
948 fn default() -> Self {
949 Self {
950 enabled: false,
951 port: 2121,
952 host: "0.0.0.0".to_string(),
953 passive_ports: (50000, 51000),
954 max_connections: 100,
955 timeout_secs: 300,
956 allow_anonymous: true,
957 fixtures_dir: None,
958 virtual_root: std::path::PathBuf::from("/mockforge"),
959 }
960 }
961}
962
963#[derive(Debug, Clone, Serialize, Deserialize)]
965#[serde(default)]
966pub struct KafkaConfig {
967 pub enabled: bool,
969 pub port: u16,
971 pub host: String,
973 pub broker_id: i32,
975 pub max_connections: usize,
977 pub log_retention_ms: i64,
979 pub log_segment_bytes: i64,
981 pub fixtures_dir: Option<std::path::PathBuf>,
983 pub auto_create_topics: bool,
985 pub default_partitions: i32,
987 pub default_replication_factor: i16,
989}
990
991impl Default for KafkaConfig {
992 fn default() -> Self {
993 Self {
994 enabled: false,
995 port: 9092, host: "0.0.0.0".to_string(),
997 broker_id: 1,
998 max_connections: 1000,
999 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
1002 auto_create_topics: true,
1003 default_partitions: 3,
1004 default_replication_factor: 1,
1005 }
1006 }
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1011#[serde(default)]
1012pub struct AmqpConfig {
1013 pub enabled: bool,
1015 pub port: u16,
1017 pub host: String,
1019 pub max_connections: usize,
1021 pub max_channels_per_connection: u16,
1023 pub frame_max: u32,
1025 pub heartbeat_interval: u16,
1027 pub fixtures_dir: Option<std::path::PathBuf>,
1029 pub virtual_hosts: Vec<String>,
1031}
1032
1033impl Default for AmqpConfig {
1034 fn default() -> Self {
1035 Self {
1036 enabled: false,
1037 port: 5672, host: "0.0.0.0".to_string(),
1039 max_connections: 1000,
1040 max_channels_per_connection: 100,
1041 frame_max: 131072, heartbeat_interval: 60,
1043 fixtures_dir: None,
1044 virtual_hosts: vec!["/".to_string()],
1045 }
1046 }
1047}
1048
1049#[derive(Debug, Clone, Serialize, Deserialize)]
1051#[serde(default)]
1052pub struct TcpConfig {
1053 pub enabled: bool,
1055 pub port: u16,
1057 pub host: String,
1059 pub max_connections: usize,
1061 pub timeout_secs: u64,
1063 pub fixtures_dir: Option<std::path::PathBuf>,
1065 pub echo_mode: bool,
1067 pub enable_tls: bool,
1069 pub tls_cert_path: Option<std::path::PathBuf>,
1071 pub tls_key_path: Option<std::path::PathBuf>,
1073}
1074
1075impl Default for TcpConfig {
1076 fn default() -> Self {
1077 Self {
1078 enabled: false,
1079 port: 9999,
1080 host: "0.0.0.0".to_string(),
1081 max_connections: 1000,
1082 timeout_secs: 300,
1083 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/tcp")),
1084 echo_mode: true,
1085 enable_tls: false,
1086 tls_cert_path: None,
1087 tls_key_path: None,
1088 }
1089 }
1090}
1091
1092#[derive(Debug, Clone, Serialize, Deserialize)]
1094#[serde(default)]
1095pub struct AdminConfig {
1096 pub enabled: bool,
1098 pub port: u16,
1100 pub host: String,
1102 pub auth_required: bool,
1104 pub username: Option<String>,
1106 pub password: Option<String>,
1108 pub mount_path: Option<String>,
1110 pub api_enabled: bool,
1112 pub prometheus_url: String,
1114}
1115
1116impl Default for AdminConfig {
1117 fn default() -> Self {
1118 let default_host = if std::env::var("DOCKER_CONTAINER").is_ok()
1121 || std::env::var("container").is_ok()
1122 || std::path::Path::new("/.dockerenv").exists()
1123 {
1124 "0.0.0.0".to_string()
1125 } else {
1126 "127.0.0.1".to_string()
1127 };
1128
1129 Self {
1130 enabled: false,
1131 port: 9080,
1132 host: default_host,
1133 auth_required: false,
1134 username: None,
1135 password: None,
1136 mount_path: None,
1137 api_enabled: true,
1138 prometheus_url: "http://localhost:9090".to_string(),
1139 }
1140 }
1141}
1142
1143#[derive(Debug, Clone, Serialize, Deserialize)]
1145#[serde(default)]
1146pub struct LoggingConfig {
1147 pub level: String,
1149 pub json_format: bool,
1151 pub file_path: Option<String>,
1153 pub max_file_size_mb: u64,
1155 pub max_files: u32,
1157}
1158
1159impl Default for LoggingConfig {
1160 fn default() -> Self {
1161 Self {
1162 level: "info".to_string(),
1163 json_format: false,
1164 file_path: None,
1165 max_file_size_mb: 10,
1166 max_files: 5,
1167 }
1168 }
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize)]
1173#[serde(default, rename_all = "camelCase")]
1174pub struct ChainingConfig {
1175 pub enabled: bool,
1177 pub max_chain_length: usize,
1179 pub global_timeout_secs: u64,
1181 pub enable_parallel_execution: bool,
1183}
1184
1185impl Default for ChainingConfig {
1186 fn default() -> Self {
1187 Self {
1188 enabled: false,
1189 max_chain_length: 20,
1190 global_timeout_secs: 300,
1191 enable_parallel_execution: false,
1192 }
1193 }
1194}
1195
1196#[derive(Debug, Clone, Serialize, Deserialize)]
1198#[serde(default)]
1199pub struct DataConfig {
1200 pub default_rows: usize,
1202 pub default_format: String,
1204 pub locale: String,
1206 pub templates: HashMap<String, String>,
1208 pub rag: RagConfig,
1210 #[serde(skip_serializing_if = "Option::is_none")]
1212 pub persona_domain: Option<String>,
1213 #[serde(default = "default_false")]
1215 pub persona_consistency_enabled: bool,
1216 #[serde(skip_serializing_if = "Option::is_none")]
1218 pub persona_registry: Option<PersonaRegistryConfig>,
1219}
1220
1221impl Default for DataConfig {
1222 fn default() -> Self {
1223 Self {
1224 default_rows: 100,
1225 default_format: "json".to_string(),
1226 locale: "en".to_string(),
1227 templates: HashMap::new(),
1228 rag: RagConfig::default(),
1229 persona_domain: None,
1230 persona_consistency_enabled: false,
1231 persona_registry: None,
1232 }
1233 }
1234}
1235
1236#[derive(Debug, Clone, Serialize, Deserialize)]
1238#[serde(default)]
1239pub struct RagConfig {
1240 pub enabled: bool,
1242 #[serde(default)]
1244 pub provider: String,
1245 pub api_endpoint: Option<String>,
1247 pub api_key: Option<String>,
1249 pub model: Option<String>,
1251 #[serde(default = "default_max_tokens")]
1253 pub max_tokens: usize,
1254 #[serde(default = "default_temperature")]
1256 pub temperature: f64,
1257 pub context_window: usize,
1259 #[serde(default = "default_true")]
1261 pub caching: bool,
1262 #[serde(default = "default_cache_ttl")]
1264 pub cache_ttl_secs: u64,
1265 #[serde(default = "default_timeout")]
1267 pub timeout_secs: u64,
1268 #[serde(default = "default_max_retries")]
1270 pub max_retries: usize,
1271}
1272
1273fn default_max_tokens() -> usize {
1274 1024
1275}
1276
1277fn default_temperature() -> f64 {
1278 0.7
1279}
1280
1281fn default_true() -> bool {
1282 true
1283}
1284
1285fn default_cache_ttl() -> u64 {
1286 3600
1287}
1288
1289fn default_timeout() -> u64 {
1290 30
1291}
1292
1293fn default_max_retries() -> usize {
1294 3
1295}
1296
1297fn default_false() -> bool {
1298 false
1299}
1300
1301impl Default for RagConfig {
1302 fn default() -> Self {
1303 Self {
1304 enabled: false,
1305 provider: "openai".to_string(),
1306 api_endpoint: None,
1307 api_key: None,
1308 model: Some("gpt-3.5-turbo".to_string()),
1309 max_tokens: default_max_tokens(),
1310 temperature: default_temperature(),
1311 context_window: 4000,
1312 caching: default_true(),
1313 cache_ttl_secs: default_cache_ttl(),
1314 timeout_secs: default_timeout(),
1315 max_retries: default_max_retries(),
1316 }
1317 }
1318}
1319
1320#[derive(Debug, Clone, Serialize, Deserialize)]
1322#[serde(default)]
1323pub struct PersonaRegistryConfig {
1324 #[serde(default = "default_false")]
1326 pub persistent: bool,
1327 #[serde(skip_serializing_if = "Option::is_none")]
1329 pub storage_path: Option<String>,
1330 #[serde(default)]
1332 pub default_traits: HashMap<String, String>,
1333}
1334
1335impl Default for PersonaRegistryConfig {
1336 fn default() -> Self {
1337 Self {
1338 persistent: false,
1339 storage_path: None,
1340 default_traits: HashMap::new(),
1341 }
1342 }
1343}
1344
1345#[derive(Debug, Clone, Serialize, Deserialize)]
1347#[serde(default)]
1348pub struct MockAIConfig {
1349 pub enabled: bool,
1351 pub intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig,
1353 pub auto_learn: bool,
1355 pub mutation_detection: bool,
1357 pub ai_validation_errors: bool,
1359 pub intelligent_pagination: bool,
1361 #[serde(default)]
1363 pub enabled_endpoints: Vec<String>,
1364}
1365
1366impl Default for MockAIConfig {
1367 fn default() -> Self {
1368 Self {
1369 enabled: false,
1370 intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig::default(),
1371 auto_learn: true,
1372 mutation_detection: true,
1373 ai_validation_errors: true,
1374 intelligent_pagination: true,
1375 enabled_endpoints: Vec::new(),
1376 }
1377 }
1378}
1379
1380#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1382#[serde(default)]
1383pub struct ObservabilityConfig {
1384 pub prometheus: PrometheusConfig,
1386 pub opentelemetry: Option<OpenTelemetryConfig>,
1388 pub recorder: Option<RecorderConfig>,
1390 pub chaos: Option<ChaosEngConfig>,
1392}
1393
1394#[derive(Debug, Clone, Serialize, Deserialize)]
1396#[serde(default)]
1397pub struct PrometheusConfig {
1398 pub enabled: bool,
1400 pub port: u16,
1402 pub host: String,
1404 pub path: String,
1406}
1407
1408impl Default for PrometheusConfig {
1409 fn default() -> Self {
1410 Self {
1411 enabled: true,
1412 port: 9090,
1413 host: "0.0.0.0".to_string(),
1414 path: "/metrics".to_string(),
1415 }
1416 }
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1421#[serde(default)]
1422pub struct OpenTelemetryConfig {
1423 pub enabled: bool,
1425 pub service_name: String,
1427 pub environment: String,
1429 pub jaeger_endpoint: String,
1431 pub otlp_endpoint: Option<String>,
1433 pub protocol: String,
1435 pub sampling_rate: f64,
1437}
1438
1439impl Default for OpenTelemetryConfig {
1440 fn default() -> Self {
1441 Self {
1442 enabled: false,
1443 service_name: "mockforge".to_string(),
1444 environment: "development".to_string(),
1445 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
1446 otlp_endpoint: Some("http://localhost:4317".to_string()),
1447 protocol: "grpc".to_string(),
1448 sampling_rate: 1.0,
1449 }
1450 }
1451}
1452
1453#[derive(Debug, Clone, Serialize, Deserialize)]
1455#[serde(default)]
1456pub struct RecorderConfig {
1457 pub enabled: bool,
1459 pub database_path: String,
1461 pub api_enabled: bool,
1463 pub api_port: Option<u16>,
1465 pub max_requests: i64,
1467 pub retention_days: i64,
1469 pub record_http: bool,
1471 pub record_grpc: bool,
1473 pub record_websocket: bool,
1475 pub record_graphql: bool,
1477 #[serde(default = "default_true")]
1480 pub record_proxy: bool,
1481}
1482
1483impl Default for RecorderConfig {
1484 fn default() -> Self {
1485 Self {
1486 enabled: false,
1487 database_path: "./mockforge-recordings.db".to_string(),
1488 api_enabled: true,
1489 api_port: None,
1490 max_requests: 10000,
1491 retention_days: 7,
1492 record_http: true,
1493 record_grpc: true,
1494 record_websocket: true,
1495 record_graphql: true,
1496 record_proxy: true,
1497 }
1498 }
1499}
1500
1501#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1503#[serde(default)]
1504pub struct ChaosEngConfig {
1505 pub enabled: bool,
1507 pub latency: Option<LatencyInjectionConfig>,
1509 pub fault_injection: Option<FaultConfig>,
1511 pub rate_limit: Option<RateLimitingConfig>,
1513 pub traffic_shaping: Option<NetworkShapingConfig>,
1515 pub scenario: Option<String>,
1517}
1518
1519#[derive(Debug, Clone, Serialize, Deserialize)]
1521pub struct LatencyInjectionConfig {
1522 pub enabled: bool,
1524 pub fixed_delay_ms: Option<u64>,
1526 pub random_delay_range_ms: Option<(u64, u64)>,
1528 pub jitter_percent: f64,
1530 pub probability: f64,
1532}
1533
1534#[derive(Debug, Clone, Serialize, Deserialize)]
1536pub struct FaultConfig {
1537 pub enabled: bool,
1539 pub http_errors: Vec<u16>,
1541 pub http_error_probability: f64,
1543 pub connection_errors: bool,
1545 pub connection_error_probability: f64,
1547 pub timeout_errors: bool,
1549 pub timeout_ms: u64,
1551 pub timeout_probability: f64,
1553}
1554
1555#[derive(Debug, Clone, Serialize, Deserialize)]
1557pub struct RateLimitingConfig {
1558 pub enabled: bool,
1560 pub requests_per_second: u32,
1562 pub burst_size: u32,
1564 pub per_ip: bool,
1566 pub per_endpoint: bool,
1568}
1569
1570#[derive(Debug, Clone, Serialize, Deserialize)]
1572pub struct NetworkShapingConfig {
1573 pub enabled: bool,
1575 pub bandwidth_limit_bps: u64,
1577 pub packet_loss_percent: f64,
1579 pub max_connections: u32,
1581}
1582
1583pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1585 let content = fs::read_to_string(&path)
1586 .await
1587 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1588
1589 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1591 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1592 {
1593 serde_yaml::from_str(&content).map_err(|e| {
1594 let error_msg = e.to_string();
1596 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
1597
1598 if error_msg.contains("missing field") {
1600 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1601 full_msg.push_str(
1602 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1603 );
1604 full_msg.push_str("\n See config.template.yaml for all available options.");
1605 } else if error_msg.contains("unknown field") {
1606 full_msg.push_str("\n\n💡 Check for typos in field names.");
1607 full_msg.push_str("\n See config.template.yaml for valid field names.");
1608 }
1609
1610 Error::generic(full_msg)
1611 })?
1612 } else {
1613 serde_json::from_str(&content).map_err(|e| {
1614 let error_msg = e.to_string();
1616 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
1617
1618 if error_msg.contains("missing field") {
1620 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1621 full_msg.push_str(
1622 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1623 );
1624 full_msg.push_str("\n See config.template.yaml for all available options.");
1625 } else if error_msg.contains("unknown field") {
1626 full_msg.push_str("\n\n💡 Check for typos in field names.");
1627 full_msg.push_str("\n See config.template.yaml for valid field names.");
1628 }
1629
1630 Error::generic(full_msg)
1631 })?
1632 };
1633
1634 Ok(config)
1635}
1636
1637pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1639 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1640 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1641 {
1642 serde_yaml::to_string(config)
1643 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1644 } else {
1645 serde_json::to_string_pretty(config)
1646 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1647 };
1648
1649 fs::write(path, content)
1650 .await
1651 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1652
1653 Ok(())
1654}
1655
1656pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1658 match load_config(&path).await {
1659 Ok(config) => {
1660 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1661 config
1662 }
1663 Err(e) => {
1664 tracing::warn!(
1665 "Failed to load config from {:?}: {}. Using defaults.",
1666 path.as_ref(),
1667 e
1668 );
1669 ServerConfig::default()
1670 }
1671 }
1672}
1673
1674pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1676 let config = ServerConfig::default();
1677 save_config(path, &config).await?;
1678 Ok(())
1679}
1680
1681pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1683 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1685 if let Ok(port_num) = port.parse() {
1686 config.http.port = port_num;
1687 }
1688 }
1689
1690 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1691 config.http.host = host;
1692 }
1693
1694 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1696 if let Ok(port_num) = port.parse() {
1697 config.websocket.port = port_num;
1698 }
1699 }
1700
1701 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1703 if let Ok(port_num) = port.parse() {
1704 config.grpc.port = port_num;
1705 }
1706 }
1707
1708 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1710 if let Ok(port_num) = port.parse() {
1711 config.smtp.port = port_num;
1712 }
1713 }
1714
1715 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1716 config.smtp.host = host;
1717 }
1718
1719 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1720 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1721 }
1722
1723 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1724 config.smtp.hostname = hostname;
1725 }
1726
1727 if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
1729 if let Ok(port_num) = port.parse() {
1730 config.tcp.port = port_num;
1731 }
1732 }
1733
1734 if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
1735 config.tcp.host = host;
1736 }
1737
1738 if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
1739 config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1740 }
1741
1742 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1744 if let Ok(port_num) = port.parse() {
1745 config.admin.port = port_num;
1746 }
1747 }
1748
1749 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1750 config.admin.enabled = true;
1751 }
1752
1753 if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
1755 config.admin.host = host;
1756 }
1757
1758 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1759 if !mount_path.trim().is_empty() {
1760 config.admin.mount_path = Some(mount_path);
1761 }
1762 }
1763
1764 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1765 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1766 config.admin.api_enabled = on;
1767 }
1768
1769 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1770 config.admin.prometheus_url = prometheus_url;
1771 }
1772
1773 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1775 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1776 config.core.latency_enabled = enabled;
1777 }
1778
1779 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1780 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1781 config.core.failures_enabled = enabled;
1782 }
1783
1784 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1785 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1786 config.core.overrides_enabled = enabled;
1787 }
1788
1789 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1790 let enabled =
1791 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1792 config.core.traffic_shaping_enabled = enabled;
1793 }
1794
1795 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1797 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1798 config.core.traffic_shaping.bandwidth.enabled = enabled;
1799 }
1800
1801 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1802 if let Ok(bytes) = max_bytes_per_sec.parse() {
1803 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1804 config.core.traffic_shaping.bandwidth.enabled = true;
1805 }
1806 }
1807
1808 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1809 if let Ok(bytes) = burst_capacity.parse() {
1810 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1811 }
1812 }
1813
1814 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1815 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1816 config.core.traffic_shaping.burst_loss.enabled = enabled;
1817 }
1818
1819 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1820 if let Ok(prob) = burst_probability.parse::<f64>() {
1821 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1822 config.core.traffic_shaping.burst_loss.enabled = true;
1823 }
1824 }
1825
1826 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1827 if let Ok(ms) = burst_duration.parse() {
1828 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1829 }
1830 }
1831
1832 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1833 if let Ok(rate) = loss_rate.parse::<f64>() {
1834 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1835 }
1836 }
1837
1838 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1839 if let Ok(ms) = recovery_time.parse() {
1840 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1841 }
1842 }
1843
1844 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1846 config.logging.level = level;
1847 }
1848
1849 config
1850}
1851
1852pub fn validate_config(config: &ServerConfig) -> Result<()> {
1854 if config.http.port == 0 {
1856 return Err(Error::generic("HTTP port cannot be 0"));
1857 }
1858 if config.websocket.port == 0 {
1859 return Err(Error::generic("WebSocket port cannot be 0"));
1860 }
1861 if config.grpc.port == 0 {
1862 return Err(Error::generic("gRPC port cannot be 0"));
1863 }
1864 if config.admin.port == 0 {
1865 return Err(Error::generic("Admin port cannot be 0"));
1866 }
1867
1868 let ports = [
1870 ("HTTP", config.http.port),
1871 ("WebSocket", config.websocket.port),
1872 ("gRPC", config.grpc.port),
1873 ("Admin", config.admin.port),
1874 ];
1875
1876 for i in 0..ports.len() {
1877 for j in (i + 1)..ports.len() {
1878 if ports[i].1 == ports[j].1 {
1879 return Err(Error::generic(format!(
1880 "Port conflict: {} and {} both use port {}",
1881 ports[i].0, ports[j].0, ports[i].1
1882 )));
1883 }
1884 }
1885 }
1886
1887 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1889 if !valid_levels.contains(&config.logging.level.as_str()) {
1890 return Err(Error::generic(format!(
1891 "Invalid log level: {}. Valid levels: {}",
1892 config.logging.level,
1893 valid_levels.join(", ")
1894 )));
1895 }
1896
1897 Ok(())
1898}
1899
1900pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1902 macro_rules! merge_field {
1904 ($field:ident) => {
1905 if let Some(override_val) = profile.$field {
1906 base.$field = override_val;
1907 }
1908 };
1909 }
1910
1911 merge_field!(http);
1912 merge_field!(websocket);
1913 merge_field!(graphql);
1914 merge_field!(grpc);
1915 merge_field!(mqtt);
1916 merge_field!(smtp);
1917 merge_field!(ftp);
1918 merge_field!(kafka);
1919 merge_field!(amqp);
1920 merge_field!(tcp);
1921 merge_field!(admin);
1922 merge_field!(chaining);
1923 merge_field!(core);
1924 merge_field!(logging);
1925 merge_field!(data);
1926 merge_field!(mockai);
1927 merge_field!(observability);
1928 merge_field!(multi_tenant);
1929 merge_field!(routes);
1930 merge_field!(protocols);
1931
1932 base
1933}
1934
1935pub async fn load_config_with_profile<P: AsRef<Path>>(
1937 path: P,
1938 profile_name: Option<&str>,
1939) -> Result<ServerConfig> {
1940 let mut config = load_config_auto(&path).await?;
1942
1943 if let Some(profile) = profile_name {
1945 if let Some(profile_config) = config.profiles.remove(profile) {
1946 tracing::info!("Applying profile: {}", profile);
1947 config = apply_profile(config, profile_config);
1948 } else {
1949 return Err(Error::generic(format!(
1950 "Profile '{}' not found in configuration. Available profiles: {}",
1951 profile,
1952 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
1953 )));
1954 }
1955 }
1956
1957 config.profiles.clear();
1959
1960 Ok(config)
1961}
1962
1963pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1965 use rquickjs::{Context, Runtime};
1966
1967 let content = fs::read_to_string(&path)
1968 .await
1969 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
1970
1971 let runtime = Runtime::new()
1973 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
1974 let context = Context::full(&runtime)
1975 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
1976
1977 context.with(|ctx| {
1978 let js_content = if path
1981 .as_ref()
1982 .extension()
1983 .and_then(|s| s.to_str())
1984 .map(|ext| ext == "ts")
1985 .unwrap_or(false)
1986 {
1987 strip_typescript_types(&content)?
1988 } else {
1989 content
1990 };
1991
1992 let result: rquickjs::Value = ctx
1994 .eval(js_content.as_bytes())
1995 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
1996
1997 let json_str: String = ctx
1999 .json_stringify(result)
2000 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
2001 .ok_or_else(|| Error::generic("JS config returned undefined"))?
2002 .get()
2003 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
2004
2005 serde_json::from_str(&json_str).map_err(|e| {
2007 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
2008 })
2009 })
2010}
2011
2012fn strip_typescript_types(content: &str) -> Result<String> {
2019 use regex::Regex;
2020
2021 let mut result = content.to_string();
2022
2023 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
2029 .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
2030 result = interface_re.replace_all(&result, "").to_string();
2031
2032 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
2034 .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
2035 result = type_alias_re.replace_all(&result, "").to_string();
2036
2037 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
2039 .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
2040 result = type_annotation_re.replace_all(&result, "").to_string();
2041
2042 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
2044 .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
2045 result = type_import_re.replace_all(&result, "").to_string();
2046
2047 let as_type_re = Regex::new(r"\s+as\s+\w+")
2049 .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
2050 result = as_type_re.replace_all(&result, "").to_string();
2051
2052 Ok(result)
2053}
2054
2055pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
2057 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
2058
2059 match ext {
2060 "ts" | "js" => load_config_from_js(&path).await,
2061 "yaml" | "yml" | "json" => load_config(&path).await,
2062 _ => Err(Error::generic(format!(
2063 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
2064 ext
2065 ))),
2066 }
2067}
2068
2069pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
2071 let current_dir = std::env::current_dir()
2072 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
2073
2074 let config_names = vec![
2075 "mockforge.config.ts",
2076 "mockforge.config.js",
2077 "mockforge.yaml",
2078 "mockforge.yml",
2079 ".mockforge.yaml",
2080 ".mockforge.yml",
2081 ];
2082
2083 for name in &config_names {
2085 let path = current_dir.join(name);
2086 if tokio::fs::metadata(&path).await.is_ok() {
2087 return Ok(path);
2088 }
2089 }
2090
2091 let mut dir = current_dir.clone();
2093 for _ in 0..5 {
2094 if let Some(parent) = dir.parent() {
2095 for name in &config_names {
2096 let path = parent.join(name);
2097 if tokio::fs::metadata(&path).await.is_ok() {
2098 return Ok(path);
2099 }
2100 }
2101 dir = parent.to_path_buf();
2102 } else {
2103 break;
2104 }
2105 }
2106
2107 Err(Error::generic(
2108 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
2109 ))
2110}
2111
2112#[cfg(test)]
2113mod tests {
2114 use super::*;
2115
2116 #[test]
2117 fn test_default_config() {
2118 let config = ServerConfig::default();
2119 assert_eq!(config.http.port, 3000);
2120 assert_eq!(config.websocket.port, 3001);
2121 assert_eq!(config.grpc.port, 50051);
2122 assert_eq!(config.admin.port, 9080);
2123 }
2124
2125 #[test]
2126 fn test_config_validation() {
2127 let mut config = ServerConfig::default();
2128 assert!(validate_config(&config).is_ok());
2129
2130 config.websocket.port = config.http.port;
2132 assert!(validate_config(&config).is_err());
2133
2134 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
2137 assert!(validate_config(&config).is_err());
2138 }
2139
2140 #[test]
2141 fn test_apply_profile() {
2142 let mut base = ServerConfig::default();
2143 assert_eq!(base.http.port, 3000);
2144
2145 let mut profile = ProfileConfig::default();
2146 profile.http = Some(HttpConfig {
2147 port: 8080,
2148 ..Default::default()
2149 });
2150 profile.logging = Some(LoggingConfig {
2151 level: "debug".to_string(),
2152 ..Default::default()
2153 });
2154
2155 let merged = apply_profile(base, profile);
2156 assert_eq!(merged.http.port, 8080);
2157 assert_eq!(merged.logging.level, "debug");
2158 assert_eq!(merged.websocket.port, 3001); }
2160
2161 #[test]
2162 fn test_strip_typescript_types() {
2163 let ts_code = r#"
2164interface Config {
2165 port: number;
2166 host: string;
2167}
2168
2169const config: Config = {
2170 port: 3000,
2171 host: "localhost"
2172} as Config;
2173"#;
2174
2175 let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
2176 assert!(!stripped.contains("interface"));
2177 assert!(!stripped.contains(": Config"));
2178 assert!(!stripped.contains("as Config"));
2179 }
2180}