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 let default_host = if std::env::var("DOCKER_CONTAINER").is_ok()
728 || std::env::var("container").is_ok()
729 || std::path::Path::new("/.dockerenv").exists()
730 {
731 "0.0.0.0".to_string()
732 } else {
733 "127.0.0.1".to_string()
734 };
735
736 Self {
737 enabled: false,
738 port: 9080,
739 host: default_host,
740 auth_required: false,
741 username: None,
742 password: None,
743 mount_path: None,
744 api_enabled: true,
745 prometheus_url: "http://localhost:9090".to_string(),
746 }
747 }
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
752#[serde(default)]
753pub struct LoggingConfig {
754 pub level: String,
756 pub json_format: bool,
758 pub file_path: Option<String>,
760 pub max_file_size_mb: u64,
762 pub max_files: u32,
764}
765
766impl Default for LoggingConfig {
767 fn default() -> Self {
768 Self {
769 level: "info".to_string(),
770 json_format: false,
771 file_path: None,
772 max_file_size_mb: 10,
773 max_files: 5,
774 }
775 }
776}
777
778#[derive(Debug, Clone, Serialize, Deserialize)]
780#[serde(default, rename_all = "camelCase")]
781pub struct ChainingConfig {
782 pub enabled: bool,
784 pub max_chain_length: usize,
786 pub global_timeout_secs: u64,
788 pub enable_parallel_execution: bool,
790}
791
792impl Default for ChainingConfig {
793 fn default() -> Self {
794 Self {
795 enabled: false,
796 max_chain_length: 20,
797 global_timeout_secs: 300,
798 enable_parallel_execution: false,
799 }
800 }
801}
802
803#[derive(Debug, Clone, Serialize, Deserialize)]
805#[serde(default)]
806pub struct DataConfig {
807 pub default_rows: usize,
809 pub default_format: String,
811 pub locale: String,
813 pub templates: HashMap<String, String>,
815 pub rag: RagConfig,
817}
818
819impl Default for DataConfig {
820 fn default() -> Self {
821 Self {
822 default_rows: 100,
823 default_format: "json".to_string(),
824 locale: "en".to_string(),
825 templates: HashMap::new(),
826 rag: RagConfig::default(),
827 }
828 }
829}
830
831#[derive(Debug, Clone, Serialize, Deserialize)]
833#[serde(default)]
834pub struct RagConfig {
835 pub enabled: bool,
837 #[serde(default)]
839 pub provider: String,
840 pub api_endpoint: Option<String>,
842 pub api_key: Option<String>,
844 pub model: Option<String>,
846 #[serde(default = "default_max_tokens")]
848 pub max_tokens: usize,
849 #[serde(default = "default_temperature")]
851 pub temperature: f64,
852 pub context_window: usize,
854 #[serde(default = "default_true")]
856 pub caching: bool,
857 #[serde(default = "default_cache_ttl")]
859 pub cache_ttl_secs: u64,
860 #[serde(default = "default_timeout")]
862 pub timeout_secs: u64,
863 #[serde(default = "default_max_retries")]
865 pub max_retries: usize,
866}
867
868fn default_max_tokens() -> usize {
869 1024
870}
871
872fn default_temperature() -> f64 {
873 0.7
874}
875
876fn default_true() -> bool {
877 true
878}
879
880fn default_cache_ttl() -> u64 {
881 3600
882}
883
884fn default_timeout() -> u64 {
885 30
886}
887
888fn default_max_retries() -> usize {
889 3
890}
891
892impl Default for RagConfig {
893 fn default() -> Self {
894 Self {
895 enabled: false,
896 provider: "openai".to_string(),
897 api_endpoint: None,
898 api_key: None,
899 model: Some("gpt-3.5-turbo".to_string()),
900 max_tokens: default_max_tokens(),
901 temperature: default_temperature(),
902 context_window: 4000,
903 caching: default_true(),
904 cache_ttl_secs: default_cache_ttl(),
905 timeout_secs: default_timeout(),
906 max_retries: default_max_retries(),
907 }
908 }
909}
910
911#[derive(Debug, Clone, Serialize, Deserialize, Default)]
913#[serde(default)]
914pub struct ObservabilityConfig {
915 pub prometheus: PrometheusConfig,
917 pub opentelemetry: Option<OpenTelemetryConfig>,
919 pub recorder: Option<RecorderConfig>,
921 pub chaos: Option<ChaosEngConfig>,
923}
924
925#[derive(Debug, Clone, Serialize, Deserialize)]
927#[serde(default)]
928pub struct PrometheusConfig {
929 pub enabled: bool,
931 pub port: u16,
933 pub host: String,
935 pub path: String,
937}
938
939impl Default for PrometheusConfig {
940 fn default() -> Self {
941 Self {
942 enabled: true,
943 port: 9090,
944 host: "0.0.0.0".to_string(),
945 path: "/metrics".to_string(),
946 }
947 }
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize)]
952#[serde(default)]
953pub struct OpenTelemetryConfig {
954 pub enabled: bool,
956 pub service_name: String,
958 pub environment: String,
960 pub jaeger_endpoint: String,
962 pub otlp_endpoint: Option<String>,
964 pub protocol: String,
966 pub sampling_rate: f64,
968}
969
970impl Default for OpenTelemetryConfig {
971 fn default() -> Self {
972 Self {
973 enabled: false,
974 service_name: "mockforge".to_string(),
975 environment: "development".to_string(),
976 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
977 otlp_endpoint: Some("http://localhost:4317".to_string()),
978 protocol: "grpc".to_string(),
979 sampling_rate: 1.0,
980 }
981 }
982}
983
984#[derive(Debug, Clone, Serialize, Deserialize)]
986#[serde(default)]
987pub struct RecorderConfig {
988 pub enabled: bool,
990 pub database_path: String,
992 pub api_enabled: bool,
994 pub api_port: Option<u16>,
996 pub max_requests: i64,
998 pub retention_days: i64,
1000 pub record_http: bool,
1002 pub record_grpc: bool,
1004 pub record_websocket: bool,
1006 pub record_graphql: bool,
1008}
1009
1010impl Default for RecorderConfig {
1011 fn default() -> Self {
1012 Self {
1013 enabled: false,
1014 database_path: "./mockforge-recordings.db".to_string(),
1015 api_enabled: true,
1016 api_port: None,
1017 max_requests: 10000,
1018 retention_days: 7,
1019 record_http: true,
1020 record_grpc: true,
1021 record_websocket: true,
1022 record_graphql: true,
1023 }
1024 }
1025}
1026
1027#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1029#[serde(default)]
1030pub struct ChaosEngConfig {
1031 pub enabled: bool,
1033 pub latency: Option<LatencyInjectionConfig>,
1035 pub fault_injection: Option<FaultConfig>,
1037 pub rate_limit: Option<RateLimitingConfig>,
1039 pub traffic_shaping: Option<NetworkShapingConfig>,
1041 pub scenario: Option<String>,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1047pub struct LatencyInjectionConfig {
1048 pub enabled: bool,
1050 pub fixed_delay_ms: Option<u64>,
1052 pub random_delay_range_ms: Option<(u64, u64)>,
1054 pub jitter_percent: f64,
1056 pub probability: f64,
1058}
1059
1060#[derive(Debug, Clone, Serialize, Deserialize)]
1062pub struct FaultConfig {
1063 pub enabled: bool,
1065 pub http_errors: Vec<u16>,
1067 pub http_error_probability: f64,
1069 pub connection_errors: bool,
1071 pub connection_error_probability: f64,
1073 pub timeout_errors: bool,
1075 pub timeout_ms: u64,
1077 pub timeout_probability: f64,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize)]
1083pub struct RateLimitingConfig {
1084 pub enabled: bool,
1086 pub requests_per_second: u32,
1088 pub burst_size: u32,
1090 pub per_ip: bool,
1092 pub per_endpoint: bool,
1094}
1095
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1098pub struct NetworkShapingConfig {
1099 pub enabled: bool,
1101 pub bandwidth_limit_bps: u64,
1103 pub packet_loss_percent: f64,
1105 pub max_connections: u32,
1107}
1108
1109pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1111 let content = fs::read_to_string(&path)
1112 .await
1113 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1114
1115 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1117 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1118 {
1119 serde_yaml::from_str(&content).map_err(|e| {
1120 let error_msg = e.to_string();
1122 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
1123
1124 if error_msg.contains("missing field") {
1126 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1127 full_msg.push_str("\n Omit fields you don't need - MockForge will use sensible defaults.");
1128 full_msg.push_str("\n See config.template.yaml for all available options.");
1129 } else if error_msg.contains("unknown field") {
1130 full_msg.push_str("\n\n💡 Check for typos in field names.");
1131 full_msg.push_str("\n See config.template.yaml for valid field names.");
1132 }
1133
1134 Error::generic(full_msg)
1135 })?
1136 } else {
1137 serde_json::from_str(&content).map_err(|e| {
1138 let error_msg = e.to_string();
1140 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
1141
1142 if error_msg.contains("missing field") {
1144 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1145 full_msg.push_str("\n Omit fields you don't need - MockForge will use sensible defaults.");
1146 full_msg.push_str("\n See config.template.yaml for all available options.");
1147 } else if error_msg.contains("unknown field") {
1148 full_msg.push_str("\n\n💡 Check for typos in field names.");
1149 full_msg.push_str("\n See config.template.yaml for valid field names.");
1150 }
1151
1152 Error::generic(full_msg)
1153 })?
1154 };
1155
1156 Ok(config)
1157}
1158
1159pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1161 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1162 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1163 {
1164 serde_yaml::to_string(config)
1165 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1166 } else {
1167 serde_json::to_string_pretty(config)
1168 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1169 };
1170
1171 fs::write(path, content)
1172 .await
1173 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1174
1175 Ok(())
1176}
1177
1178pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1180 match load_config(&path).await {
1181 Ok(config) => {
1182 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1183 config
1184 }
1185 Err(e) => {
1186 tracing::warn!(
1187 "Failed to load config from {:?}: {}. Using defaults.",
1188 path.as_ref(),
1189 e
1190 );
1191 ServerConfig::default()
1192 }
1193 }
1194}
1195
1196pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1198 let config = ServerConfig::default();
1199 save_config(path, &config).await?;
1200 Ok(())
1201}
1202
1203pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1205 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1207 if let Ok(port_num) = port.parse() {
1208 config.http.port = port_num;
1209 }
1210 }
1211
1212 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1213 config.http.host = host;
1214 }
1215
1216 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1218 if let Ok(port_num) = port.parse() {
1219 config.websocket.port = port_num;
1220 }
1221 }
1222
1223 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1225 if let Ok(port_num) = port.parse() {
1226 config.grpc.port = port_num;
1227 }
1228 }
1229
1230 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1232 if let Ok(port_num) = port.parse() {
1233 config.smtp.port = port_num;
1234 }
1235 }
1236
1237 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1238 config.smtp.host = host;
1239 }
1240
1241 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1242 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1243 }
1244
1245 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1246 config.smtp.hostname = hostname;
1247 }
1248
1249 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1251 if let Ok(port_num) = port.parse() {
1252 config.admin.port = port_num;
1253 }
1254 }
1255
1256 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1257 config.admin.enabled = true;
1258 }
1259
1260 if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
1262 config.admin.host = host;
1263 }
1264
1265 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1266 if !mount_path.trim().is_empty() {
1267 config.admin.mount_path = Some(mount_path);
1268 }
1269 }
1270
1271 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1272 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1273 config.admin.api_enabled = on;
1274 }
1275
1276 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1277 config.admin.prometheus_url = prometheus_url;
1278 }
1279
1280 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1282 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1283 config.core.latency_enabled = enabled;
1284 }
1285
1286 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1287 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1288 config.core.failures_enabled = enabled;
1289 }
1290
1291 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1292 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1293 config.core.overrides_enabled = enabled;
1294 }
1295
1296 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1297 let enabled =
1298 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1299 config.core.traffic_shaping_enabled = enabled;
1300 }
1301
1302 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1304 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1305 config.core.traffic_shaping.bandwidth.enabled = enabled;
1306 }
1307
1308 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1309 if let Ok(bytes) = max_bytes_per_sec.parse() {
1310 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1311 config.core.traffic_shaping.bandwidth.enabled = true;
1312 }
1313 }
1314
1315 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1316 if let Ok(bytes) = burst_capacity.parse() {
1317 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1318 }
1319 }
1320
1321 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1322 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1323 config.core.traffic_shaping.burst_loss.enabled = enabled;
1324 }
1325
1326 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1327 if let Ok(prob) = burst_probability.parse::<f64>() {
1328 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1329 config.core.traffic_shaping.burst_loss.enabled = true;
1330 }
1331 }
1332
1333 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1334 if let Ok(ms) = burst_duration.parse() {
1335 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1336 }
1337 }
1338
1339 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1340 if let Ok(rate) = loss_rate.parse::<f64>() {
1341 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1342 }
1343 }
1344
1345 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1346 if let Ok(ms) = recovery_time.parse() {
1347 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1348 }
1349 }
1350
1351 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1353 config.logging.level = level;
1354 }
1355
1356 config
1357}
1358
1359pub fn validate_config(config: &ServerConfig) -> Result<()> {
1361 if config.http.port == 0 {
1363 return Err(Error::generic("HTTP port cannot be 0"));
1364 }
1365 if config.websocket.port == 0 {
1366 return Err(Error::generic("WebSocket port cannot be 0"));
1367 }
1368 if config.grpc.port == 0 {
1369 return Err(Error::generic("gRPC port cannot be 0"));
1370 }
1371 if config.admin.port == 0 {
1372 return Err(Error::generic("Admin port cannot be 0"));
1373 }
1374
1375 let ports = [
1377 ("HTTP", config.http.port),
1378 ("WebSocket", config.websocket.port),
1379 ("gRPC", config.grpc.port),
1380 ("Admin", config.admin.port),
1381 ];
1382
1383 for i in 0..ports.len() {
1384 for j in (i + 1)..ports.len() {
1385 if ports[i].1 == ports[j].1 {
1386 return Err(Error::generic(format!(
1387 "Port conflict: {} and {} both use port {}",
1388 ports[i].0, ports[j].0, ports[i].1
1389 )));
1390 }
1391 }
1392 }
1393
1394 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1396 if !valid_levels.contains(&config.logging.level.as_str()) {
1397 return Err(Error::generic(format!(
1398 "Invalid log level: {}. Valid levels: {}",
1399 config.logging.level,
1400 valid_levels.join(", ")
1401 )));
1402 }
1403
1404 Ok(())
1405}
1406
1407pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1409 macro_rules! merge_field {
1411 ($field:ident) => {
1412 if let Some(override_val) = profile.$field {
1413 base.$field = override_val;
1414 }
1415 };
1416 }
1417
1418 merge_field!(http);
1419 merge_field!(websocket);
1420 merge_field!(graphql);
1421 merge_field!(grpc);
1422 merge_field!(mqtt);
1423 merge_field!(smtp);
1424 merge_field!(ftp);
1425 merge_field!(kafka);
1426 merge_field!(amqp);
1427 merge_field!(admin);
1428 merge_field!(chaining);
1429 merge_field!(core);
1430 merge_field!(logging);
1431 merge_field!(data);
1432 merge_field!(observability);
1433 merge_field!(multi_tenant);
1434 merge_field!(routes);
1435 merge_field!(protocols);
1436
1437 base
1438}
1439
1440pub async fn load_config_with_profile<P: AsRef<Path>>(
1442 path: P,
1443 profile_name: Option<&str>,
1444) -> Result<ServerConfig> {
1445 let mut config = load_config_auto(&path).await?;
1447
1448 if let Some(profile) = profile_name {
1450 if let Some(profile_config) = config.profiles.remove(profile) {
1451 tracing::info!("Applying profile: {}", profile);
1452 config = apply_profile(config, profile_config);
1453 } else {
1454 return Err(Error::generic(format!(
1455 "Profile '{}' not found in configuration. Available profiles: {}",
1456 profile,
1457 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
1458 )));
1459 }
1460 }
1461
1462 config.profiles.clear();
1464
1465 Ok(config)
1466}
1467
1468pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1470 use rquickjs::{Context, Runtime};
1471
1472 let content = fs::read_to_string(&path)
1473 .await
1474 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
1475
1476 let runtime = Runtime::new()
1478 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
1479 let context = Context::full(&runtime)
1480 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
1481
1482 context.with(|ctx| {
1483 let js_content = if path
1486 .as_ref()
1487 .extension()
1488 .and_then(|s| s.to_str())
1489 .map(|ext| ext == "ts")
1490 .unwrap_or(false)
1491 {
1492 strip_typescript_types(&content)?
1493 } else {
1494 content
1495 };
1496
1497 let result: rquickjs::Value = ctx
1499 .eval(js_content.as_bytes())
1500 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
1501
1502 let json_str: String = ctx
1504 .json_stringify(result)
1505 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
1506 .ok_or_else(|| Error::generic("JS config returned undefined"))?
1507 .get()
1508 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
1509
1510 serde_json::from_str(&json_str).map_err(|e| {
1512 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
1513 })
1514 })
1515}
1516
1517fn strip_typescript_types(content: &str) -> Result<String> {
1524 use regex::Regex;
1525
1526 let mut result = content.to_string();
1527
1528 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
1534 .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
1535 result = interface_re.replace_all(&result, "").to_string();
1536
1537 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
1539 .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
1540 result = type_alias_re.replace_all(&result, "").to_string();
1541
1542 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
1544 .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
1545 result = type_annotation_re.replace_all(&result, "").to_string();
1546
1547 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
1549 .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
1550 result = type_import_re.replace_all(&result, "").to_string();
1551
1552 let as_type_re = Regex::new(r"\s+as\s+\w+")
1554 .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
1555 result = as_type_re.replace_all(&result, "").to_string();
1556
1557 Ok(result)
1558}
1559
1560pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1562 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
1563
1564 match ext {
1565 "ts" | "js" => load_config_from_js(&path).await,
1566 "yaml" | "yml" | "json" => load_config(&path).await,
1567 _ => Err(Error::generic(format!(
1568 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
1569 ext
1570 ))),
1571 }
1572}
1573
1574pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
1576 let current_dir = std::env::current_dir()
1577 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
1578
1579 let config_names = vec![
1580 "mockforge.config.ts",
1581 "mockforge.config.js",
1582 "mockforge.yaml",
1583 "mockforge.yml",
1584 ".mockforge.yaml",
1585 ".mockforge.yml",
1586 ];
1587
1588 for name in &config_names {
1590 let path = current_dir.join(name);
1591 if tokio::fs::metadata(&path).await.is_ok() {
1592 return Ok(path);
1593 }
1594 }
1595
1596 let mut dir = current_dir.clone();
1598 for _ in 0..5 {
1599 if let Some(parent) = dir.parent() {
1600 for name in &config_names {
1601 let path = parent.join(name);
1602 if tokio::fs::metadata(&path).await.is_ok() {
1603 return Ok(path);
1604 }
1605 }
1606 dir = parent.to_path_buf();
1607 } else {
1608 break;
1609 }
1610 }
1611
1612 Err(Error::generic(
1613 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
1614 ))
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620
1621 #[test]
1622 fn test_default_config() {
1623 let config = ServerConfig::default();
1624 assert_eq!(config.http.port, 3000);
1625 assert_eq!(config.websocket.port, 3001);
1626 assert_eq!(config.grpc.port, 50051);
1627 assert_eq!(config.admin.port, 9080);
1628 }
1629
1630 #[test]
1631 fn test_config_validation() {
1632 let mut config = ServerConfig::default();
1633 assert!(validate_config(&config).is_ok());
1634
1635 config.websocket.port = config.http.port;
1637 assert!(validate_config(&config).is_err());
1638
1639 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
1642 assert!(validate_config(&config).is_err());
1643 }
1644
1645 #[test]
1646 fn test_apply_profile() {
1647 let mut base = ServerConfig::default();
1648 assert_eq!(base.http.port, 3000);
1649
1650 let mut profile = ProfileConfig::default();
1651 profile.http = Some(HttpConfig {
1652 port: 8080,
1653 ..Default::default()
1654 });
1655 profile.logging = Some(LoggingConfig {
1656 level: "debug".to_string(),
1657 ..Default::default()
1658 });
1659
1660 let merged = apply_profile(base, profile);
1661 assert_eq!(merged.http.port, 8080);
1662 assert_eq!(merged.logging.level, "debug");
1663 assert_eq!(merged.websocket.port, 3001); }
1665
1666 #[test]
1667 fn test_strip_typescript_types() {
1668 let ts_code = r#"
1669interface Config {
1670 port: number;
1671 host: string;
1672}
1673
1674const config: Config = {
1675 port: 3000,
1676 host: "localhost"
1677} as Config;
1678"#;
1679
1680 let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
1681 assert!(!stripped.contains("interface"));
1682 assert!(!stripped.contains(": Config"));
1683 assert!(!stripped.contains("as Config"));
1684 }
1685}