1use crate::{Config as CoreConfig, Error, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7use tokio::fs;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(default)]
12pub struct AuthConfig {
13 pub jwt: Option<JwtConfig>,
15 pub oauth2: Option<OAuth2Config>,
17 pub basic_auth: Option<BasicAuthConfig>,
19 pub api_key: Option<ApiKeyConfig>,
21 pub require_auth: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct JwtConfig {
28 pub secret: Option<String>,
30 pub rsa_public_key: Option<String>,
32 pub ecdsa_public_key: Option<String>,
34 pub issuer: Option<String>,
36 pub audience: Option<String>,
38 pub algorithms: Vec<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct OAuth2Config {
45 pub client_id: String,
47 pub client_secret: String,
49 pub introspection_url: String,
51 pub auth_url: Option<String>,
53 pub token_url: Option<String>,
55 pub token_type_hint: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct BasicAuthConfig {
62 pub credentials: HashMap<String, String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ApiKeyConfig {
69 pub header_name: String,
71 pub query_name: Option<String>,
73 pub keys: Vec<String>,
75}
76
77impl Default for AuthConfig {
78 fn default() -> Self {
79 Self {
80 jwt: None,
81 oauth2: None,
82 basic_auth: None,
83 api_key: Some(ApiKeyConfig {
84 header_name: "X-API-Key".to_string(),
85 query_name: None,
86 keys: vec![],
87 }),
88 require_auth: false,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct RouteConfig {
96 pub path: String,
98 pub method: String,
100 pub request: Option<RouteRequestConfig>,
102 pub response: RouteResponseConfig,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct RouteRequestConfig {
109 pub validation: Option<RouteValidationConfig>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct RouteResponseConfig {
116 pub status: u16,
118 #[serde(default)]
120 pub headers: HashMap<String, String>,
121 pub body: Option<serde_json::Value>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RouteValidationConfig {
128 pub schema: serde_json::Value,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ProtocolConfig {
135 pub enabled: bool,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ProtocolsConfig {
142 pub http: ProtocolConfig,
144 pub graphql: ProtocolConfig,
146 pub grpc: ProtocolConfig,
148 pub websocket: ProtocolConfig,
150 pub smtp: ProtocolConfig,
152 pub mqtt: ProtocolConfig,
154 pub ftp: ProtocolConfig,
156 pub kafka: ProtocolConfig,
158 pub rabbitmq: ProtocolConfig,
160 pub amqp: ProtocolConfig,
162}
163
164impl Default for ProtocolsConfig {
165 fn default() -> Self {
166 Self {
167 http: ProtocolConfig { enabled: true },
168 graphql: ProtocolConfig { enabled: true },
169 grpc: ProtocolConfig { enabled: true },
170 websocket: ProtocolConfig { enabled: true },
171 smtp: ProtocolConfig { enabled: false },
172 mqtt: ProtocolConfig { enabled: true },
173 ftp: ProtocolConfig { enabled: false },
174 kafka: ProtocolConfig { enabled: false },
175 rabbitmq: ProtocolConfig { enabled: false },
176 amqp: ProtocolConfig { enabled: false },
177 }
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, Default)]
183#[serde(default)]
184pub struct ServerConfig {
185 pub http: HttpConfig,
187 pub websocket: WebSocketConfig,
189 pub graphql: GraphQLConfig,
191 pub grpc: GrpcConfig,
193 pub mqtt: MqttConfig,
195 pub smtp: SmtpConfig,
197 pub ftp: FtpConfig,
199 pub kafka: KafkaConfig,
201 pub amqp: AmqpConfig,
203 pub admin: AdminConfig,
205 pub chaining: ChainingConfig,
207 pub core: CoreConfig,
209 pub logging: LoggingConfig,
211 pub data: DataConfig,
213 pub observability: ObservabilityConfig,
215 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
217 #[serde(default)]
219 pub routes: Vec<RouteConfig>,
220 #[serde(default)]
222 pub protocols: ProtocolsConfig,
223 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
225 pub profiles: HashMap<String, ProfileConfig>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, Default)]
230#[serde(default)]
231pub struct ProfileConfig {
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub http: Option<HttpConfig>,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub websocket: Option<WebSocketConfig>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub graphql: Option<GraphQLConfig>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub grpc: Option<GrpcConfig>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub mqtt: Option<MqttConfig>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub smtp: Option<SmtpConfig>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub ftp: Option<FtpConfig>,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub kafka: Option<KafkaConfig>,
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub amqp: Option<AmqpConfig>,
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub admin: Option<AdminConfig>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub chaining: Option<ChainingConfig>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub core: Option<CoreConfig>,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub logging: Option<LoggingConfig>,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub data: Option<DataConfig>,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub observability: Option<ObservabilityConfig>,
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub routes: Option<Vec<RouteConfig>>,
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub protocols: Option<ProtocolsConfig>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct HttpValidationConfig {
293 pub mode: String,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct HttpCorsConfig {
300 pub enabled: bool,
302 #[serde(default)]
304 pub allowed_origins: Vec<String>,
305 #[serde(default)]
307 pub allowed_methods: Vec<String>,
308 #[serde(default)]
310 pub allowed_headers: Vec<String>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315#[serde(default)]
316pub struct HttpConfig {
317 pub enabled: bool,
319 pub port: u16,
321 pub host: String,
323 pub openapi_spec: Option<String>,
325 pub cors: Option<HttpCorsConfig>,
327 pub request_timeout_secs: u64,
329 pub validation: Option<HttpValidationConfig>,
331 pub aggregate_validation_errors: bool,
333 pub validate_responses: bool,
335 pub response_template_expand: bool,
337 pub validation_status: Option<u16>,
339 pub validation_overrides: std::collections::HashMap<String, String>,
341 pub skip_admin_validation: bool,
343 pub auth: Option<AuthConfig>,
345}
346
347impl Default for HttpConfig {
348 fn default() -> Self {
349 Self {
350 enabled: true,
351 port: 3000,
352 host: "0.0.0.0".to_string(),
353 openapi_spec: None,
354 cors: Some(HttpCorsConfig {
355 enabled: true,
356 allowed_origins: vec!["*".to_string()],
357 allowed_methods: vec![
358 "GET".to_string(),
359 "POST".to_string(),
360 "PUT".to_string(),
361 "DELETE".to_string(),
362 "PATCH".to_string(),
363 "OPTIONS".to_string(),
364 ],
365 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
366 }),
367 request_timeout_secs: 30,
368 validation: Some(HttpValidationConfig {
369 mode: "enforce".to_string(),
370 }),
371 aggregate_validation_errors: true,
372 validate_responses: false,
373 response_template_expand: false,
374 validation_status: None,
375 validation_overrides: std::collections::HashMap::new(),
376 skip_admin_validation: true,
377 auth: None,
378 }
379 }
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384#[serde(default)]
385pub struct WebSocketConfig {
386 pub enabled: bool,
388 pub port: u16,
390 pub host: String,
392 pub replay_file: Option<String>,
394 pub connection_timeout_secs: u64,
396}
397
398impl Default for WebSocketConfig {
399 fn default() -> Self {
400 Self {
401 enabled: true,
402 port: 3001,
403 host: "0.0.0.0".to_string(),
404 replay_file: None,
405 connection_timeout_secs: 300,
406 }
407 }
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412#[serde(default)]
413pub struct GrpcConfig {
414 pub enabled: bool,
416 pub port: u16,
418 pub host: String,
420 pub proto_dir: Option<String>,
422 pub tls: Option<TlsConfig>,
424}
425
426impl Default for GrpcConfig {
427 fn default() -> Self {
428 Self {
429 enabled: true,
430 port: 50051,
431 host: "0.0.0.0".to_string(),
432 proto_dir: None,
433 tls: None,
434 }
435 }
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(default)]
441pub struct GraphQLConfig {
442 pub enabled: bool,
444 pub port: u16,
446 pub host: String,
448 pub schema_path: Option<String>,
450 pub handlers_dir: Option<String>,
452 pub playground_enabled: bool,
454 pub upstream_url: Option<String>,
456 pub introspection_enabled: bool,
458}
459
460impl Default for GraphQLConfig {
461 fn default() -> Self {
462 Self {
463 enabled: true,
464 port: 4000,
465 host: "0.0.0.0".to_string(),
466 schema_path: None,
467 handlers_dir: None,
468 playground_enabled: true,
469 upstream_url: None,
470 introspection_enabled: true,
471 }
472 }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct TlsConfig {
478 pub cert_path: String,
480 pub key_path: String,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486#[serde(default)]
487pub struct MqttConfig {
488 pub enabled: bool,
490 pub port: u16,
492 pub host: String,
494 pub max_connections: usize,
496 pub max_packet_size: usize,
498 pub keep_alive_secs: u16,
500 pub fixtures_dir: Option<std::path::PathBuf>,
502 pub enable_retained_messages: bool,
504 pub max_retained_messages: usize,
506}
507
508impl Default for MqttConfig {
509 fn default() -> Self {
510 Self {
511 enabled: false,
512 port: 1883,
513 host: "0.0.0.0".to_string(),
514 max_connections: 1000,
515 max_packet_size: 268435456, keep_alive_secs: 60,
517 fixtures_dir: None,
518 enable_retained_messages: true,
519 max_retained_messages: 10000,
520 }
521 }
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526#[serde(default)]
527pub struct SmtpConfig {
528 pub enabled: bool,
530 pub port: u16,
532 pub host: String,
534 pub hostname: String,
536 pub fixtures_dir: Option<std::path::PathBuf>,
538 pub timeout_secs: u64,
540 pub max_connections: usize,
542 pub enable_mailbox: bool,
544 pub max_mailbox_messages: usize,
546 pub enable_starttls: bool,
548 pub tls_cert_path: Option<std::path::PathBuf>,
550 pub tls_key_path: Option<std::path::PathBuf>,
552}
553
554impl Default for SmtpConfig {
555 fn default() -> Self {
556 Self {
557 enabled: false,
558 port: 1025,
559 host: "0.0.0.0".to_string(),
560 hostname: "mockforge-smtp".to_string(),
561 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
562 timeout_secs: 300,
563 max_connections: 10,
564 enable_mailbox: true,
565 max_mailbox_messages: 1000,
566 enable_starttls: false,
567 tls_cert_path: None,
568 tls_key_path: None,
569 }
570 }
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
575#[serde(default)]
576pub struct FtpConfig {
577 pub enabled: bool,
579 pub port: u16,
581 pub host: String,
583 pub passive_ports: (u16, u16),
585 pub max_connections: usize,
587 pub timeout_secs: u64,
589 pub allow_anonymous: bool,
591 pub fixtures_dir: Option<std::path::PathBuf>,
593 pub virtual_root: std::path::PathBuf,
595}
596
597impl Default for FtpConfig {
598 fn default() -> Self {
599 Self {
600 enabled: false,
601 port: 2121,
602 host: "0.0.0.0".to_string(),
603 passive_ports: (50000, 51000),
604 max_connections: 100,
605 timeout_secs: 300,
606 allow_anonymous: true,
607 fixtures_dir: None,
608 virtual_root: std::path::PathBuf::from("/mockforge"),
609 }
610 }
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize)]
615#[serde(default)]
616pub struct KafkaConfig {
617 pub enabled: bool,
619 pub port: u16,
621 pub host: String,
623 pub broker_id: i32,
625 pub max_connections: usize,
627 pub log_retention_ms: i64,
629 pub log_segment_bytes: i64,
631 pub fixtures_dir: Option<std::path::PathBuf>,
633 pub auto_create_topics: bool,
635 pub default_partitions: i32,
637 pub default_replication_factor: i16,
639}
640
641impl Default for KafkaConfig {
642 fn default() -> Self {
643 Self {
644 enabled: false,
645 port: 9092, host: "0.0.0.0".to_string(),
647 broker_id: 1,
648 max_connections: 1000,
649 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
652 auto_create_topics: true,
653 default_partitions: 3,
654 default_replication_factor: 1,
655 }
656 }
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
661#[serde(default)]
662pub struct AmqpConfig {
663 pub enabled: bool,
665 pub port: u16,
667 pub host: String,
669 pub max_connections: usize,
671 pub max_channels_per_connection: u16,
673 pub frame_max: u32,
675 pub heartbeat_interval: u16,
677 pub fixtures_dir: Option<std::path::PathBuf>,
679 pub virtual_hosts: Vec<String>,
681}
682
683impl Default for AmqpConfig {
684 fn default() -> Self {
685 Self {
686 enabled: false,
687 port: 5672, host: "0.0.0.0".to_string(),
689 max_connections: 1000,
690 max_channels_per_connection: 100,
691 frame_max: 131072, heartbeat_interval: 60,
693 fixtures_dir: None,
694 virtual_hosts: vec!["/".to_string()],
695 }
696 }
697}
698
699#[derive(Debug, Clone, Serialize, Deserialize)]
701#[serde(default)]
702pub struct AdminConfig {
703 pub enabled: bool,
705 pub port: u16,
707 pub host: String,
709 pub auth_required: bool,
711 pub username: Option<String>,
713 pub password: Option<String>,
715 pub mount_path: Option<String>,
717 pub api_enabled: bool,
719 pub prometheus_url: String,
721}
722
723impl Default for AdminConfig {
724 fn default() -> Self {
725 Self {
726 enabled: false,
727 port: 9080,
728 host: "127.0.0.1".to_string(),
729 auth_required: false,
730 username: None,
731 password: None,
732 mount_path: None,
733 api_enabled: true,
734 prometheus_url: "http://localhost:9090".to_string(),
735 }
736 }
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
741#[serde(default)]
742pub struct LoggingConfig {
743 pub level: String,
745 pub json_format: bool,
747 pub file_path: Option<String>,
749 pub max_file_size_mb: u64,
751 pub max_files: u32,
753}
754
755impl Default for LoggingConfig {
756 fn default() -> Self {
757 Self {
758 level: "info".to_string(),
759 json_format: false,
760 file_path: None,
761 max_file_size_mb: 10,
762 max_files: 5,
763 }
764 }
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize)]
768#[serde(default, rename_all = "camelCase")]
769pub struct ChainingConfig {
770 pub enabled: bool,
772 pub max_chain_length: usize,
774 pub global_timeout_secs: u64,
776 pub enable_parallel_execution: bool,
778}
779
780impl Default for ChainingConfig {
781 fn default() -> Self {
782 Self {
783 enabled: false,
784 max_chain_length: 20,
785 global_timeout_secs: 300,
786 enable_parallel_execution: false,
787 }
788 }
789}
790
791#[derive(Debug, Clone, Serialize, Deserialize)]
793#[serde(default)]
794pub struct DataConfig {
795 pub default_rows: usize,
797 pub default_format: String,
799 pub locale: String,
801 pub templates: HashMap<String, String>,
803 pub rag: RagConfig,
805}
806
807impl Default for DataConfig {
808 fn default() -> Self {
809 Self {
810 default_rows: 100,
811 default_format: "json".to_string(),
812 locale: "en".to_string(),
813 templates: HashMap::new(),
814 rag: RagConfig::default(),
815 }
816 }
817}
818
819#[derive(Debug, Clone, Serialize, Deserialize)]
821#[serde(default)]
822pub struct RagConfig {
823 pub enabled: bool,
825 #[serde(default)]
827 pub provider: String,
828 pub api_endpoint: Option<String>,
830 pub api_key: Option<String>,
832 pub model: Option<String>,
834 #[serde(default = "default_max_tokens")]
836 pub max_tokens: usize,
837 #[serde(default = "default_temperature")]
839 pub temperature: f64,
840 pub context_window: usize,
842 #[serde(default = "default_true")]
844 pub caching: bool,
845 #[serde(default = "default_cache_ttl")]
847 pub cache_ttl_secs: u64,
848 #[serde(default = "default_timeout")]
850 pub timeout_secs: u64,
851 #[serde(default = "default_max_retries")]
853 pub max_retries: usize,
854}
855
856fn default_max_tokens() -> usize {
857 1024
858}
859
860fn default_temperature() -> f64 {
861 0.7
862}
863
864fn default_true() -> bool {
865 true
866}
867
868fn default_cache_ttl() -> u64 {
869 3600
870}
871
872fn default_timeout() -> u64 {
873 30
874}
875
876fn default_max_retries() -> usize {
877 3
878}
879
880impl Default for RagConfig {
881 fn default() -> Self {
882 Self {
883 enabled: false,
884 provider: "openai".to_string(),
885 api_endpoint: None,
886 api_key: None,
887 model: Some("gpt-3.5-turbo".to_string()),
888 max_tokens: default_max_tokens(),
889 temperature: default_temperature(),
890 context_window: 4000,
891 caching: default_true(),
892 cache_ttl_secs: default_cache_ttl(),
893 timeout_secs: default_timeout(),
894 max_retries: default_max_retries(),
895 }
896 }
897}
898
899#[derive(Debug, Clone, Serialize, Deserialize, Default)]
901#[serde(default)]
902pub struct ObservabilityConfig {
903 pub prometheus: PrometheusConfig,
905 pub opentelemetry: Option<OpenTelemetryConfig>,
907 pub recorder: Option<RecorderConfig>,
909 pub chaos: Option<ChaosEngConfig>,
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize)]
915#[serde(default)]
916pub struct PrometheusConfig {
917 pub enabled: bool,
919 pub port: u16,
921 pub host: String,
923 pub path: String,
925}
926
927impl Default for PrometheusConfig {
928 fn default() -> Self {
929 Self {
930 enabled: true,
931 port: 9090,
932 host: "0.0.0.0".to_string(),
933 path: "/metrics".to_string(),
934 }
935 }
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
940#[serde(default)]
941pub struct OpenTelemetryConfig {
942 pub enabled: bool,
944 pub service_name: String,
946 pub environment: String,
948 pub jaeger_endpoint: String,
950 pub otlp_endpoint: Option<String>,
952 pub protocol: String,
954 pub sampling_rate: f64,
956}
957
958impl Default for OpenTelemetryConfig {
959 fn default() -> Self {
960 Self {
961 enabled: false,
962 service_name: "mockforge".to_string(),
963 environment: "development".to_string(),
964 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
965 otlp_endpoint: Some("http://localhost:4317".to_string()),
966 protocol: "grpc".to_string(),
967 sampling_rate: 1.0,
968 }
969 }
970}
971
972#[derive(Debug, Clone, Serialize, Deserialize)]
974#[serde(default)]
975pub struct RecorderConfig {
976 pub enabled: bool,
978 pub database_path: String,
980 pub api_enabled: bool,
982 pub api_port: Option<u16>,
984 pub max_requests: i64,
986 pub retention_days: i64,
988 pub record_http: bool,
990 pub record_grpc: bool,
992 pub record_websocket: bool,
994 pub record_graphql: bool,
996}
997
998impl Default for RecorderConfig {
999 fn default() -> Self {
1000 Self {
1001 enabled: false,
1002 database_path: "./mockforge-recordings.db".to_string(),
1003 api_enabled: true,
1004 api_port: None,
1005 max_requests: 10000,
1006 retention_days: 7,
1007 record_http: true,
1008 record_grpc: true,
1009 record_websocket: true,
1010 record_graphql: true,
1011 }
1012 }
1013}
1014
1015#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1017#[serde(default)]
1018pub struct ChaosEngConfig {
1019 pub enabled: bool,
1021 pub latency: Option<LatencyInjectionConfig>,
1023 pub fault_injection: Option<FaultConfig>,
1025 pub rate_limit: Option<RateLimitingConfig>,
1027 pub traffic_shaping: Option<NetworkShapingConfig>,
1029 pub scenario: Option<String>,
1031}
1032
1033#[derive(Debug, Clone, Serialize, Deserialize)]
1035pub struct LatencyInjectionConfig {
1036 pub enabled: bool,
1037 pub fixed_delay_ms: Option<u64>,
1038 pub random_delay_range_ms: Option<(u64, u64)>,
1039 pub jitter_percent: f64,
1040 pub probability: f64,
1041}
1042
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1045pub struct FaultConfig {
1046 pub enabled: bool,
1047 pub http_errors: Vec<u16>,
1048 pub http_error_probability: f64,
1049 pub connection_errors: bool,
1050 pub connection_error_probability: f64,
1051 pub timeout_errors: bool,
1052 pub timeout_ms: u64,
1053 pub timeout_probability: f64,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1058pub struct RateLimitingConfig {
1059 pub enabled: bool,
1060 pub requests_per_second: u32,
1061 pub burst_size: u32,
1062 pub per_ip: bool,
1063 pub per_endpoint: bool,
1064}
1065
1066#[derive(Debug, Clone, Serialize, Deserialize)]
1068pub struct NetworkShapingConfig {
1069 pub enabled: bool,
1070 pub bandwidth_limit_bps: u64,
1071 pub packet_loss_percent: f64,
1072 pub max_connections: u32,
1073}
1074
1075pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1077 let content = fs::read_to_string(&path)
1078 .await
1079 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1080
1081 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1082 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1083 {
1084 serde_yaml::from_str(&content)
1085 .map_err(|e| Error::generic(format!("Failed to parse YAML config: {}", e)))?
1086 } else {
1087 serde_json::from_str(&content)
1088 .map_err(|e| Error::generic(format!("Failed to parse JSON config: {}", e)))?
1089 };
1090
1091 Ok(config)
1092}
1093
1094pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1096 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1097 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1098 {
1099 serde_yaml::to_string(config)
1100 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1101 } else {
1102 serde_json::to_string_pretty(config)
1103 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1104 };
1105
1106 fs::write(path, content)
1107 .await
1108 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1109
1110 Ok(())
1111}
1112
1113pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1115 match load_config(&path).await {
1116 Ok(config) => {
1117 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1118 config
1119 }
1120 Err(e) => {
1121 tracing::warn!(
1122 "Failed to load config from {:?}: {}. Using defaults.",
1123 path.as_ref(),
1124 e
1125 );
1126 ServerConfig::default()
1127 }
1128 }
1129}
1130
1131pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1133 let config = ServerConfig::default();
1134 save_config(path, &config).await?;
1135 Ok(())
1136}
1137
1138pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1140 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1142 if let Ok(port_num) = port.parse() {
1143 config.http.port = port_num;
1144 }
1145 }
1146
1147 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1148 config.http.host = host;
1149 }
1150
1151 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1153 if let Ok(port_num) = port.parse() {
1154 config.websocket.port = port_num;
1155 }
1156 }
1157
1158 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1160 if let Ok(port_num) = port.parse() {
1161 config.grpc.port = port_num;
1162 }
1163 }
1164
1165 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1167 if let Ok(port_num) = port.parse() {
1168 config.smtp.port = port_num;
1169 }
1170 }
1171
1172 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1173 config.smtp.host = host;
1174 }
1175
1176 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1177 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1178 }
1179
1180 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1181 config.smtp.hostname = hostname;
1182 }
1183
1184 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1186 if let Ok(port_num) = port.parse() {
1187 config.admin.port = port_num;
1188 }
1189 }
1190
1191 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1192 config.admin.enabled = true;
1193 }
1194
1195 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1196 if !mount_path.trim().is_empty() {
1197 config.admin.mount_path = Some(mount_path);
1198 }
1199 }
1200
1201 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1202 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1203 config.admin.api_enabled = on;
1204 }
1205
1206 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1207 config.admin.prometheus_url = prometheus_url;
1208 }
1209
1210 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1212 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1213 config.core.latency_enabled = enabled;
1214 }
1215
1216 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1217 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1218 config.core.failures_enabled = enabled;
1219 }
1220
1221 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1222 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1223 config.core.overrides_enabled = enabled;
1224 }
1225
1226 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1227 let enabled =
1228 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1229 config.core.traffic_shaping_enabled = enabled;
1230 }
1231
1232 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1234 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1235 config.core.traffic_shaping.bandwidth.enabled = enabled;
1236 }
1237
1238 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1239 if let Ok(bytes) = max_bytes_per_sec.parse() {
1240 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1241 config.core.traffic_shaping.bandwidth.enabled = true;
1242 }
1243 }
1244
1245 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1246 if let Ok(bytes) = burst_capacity.parse() {
1247 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1248 }
1249 }
1250
1251 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1252 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1253 config.core.traffic_shaping.burst_loss.enabled = enabled;
1254 }
1255
1256 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1257 if let Ok(prob) = burst_probability.parse::<f64>() {
1258 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1259 config.core.traffic_shaping.burst_loss.enabled = true;
1260 }
1261 }
1262
1263 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1264 if let Ok(ms) = burst_duration.parse() {
1265 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1266 }
1267 }
1268
1269 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1270 if let Ok(rate) = loss_rate.parse::<f64>() {
1271 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1272 }
1273 }
1274
1275 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1276 if let Ok(ms) = recovery_time.parse() {
1277 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1278 }
1279 }
1280
1281 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1283 config.logging.level = level;
1284 }
1285
1286 config
1287}
1288
1289pub fn validate_config(config: &ServerConfig) -> Result<()> {
1291 if config.http.port == 0 {
1293 return Err(Error::generic("HTTP port cannot be 0"));
1294 }
1295 if config.websocket.port == 0 {
1296 return Err(Error::generic("WebSocket port cannot be 0"));
1297 }
1298 if config.grpc.port == 0 {
1299 return Err(Error::generic("gRPC port cannot be 0"));
1300 }
1301 if config.admin.port == 0 {
1302 return Err(Error::generic("Admin port cannot be 0"));
1303 }
1304
1305 let ports = [
1307 ("HTTP", config.http.port),
1308 ("WebSocket", config.websocket.port),
1309 ("gRPC", config.grpc.port),
1310 ("Admin", config.admin.port),
1311 ];
1312
1313 for i in 0..ports.len() {
1314 for j in (i + 1)..ports.len() {
1315 if ports[i].1 == ports[j].1 {
1316 return Err(Error::generic(format!(
1317 "Port conflict: {} and {} both use port {}",
1318 ports[i].0, ports[j].0, ports[i].1
1319 )));
1320 }
1321 }
1322 }
1323
1324 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1326 if !valid_levels.contains(&config.logging.level.as_str()) {
1327 return Err(Error::generic(format!(
1328 "Invalid log level: {}. Valid levels: {}",
1329 config.logging.level,
1330 valid_levels.join(", ")
1331 )));
1332 }
1333
1334 Ok(())
1335}
1336
1337pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1339 macro_rules! merge_field {
1341 ($field:ident) => {
1342 if let Some(override_val) = profile.$field {
1343 base.$field = override_val;
1344 }
1345 };
1346 }
1347
1348 merge_field!(http);
1349 merge_field!(websocket);
1350 merge_field!(graphql);
1351 merge_field!(grpc);
1352 merge_field!(mqtt);
1353 merge_field!(smtp);
1354 merge_field!(ftp);
1355 merge_field!(kafka);
1356 merge_field!(amqp);
1357 merge_field!(admin);
1358 merge_field!(chaining);
1359 merge_field!(core);
1360 merge_field!(logging);
1361 merge_field!(data);
1362 merge_field!(observability);
1363 merge_field!(multi_tenant);
1364 merge_field!(routes);
1365 merge_field!(protocols);
1366
1367 base
1368}
1369
1370pub async fn load_config_with_profile<P: AsRef<Path>>(
1372 path: P,
1373 profile_name: Option<&str>,
1374) -> Result<ServerConfig> {
1375 let mut config = load_config_auto(&path).await?;
1377
1378 if let Some(profile) = profile_name {
1380 if let Some(profile_config) = config.profiles.remove(profile) {
1381 tracing::info!("Applying profile: {}", profile);
1382 config = apply_profile(config, profile_config);
1383 } else {
1384 return Err(Error::generic(format!(
1385 "Profile '{}' not found in configuration. Available profiles: {}",
1386 profile,
1387 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
1388 )));
1389 }
1390 }
1391
1392 config.profiles.clear();
1394
1395 Ok(config)
1396}
1397
1398pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1400 use rquickjs::{Context, Runtime};
1401
1402 let content = fs::read_to_string(&path)
1403 .await
1404 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
1405
1406 let runtime = Runtime::new()
1408 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
1409 let context = Context::full(&runtime)
1410 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
1411
1412 context.with(|ctx| {
1413 let js_content = if path
1416 .as_ref()
1417 .extension()
1418 .and_then(|s| s.to_str())
1419 .map(|ext| ext == "ts")
1420 .unwrap_or(false)
1421 {
1422 strip_typescript_types(&content)
1423 } else {
1424 content
1425 };
1426
1427 let result: rquickjs::Value = ctx
1429 .eval(js_content.as_bytes())
1430 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
1431
1432 let json_str: String = ctx
1434 .json_stringify(result)
1435 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
1436 .ok_or_else(|| Error::generic("JS config returned undefined"))?
1437 .get()
1438 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
1439
1440 serde_json::from_str(&json_str).map_err(|e| {
1442 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
1443 })
1444 })
1445}
1446
1447fn strip_typescript_types(content: &str) -> String {
1450 use regex::Regex;
1451
1452 let mut result = content.to_string();
1453
1454 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*").unwrap();
1456 result = interface_re.replace_all(&result, "").to_string();
1457
1458 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*").unwrap();
1460 result = type_alias_re.replace_all(&result, "").to_string();
1461
1462 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?").unwrap();
1464 result = type_annotation_re.replace_all(&result, "").to_string();
1465
1466 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$").unwrap();
1468 result = type_import_re.replace_all(&result, "").to_string();
1469
1470 let as_type_re = Regex::new(r"\s+as\s+\w+").unwrap();
1472 result = as_type_re.replace_all(&result, "").to_string();
1473
1474 result
1475}
1476
1477pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1479 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
1480
1481 match ext {
1482 "ts" | "js" => load_config_from_js(&path).await,
1483 "yaml" | "yml" | "json" => load_config(&path).await,
1484 _ => Err(Error::generic(format!(
1485 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
1486 ext
1487 ))),
1488 }
1489}
1490
1491pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
1493 let current_dir = std::env::current_dir()
1494 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
1495
1496 let config_names = vec![
1497 "mockforge.config.ts",
1498 "mockforge.config.js",
1499 "mockforge.yaml",
1500 "mockforge.yml",
1501 ".mockforge.yaml",
1502 ".mockforge.yml",
1503 ];
1504
1505 for name in &config_names {
1507 let path = current_dir.join(name);
1508 if tokio::fs::metadata(&path).await.is_ok() {
1509 return Ok(path);
1510 }
1511 }
1512
1513 let mut dir = current_dir.clone();
1515 for _ in 0..5 {
1516 if let Some(parent) = dir.parent() {
1517 for name in &config_names {
1518 let path = parent.join(name);
1519 if tokio::fs::metadata(&path).await.is_ok() {
1520 return Ok(path);
1521 }
1522 }
1523 dir = parent.to_path_buf();
1524 } else {
1525 break;
1526 }
1527 }
1528
1529 Err(Error::generic(
1530 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
1531 ))
1532}
1533
1534#[cfg(test)]
1535mod tests {
1536 use super::*;
1537
1538 #[test]
1539 fn test_default_config() {
1540 let config = ServerConfig::default();
1541 assert_eq!(config.http.port, 3000);
1542 assert_eq!(config.websocket.port, 3001);
1543 assert_eq!(config.grpc.port, 50051);
1544 assert_eq!(config.admin.port, 9080);
1545 }
1546
1547 #[test]
1548 fn test_config_validation() {
1549 let mut config = ServerConfig::default();
1550 assert!(validate_config(&config).is_ok());
1551
1552 config.websocket.port = config.http.port;
1554 assert!(validate_config(&config).is_err());
1555
1556 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
1559 assert!(validate_config(&config).is_err());
1560 }
1561
1562 #[test]
1563 fn test_apply_profile() {
1564 let mut base = ServerConfig::default();
1565 assert_eq!(base.http.port, 3000);
1566
1567 let mut profile = ProfileConfig::default();
1568 profile.http = Some(HttpConfig {
1569 port: 8080,
1570 ..Default::default()
1571 });
1572 profile.logging = Some(LoggingConfig {
1573 level: "debug".to_string(),
1574 ..Default::default()
1575 });
1576
1577 let merged = apply_profile(base, profile);
1578 assert_eq!(merged.http.port, 8080);
1579 assert_eq!(merged.logging.level, "debug");
1580 assert_eq!(merged.websocket.port, 3001); }
1582
1583 #[test]
1584 fn test_strip_typescript_types() {
1585 let ts_code = r#"
1586interface Config {
1587 port: number;
1588 host: string;
1589}
1590
1591const config: Config = {
1592 port: 3000,
1593 host: "localhost"
1594} as Config;
1595"#;
1596
1597 let stripped = strip_typescript_types(ts_code);
1598 assert!(!stripped.contains("interface"));
1599 assert!(!stripped.contains(": Config"));
1600 assert!(!stripped.contains("as Config"));
1601 }
1602}