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 pub tcp: ProtocolConfig,
164}
165
166impl Default for ProtocolsConfig {
167 fn default() -> Self {
168 Self {
169 http: ProtocolConfig { enabled: true },
170 graphql: ProtocolConfig { enabled: true },
171 grpc: ProtocolConfig { enabled: true },
172 websocket: ProtocolConfig { enabled: true },
173 smtp: ProtocolConfig { enabled: false },
174 mqtt: ProtocolConfig { enabled: true },
175 ftp: ProtocolConfig { enabled: false },
176 kafka: ProtocolConfig { enabled: false },
177 rabbitmq: ProtocolConfig { enabled: false },
178 amqp: ProtocolConfig { enabled: false },
179 tcp: ProtocolConfig { enabled: false },
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, Default)]
186#[serde(default)]
187pub struct ServerConfig {
188 pub http: HttpConfig,
190 pub websocket: WebSocketConfig,
192 pub graphql: GraphQLConfig,
194 pub grpc: GrpcConfig,
196 pub mqtt: MqttConfig,
198 pub smtp: SmtpConfig,
200 pub ftp: FtpConfig,
202 pub kafka: KafkaConfig,
204 pub amqp: AmqpConfig,
206 pub tcp: TcpConfig,
208 pub admin: AdminConfig,
210 pub chaining: ChainingConfig,
212 pub core: CoreConfig,
214 pub logging: LoggingConfig,
216 pub data: DataConfig,
218 pub observability: ObservabilityConfig,
220 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
222 #[serde(default)]
224 pub routes: Vec<RouteConfig>,
225 #[serde(default)]
227 pub protocols: ProtocolsConfig,
228 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
230 pub profiles: HashMap<String, ProfileConfig>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, Default)]
235#[serde(default)]
236pub struct ProfileConfig {
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub http: Option<HttpConfig>,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub websocket: Option<WebSocketConfig>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub graphql: Option<GraphQLConfig>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub grpc: Option<GrpcConfig>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub mqtt: Option<MqttConfig>,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub smtp: Option<SmtpConfig>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub ftp: Option<FtpConfig>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub kafka: Option<KafkaConfig>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub amqp: Option<AmqpConfig>,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub tcp: Option<TcpConfig>,
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub admin: Option<AdminConfig>,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub chaining: Option<ChainingConfig>,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub core: Option<CoreConfig>,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub logging: Option<LoggingConfig>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub data: Option<DataConfig>,
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub observability: Option<ObservabilityConfig>,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub routes: Option<Vec<RouteConfig>>,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub protocols: Option<ProtocolsConfig>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct HttpValidationConfig {
301 pub mode: String,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct HttpCorsConfig {
308 pub enabled: bool,
310 #[serde(default)]
312 pub allowed_origins: Vec<String>,
313 #[serde(default)]
315 pub allowed_methods: Vec<String>,
316 #[serde(default)]
318 pub allowed_headers: Vec<String>,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(default)]
324pub struct HttpConfig {
325 pub enabled: bool,
327 pub port: u16,
329 pub host: String,
331 pub openapi_spec: Option<String>,
333 pub cors: Option<HttpCorsConfig>,
335 pub request_timeout_secs: u64,
337 pub validation: Option<HttpValidationConfig>,
339 pub aggregate_validation_errors: bool,
341 pub validate_responses: bool,
343 pub response_template_expand: bool,
345 pub validation_status: Option<u16>,
347 pub validation_overrides: std::collections::HashMap<String, String>,
349 pub skip_admin_validation: bool,
351 pub auth: Option<AuthConfig>,
353}
354
355impl Default for HttpConfig {
356 fn default() -> Self {
357 Self {
358 enabled: true,
359 port: 3000,
360 host: "0.0.0.0".to_string(),
361 openapi_spec: None,
362 cors: Some(HttpCorsConfig {
363 enabled: true,
364 allowed_origins: vec!["*".to_string()],
365 allowed_methods: vec![
366 "GET".to_string(),
367 "POST".to_string(),
368 "PUT".to_string(),
369 "DELETE".to_string(),
370 "PATCH".to_string(),
371 "OPTIONS".to_string(),
372 ],
373 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
374 }),
375 request_timeout_secs: 30,
376 validation: Some(HttpValidationConfig {
377 mode: "enforce".to_string(),
378 }),
379 aggregate_validation_errors: true,
380 validate_responses: false,
381 response_template_expand: false,
382 validation_status: None,
383 validation_overrides: std::collections::HashMap::new(),
384 skip_admin_validation: true,
385 auth: None,
386 }
387 }
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(default)]
393pub struct WebSocketConfig {
394 pub enabled: bool,
396 pub port: u16,
398 pub host: String,
400 pub replay_file: Option<String>,
402 pub connection_timeout_secs: u64,
404}
405
406impl Default for WebSocketConfig {
407 fn default() -> Self {
408 Self {
409 enabled: true,
410 port: 3001,
411 host: "0.0.0.0".to_string(),
412 replay_file: None,
413 connection_timeout_secs: 300,
414 }
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420#[serde(default)]
421pub struct GrpcConfig {
422 pub enabled: bool,
424 pub port: u16,
426 pub host: String,
428 pub proto_dir: Option<String>,
430 pub tls: Option<TlsConfig>,
432}
433
434impl Default for GrpcConfig {
435 fn default() -> Self {
436 Self {
437 enabled: true,
438 port: 50051,
439 host: "0.0.0.0".to_string(),
440 proto_dir: None,
441 tls: None,
442 }
443 }
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(default)]
449pub struct GraphQLConfig {
450 pub enabled: bool,
452 pub port: u16,
454 pub host: String,
456 pub schema_path: Option<String>,
458 pub handlers_dir: Option<String>,
460 pub playground_enabled: bool,
462 pub upstream_url: Option<String>,
464 pub introspection_enabled: bool,
466}
467
468impl Default for GraphQLConfig {
469 fn default() -> Self {
470 Self {
471 enabled: true,
472 port: 4000,
473 host: "0.0.0.0".to_string(),
474 schema_path: None,
475 handlers_dir: None,
476 playground_enabled: true,
477 upstream_url: None,
478 introspection_enabled: true,
479 }
480 }
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct TlsConfig {
486 pub cert_path: String,
488 pub key_path: String,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize)]
494#[serde(default)]
495pub struct MqttConfig {
496 pub enabled: bool,
498 pub port: u16,
500 pub host: String,
502 pub max_connections: usize,
504 pub max_packet_size: usize,
506 pub keep_alive_secs: u16,
508 pub fixtures_dir: Option<std::path::PathBuf>,
510 pub enable_retained_messages: bool,
512 pub max_retained_messages: usize,
514}
515
516impl Default for MqttConfig {
517 fn default() -> Self {
518 Self {
519 enabled: false,
520 port: 1883,
521 host: "0.0.0.0".to_string(),
522 max_connections: 1000,
523 max_packet_size: 268435456, keep_alive_secs: 60,
525 fixtures_dir: None,
526 enable_retained_messages: true,
527 max_retained_messages: 10000,
528 }
529 }
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize)]
534#[serde(default)]
535pub struct SmtpConfig {
536 pub enabled: bool,
538 pub port: u16,
540 pub host: String,
542 pub hostname: String,
544 pub fixtures_dir: Option<std::path::PathBuf>,
546 pub timeout_secs: u64,
548 pub max_connections: usize,
550 pub enable_mailbox: bool,
552 pub max_mailbox_messages: usize,
554 pub enable_starttls: bool,
556 pub tls_cert_path: Option<std::path::PathBuf>,
558 pub tls_key_path: Option<std::path::PathBuf>,
560}
561
562impl Default for SmtpConfig {
563 fn default() -> Self {
564 Self {
565 enabled: false,
566 port: 1025,
567 host: "0.0.0.0".to_string(),
568 hostname: "mockforge-smtp".to_string(),
569 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
570 timeout_secs: 300,
571 max_connections: 10,
572 enable_mailbox: true,
573 max_mailbox_messages: 1000,
574 enable_starttls: false,
575 tls_cert_path: None,
576 tls_key_path: None,
577 }
578 }
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
583#[serde(default)]
584pub struct FtpConfig {
585 pub enabled: bool,
587 pub port: u16,
589 pub host: String,
591 pub passive_ports: (u16, u16),
593 pub max_connections: usize,
595 pub timeout_secs: u64,
597 pub allow_anonymous: bool,
599 pub fixtures_dir: Option<std::path::PathBuf>,
601 pub virtual_root: std::path::PathBuf,
603}
604
605impl Default for FtpConfig {
606 fn default() -> Self {
607 Self {
608 enabled: false,
609 port: 2121,
610 host: "0.0.0.0".to_string(),
611 passive_ports: (50000, 51000),
612 max_connections: 100,
613 timeout_secs: 300,
614 allow_anonymous: true,
615 fixtures_dir: None,
616 virtual_root: std::path::PathBuf::from("/mockforge"),
617 }
618 }
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
623#[serde(default)]
624pub struct KafkaConfig {
625 pub enabled: bool,
627 pub port: u16,
629 pub host: String,
631 pub broker_id: i32,
633 pub max_connections: usize,
635 pub log_retention_ms: i64,
637 pub log_segment_bytes: i64,
639 pub fixtures_dir: Option<std::path::PathBuf>,
641 pub auto_create_topics: bool,
643 pub default_partitions: i32,
645 pub default_replication_factor: i16,
647}
648
649impl Default for KafkaConfig {
650 fn default() -> Self {
651 Self {
652 enabled: false,
653 port: 9092, host: "0.0.0.0".to_string(),
655 broker_id: 1,
656 max_connections: 1000,
657 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
660 auto_create_topics: true,
661 default_partitions: 3,
662 default_replication_factor: 1,
663 }
664 }
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
669#[serde(default)]
670pub struct AmqpConfig {
671 pub enabled: bool,
673 pub port: u16,
675 pub host: String,
677 pub max_connections: usize,
679 pub max_channels_per_connection: u16,
681 pub frame_max: u32,
683 pub heartbeat_interval: u16,
685 pub fixtures_dir: Option<std::path::PathBuf>,
687 pub virtual_hosts: Vec<String>,
689}
690
691impl Default for AmqpConfig {
692 fn default() -> Self {
693 Self {
694 enabled: false,
695 port: 5672, host: "0.0.0.0".to_string(),
697 max_connections: 1000,
698 max_channels_per_connection: 100,
699 frame_max: 131072, heartbeat_interval: 60,
701 fixtures_dir: None,
702 virtual_hosts: vec!["/".to_string()],
703 }
704 }
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize)]
709#[serde(default)]
710pub struct TcpConfig {
711 pub enabled: bool,
713 pub port: u16,
715 pub host: String,
717 pub max_connections: usize,
719 pub timeout_secs: u64,
721 pub fixtures_dir: Option<std::path::PathBuf>,
723 pub echo_mode: bool,
725 pub enable_tls: bool,
727 pub tls_cert_path: Option<std::path::PathBuf>,
729 pub tls_key_path: Option<std::path::PathBuf>,
731}
732
733impl Default for TcpConfig {
734 fn default() -> Self {
735 Self {
736 enabled: false,
737 port: 9999,
738 host: "0.0.0.0".to_string(),
739 max_connections: 1000,
740 timeout_secs: 300,
741 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/tcp")),
742 echo_mode: true,
743 enable_tls: false,
744 tls_cert_path: None,
745 tls_key_path: None,
746 }
747 }
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
752#[serde(default)]
753pub struct AdminConfig {
754 pub enabled: bool,
756 pub port: u16,
758 pub host: String,
760 pub auth_required: bool,
762 pub username: Option<String>,
764 pub password: Option<String>,
766 pub mount_path: Option<String>,
768 pub api_enabled: bool,
770 pub prometheus_url: String,
772}
773
774impl Default for AdminConfig {
775 fn default() -> Self {
776 let default_host = if std::env::var("DOCKER_CONTAINER").is_ok()
779 || std::env::var("container").is_ok()
780 || std::path::Path::new("/.dockerenv").exists()
781 {
782 "0.0.0.0".to_string()
783 } else {
784 "127.0.0.1".to_string()
785 };
786
787 Self {
788 enabled: false,
789 port: 9080,
790 host: default_host,
791 auth_required: false,
792 username: None,
793 password: None,
794 mount_path: None,
795 api_enabled: true,
796 prometheus_url: "http://localhost:9090".to_string(),
797 }
798 }
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize)]
803#[serde(default)]
804pub struct LoggingConfig {
805 pub level: String,
807 pub json_format: bool,
809 pub file_path: Option<String>,
811 pub max_file_size_mb: u64,
813 pub max_files: u32,
815}
816
817impl Default for LoggingConfig {
818 fn default() -> Self {
819 Self {
820 level: "info".to_string(),
821 json_format: false,
822 file_path: None,
823 max_file_size_mb: 10,
824 max_files: 5,
825 }
826 }
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize)]
831#[serde(default, rename_all = "camelCase")]
832pub struct ChainingConfig {
833 pub enabled: bool,
835 pub max_chain_length: usize,
837 pub global_timeout_secs: u64,
839 pub enable_parallel_execution: bool,
841}
842
843impl Default for ChainingConfig {
844 fn default() -> Self {
845 Self {
846 enabled: false,
847 max_chain_length: 20,
848 global_timeout_secs: 300,
849 enable_parallel_execution: false,
850 }
851 }
852}
853
854#[derive(Debug, Clone, Serialize, Deserialize)]
856#[serde(default)]
857pub struct DataConfig {
858 pub default_rows: usize,
860 pub default_format: String,
862 pub locale: String,
864 pub templates: HashMap<String, String>,
866 pub rag: RagConfig,
868}
869
870impl Default for DataConfig {
871 fn default() -> Self {
872 Self {
873 default_rows: 100,
874 default_format: "json".to_string(),
875 locale: "en".to_string(),
876 templates: HashMap::new(),
877 rag: RagConfig::default(),
878 }
879 }
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize)]
884#[serde(default)]
885pub struct RagConfig {
886 pub enabled: bool,
888 #[serde(default)]
890 pub provider: String,
891 pub api_endpoint: Option<String>,
893 pub api_key: Option<String>,
895 pub model: Option<String>,
897 #[serde(default = "default_max_tokens")]
899 pub max_tokens: usize,
900 #[serde(default = "default_temperature")]
902 pub temperature: f64,
903 pub context_window: usize,
905 #[serde(default = "default_true")]
907 pub caching: bool,
908 #[serde(default = "default_cache_ttl")]
910 pub cache_ttl_secs: u64,
911 #[serde(default = "default_timeout")]
913 pub timeout_secs: u64,
914 #[serde(default = "default_max_retries")]
916 pub max_retries: usize,
917}
918
919fn default_max_tokens() -> usize {
920 1024
921}
922
923fn default_temperature() -> f64 {
924 0.7
925}
926
927fn default_true() -> bool {
928 true
929}
930
931fn default_cache_ttl() -> u64 {
932 3600
933}
934
935fn default_timeout() -> u64 {
936 30
937}
938
939fn default_max_retries() -> usize {
940 3
941}
942
943impl Default for RagConfig {
944 fn default() -> Self {
945 Self {
946 enabled: false,
947 provider: "openai".to_string(),
948 api_endpoint: None,
949 api_key: None,
950 model: Some("gpt-3.5-turbo".to_string()),
951 max_tokens: default_max_tokens(),
952 temperature: default_temperature(),
953 context_window: 4000,
954 caching: default_true(),
955 cache_ttl_secs: default_cache_ttl(),
956 timeout_secs: default_timeout(),
957 max_retries: default_max_retries(),
958 }
959 }
960}
961
962#[derive(Debug, Clone, Serialize, Deserialize, Default)]
964#[serde(default)]
965pub struct ObservabilityConfig {
966 pub prometheus: PrometheusConfig,
968 pub opentelemetry: Option<OpenTelemetryConfig>,
970 pub recorder: Option<RecorderConfig>,
972 pub chaos: Option<ChaosEngConfig>,
974}
975
976#[derive(Debug, Clone, Serialize, Deserialize)]
978#[serde(default)]
979pub struct PrometheusConfig {
980 pub enabled: bool,
982 pub port: u16,
984 pub host: String,
986 pub path: String,
988}
989
990impl Default for PrometheusConfig {
991 fn default() -> Self {
992 Self {
993 enabled: true,
994 port: 9090,
995 host: "0.0.0.0".to_string(),
996 path: "/metrics".to_string(),
997 }
998 }
999}
1000
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1003#[serde(default)]
1004pub struct OpenTelemetryConfig {
1005 pub enabled: bool,
1007 pub service_name: String,
1009 pub environment: String,
1011 pub jaeger_endpoint: String,
1013 pub otlp_endpoint: Option<String>,
1015 pub protocol: String,
1017 pub sampling_rate: f64,
1019}
1020
1021impl Default for OpenTelemetryConfig {
1022 fn default() -> Self {
1023 Self {
1024 enabled: false,
1025 service_name: "mockforge".to_string(),
1026 environment: "development".to_string(),
1027 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
1028 otlp_endpoint: Some("http://localhost:4317".to_string()),
1029 protocol: "grpc".to_string(),
1030 sampling_rate: 1.0,
1031 }
1032 }
1033}
1034
1035#[derive(Debug, Clone, Serialize, Deserialize)]
1037#[serde(default)]
1038pub struct RecorderConfig {
1039 pub enabled: bool,
1041 pub database_path: String,
1043 pub api_enabled: bool,
1045 pub api_port: Option<u16>,
1047 pub max_requests: i64,
1049 pub retention_days: i64,
1051 pub record_http: bool,
1053 pub record_grpc: bool,
1055 pub record_websocket: bool,
1057 pub record_graphql: bool,
1059}
1060
1061impl Default for RecorderConfig {
1062 fn default() -> Self {
1063 Self {
1064 enabled: false,
1065 database_path: "./mockforge-recordings.db".to_string(),
1066 api_enabled: true,
1067 api_port: None,
1068 max_requests: 10000,
1069 retention_days: 7,
1070 record_http: true,
1071 record_grpc: true,
1072 record_websocket: true,
1073 record_graphql: true,
1074 }
1075 }
1076}
1077
1078#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1080#[serde(default)]
1081pub struct ChaosEngConfig {
1082 pub enabled: bool,
1084 pub latency: Option<LatencyInjectionConfig>,
1086 pub fault_injection: Option<FaultConfig>,
1088 pub rate_limit: Option<RateLimitingConfig>,
1090 pub traffic_shaping: Option<NetworkShapingConfig>,
1092 pub scenario: Option<String>,
1094}
1095
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1098pub struct LatencyInjectionConfig {
1099 pub enabled: bool,
1101 pub fixed_delay_ms: Option<u64>,
1103 pub random_delay_range_ms: Option<(u64, u64)>,
1105 pub jitter_percent: f64,
1107 pub probability: f64,
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize)]
1113pub struct FaultConfig {
1114 pub enabled: bool,
1116 pub http_errors: Vec<u16>,
1118 pub http_error_probability: f64,
1120 pub connection_errors: bool,
1122 pub connection_error_probability: f64,
1124 pub timeout_errors: bool,
1126 pub timeout_ms: u64,
1128 pub timeout_probability: f64,
1130}
1131
1132#[derive(Debug, Clone, Serialize, Deserialize)]
1134pub struct RateLimitingConfig {
1135 pub enabled: bool,
1137 pub requests_per_second: u32,
1139 pub burst_size: u32,
1141 pub per_ip: bool,
1143 pub per_endpoint: bool,
1145}
1146
1147#[derive(Debug, Clone, Serialize, Deserialize)]
1149pub struct NetworkShapingConfig {
1150 pub enabled: bool,
1152 pub bandwidth_limit_bps: u64,
1154 pub packet_loss_percent: f64,
1156 pub max_connections: u32,
1158}
1159
1160pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1162 let content = fs::read_to_string(&path)
1163 .await
1164 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1165
1166 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1168 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1169 {
1170 serde_yaml::from_str(&content).map_err(|e| {
1171 let error_msg = e.to_string();
1173 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
1174
1175 if error_msg.contains("missing field") {
1177 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1178 full_msg.push_str(
1179 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1180 );
1181 full_msg.push_str("\n See config.template.yaml for all available options.");
1182 } else if error_msg.contains("unknown field") {
1183 full_msg.push_str("\n\n💡 Check for typos in field names.");
1184 full_msg.push_str("\n See config.template.yaml for valid field names.");
1185 }
1186
1187 Error::generic(full_msg)
1188 })?
1189 } else {
1190 serde_json::from_str(&content).map_err(|e| {
1191 let error_msg = e.to_string();
1193 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
1194
1195 if error_msg.contains("missing field") {
1197 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1198 full_msg.push_str(
1199 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1200 );
1201 full_msg.push_str("\n See config.template.yaml for all available options.");
1202 } else if error_msg.contains("unknown field") {
1203 full_msg.push_str("\n\n💡 Check for typos in field names.");
1204 full_msg.push_str("\n See config.template.yaml for valid field names.");
1205 }
1206
1207 Error::generic(full_msg)
1208 })?
1209 };
1210
1211 Ok(config)
1212}
1213
1214pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1216 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1217 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1218 {
1219 serde_yaml::to_string(config)
1220 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1221 } else {
1222 serde_json::to_string_pretty(config)
1223 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1224 };
1225
1226 fs::write(path, content)
1227 .await
1228 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1229
1230 Ok(())
1231}
1232
1233pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1235 match load_config(&path).await {
1236 Ok(config) => {
1237 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1238 config
1239 }
1240 Err(e) => {
1241 tracing::warn!(
1242 "Failed to load config from {:?}: {}. Using defaults.",
1243 path.as_ref(),
1244 e
1245 );
1246 ServerConfig::default()
1247 }
1248 }
1249}
1250
1251pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1253 let config = ServerConfig::default();
1254 save_config(path, &config).await?;
1255 Ok(())
1256}
1257
1258pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1260 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1262 if let Ok(port_num) = port.parse() {
1263 config.http.port = port_num;
1264 }
1265 }
1266
1267 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1268 config.http.host = host;
1269 }
1270
1271 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1273 if let Ok(port_num) = port.parse() {
1274 config.websocket.port = port_num;
1275 }
1276 }
1277
1278 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1280 if let Ok(port_num) = port.parse() {
1281 config.grpc.port = port_num;
1282 }
1283 }
1284
1285 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1287 if let Ok(port_num) = port.parse() {
1288 config.smtp.port = port_num;
1289 }
1290 }
1291
1292 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1293 config.smtp.host = host;
1294 }
1295
1296 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1297 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1298 }
1299
1300 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1301 config.smtp.hostname = hostname;
1302 }
1303
1304 if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
1306 if let Ok(port_num) = port.parse() {
1307 config.tcp.port = port_num;
1308 }
1309 }
1310
1311 if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
1312 config.tcp.host = host;
1313 }
1314
1315 if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
1316 config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1317 }
1318
1319 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1321 if let Ok(port_num) = port.parse() {
1322 config.admin.port = port_num;
1323 }
1324 }
1325
1326 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1327 config.admin.enabled = true;
1328 }
1329
1330 if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
1332 config.admin.host = host;
1333 }
1334
1335 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1336 if !mount_path.trim().is_empty() {
1337 config.admin.mount_path = Some(mount_path);
1338 }
1339 }
1340
1341 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1342 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1343 config.admin.api_enabled = on;
1344 }
1345
1346 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1347 config.admin.prometheus_url = prometheus_url;
1348 }
1349
1350 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1352 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1353 config.core.latency_enabled = enabled;
1354 }
1355
1356 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1357 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1358 config.core.failures_enabled = enabled;
1359 }
1360
1361 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1362 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1363 config.core.overrides_enabled = enabled;
1364 }
1365
1366 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1367 let enabled =
1368 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1369 config.core.traffic_shaping_enabled = enabled;
1370 }
1371
1372 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1374 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1375 config.core.traffic_shaping.bandwidth.enabled = enabled;
1376 }
1377
1378 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1379 if let Ok(bytes) = max_bytes_per_sec.parse() {
1380 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1381 config.core.traffic_shaping.bandwidth.enabled = true;
1382 }
1383 }
1384
1385 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1386 if let Ok(bytes) = burst_capacity.parse() {
1387 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1388 }
1389 }
1390
1391 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1392 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1393 config.core.traffic_shaping.burst_loss.enabled = enabled;
1394 }
1395
1396 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1397 if let Ok(prob) = burst_probability.parse::<f64>() {
1398 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1399 config.core.traffic_shaping.burst_loss.enabled = true;
1400 }
1401 }
1402
1403 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1404 if let Ok(ms) = burst_duration.parse() {
1405 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1406 }
1407 }
1408
1409 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1410 if let Ok(rate) = loss_rate.parse::<f64>() {
1411 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1412 }
1413 }
1414
1415 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1416 if let Ok(ms) = recovery_time.parse() {
1417 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1418 }
1419 }
1420
1421 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1423 config.logging.level = level;
1424 }
1425
1426 config
1427}
1428
1429pub fn validate_config(config: &ServerConfig) -> Result<()> {
1431 if config.http.port == 0 {
1433 return Err(Error::generic("HTTP port cannot be 0"));
1434 }
1435 if config.websocket.port == 0 {
1436 return Err(Error::generic("WebSocket port cannot be 0"));
1437 }
1438 if config.grpc.port == 0 {
1439 return Err(Error::generic("gRPC port cannot be 0"));
1440 }
1441 if config.admin.port == 0 {
1442 return Err(Error::generic("Admin port cannot be 0"));
1443 }
1444
1445 let ports = [
1447 ("HTTP", config.http.port),
1448 ("WebSocket", config.websocket.port),
1449 ("gRPC", config.grpc.port),
1450 ("Admin", config.admin.port),
1451 ];
1452
1453 for i in 0..ports.len() {
1454 for j in (i + 1)..ports.len() {
1455 if ports[i].1 == ports[j].1 {
1456 return Err(Error::generic(format!(
1457 "Port conflict: {} and {} both use port {}",
1458 ports[i].0, ports[j].0, ports[i].1
1459 )));
1460 }
1461 }
1462 }
1463
1464 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1466 if !valid_levels.contains(&config.logging.level.as_str()) {
1467 return Err(Error::generic(format!(
1468 "Invalid log level: {}. Valid levels: {}",
1469 config.logging.level,
1470 valid_levels.join(", ")
1471 )));
1472 }
1473
1474 Ok(())
1475}
1476
1477pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1479 macro_rules! merge_field {
1481 ($field:ident) => {
1482 if let Some(override_val) = profile.$field {
1483 base.$field = override_val;
1484 }
1485 };
1486 }
1487
1488 merge_field!(http);
1489 merge_field!(websocket);
1490 merge_field!(graphql);
1491 merge_field!(grpc);
1492 merge_field!(mqtt);
1493 merge_field!(smtp);
1494 merge_field!(ftp);
1495 merge_field!(kafka);
1496 merge_field!(amqp);
1497 merge_field!(tcp);
1498 merge_field!(admin);
1499 merge_field!(chaining);
1500 merge_field!(core);
1501 merge_field!(logging);
1502 merge_field!(data);
1503 merge_field!(observability);
1504 merge_field!(multi_tenant);
1505 merge_field!(routes);
1506 merge_field!(protocols);
1507
1508 base
1509}
1510
1511pub async fn load_config_with_profile<P: AsRef<Path>>(
1513 path: P,
1514 profile_name: Option<&str>,
1515) -> Result<ServerConfig> {
1516 let mut config = load_config_auto(&path).await?;
1518
1519 if let Some(profile) = profile_name {
1521 if let Some(profile_config) = config.profiles.remove(profile) {
1522 tracing::info!("Applying profile: {}", profile);
1523 config = apply_profile(config, profile_config);
1524 } else {
1525 return Err(Error::generic(format!(
1526 "Profile '{}' not found in configuration. Available profiles: {}",
1527 profile,
1528 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
1529 )));
1530 }
1531 }
1532
1533 config.profiles.clear();
1535
1536 Ok(config)
1537}
1538
1539pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1541 use rquickjs::{Context, Runtime};
1542
1543 let content = fs::read_to_string(&path)
1544 .await
1545 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
1546
1547 let runtime = Runtime::new()
1549 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
1550 let context = Context::full(&runtime)
1551 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
1552
1553 context.with(|ctx| {
1554 let js_content = if path
1557 .as_ref()
1558 .extension()
1559 .and_then(|s| s.to_str())
1560 .map(|ext| ext == "ts")
1561 .unwrap_or(false)
1562 {
1563 strip_typescript_types(&content)?
1564 } else {
1565 content
1566 };
1567
1568 let result: rquickjs::Value = ctx
1570 .eval(js_content.as_bytes())
1571 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
1572
1573 let json_str: String = ctx
1575 .json_stringify(result)
1576 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
1577 .ok_or_else(|| Error::generic("JS config returned undefined"))?
1578 .get()
1579 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
1580
1581 serde_json::from_str(&json_str).map_err(|e| {
1583 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
1584 })
1585 })
1586}
1587
1588fn strip_typescript_types(content: &str) -> Result<String> {
1595 use regex::Regex;
1596
1597 let mut result = content.to_string();
1598
1599 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
1605 .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
1606 result = interface_re.replace_all(&result, "").to_string();
1607
1608 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
1610 .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
1611 result = type_alias_re.replace_all(&result, "").to_string();
1612
1613 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
1615 .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
1616 result = type_annotation_re.replace_all(&result, "").to_string();
1617
1618 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
1620 .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
1621 result = type_import_re.replace_all(&result, "").to_string();
1622
1623 let as_type_re = Regex::new(r"\s+as\s+\w+")
1625 .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
1626 result = as_type_re.replace_all(&result, "").to_string();
1627
1628 Ok(result)
1629}
1630
1631pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1633 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
1634
1635 match ext {
1636 "ts" | "js" => load_config_from_js(&path).await,
1637 "yaml" | "yml" | "json" => load_config(&path).await,
1638 _ => Err(Error::generic(format!(
1639 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
1640 ext
1641 ))),
1642 }
1643}
1644
1645pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
1647 let current_dir = std::env::current_dir()
1648 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
1649
1650 let config_names = vec![
1651 "mockforge.config.ts",
1652 "mockforge.config.js",
1653 "mockforge.yaml",
1654 "mockforge.yml",
1655 ".mockforge.yaml",
1656 ".mockforge.yml",
1657 ];
1658
1659 for name in &config_names {
1661 let path = current_dir.join(name);
1662 if tokio::fs::metadata(&path).await.is_ok() {
1663 return Ok(path);
1664 }
1665 }
1666
1667 let mut dir = current_dir.clone();
1669 for _ in 0..5 {
1670 if let Some(parent) = dir.parent() {
1671 for name in &config_names {
1672 let path = parent.join(name);
1673 if tokio::fs::metadata(&path).await.is_ok() {
1674 return Ok(path);
1675 }
1676 }
1677 dir = parent.to_path_buf();
1678 } else {
1679 break;
1680 }
1681 }
1682
1683 Err(Error::generic(
1684 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
1685 ))
1686}
1687
1688#[cfg(test)]
1689mod tests {
1690 use super::*;
1691
1692 #[test]
1693 fn test_default_config() {
1694 let config = ServerConfig::default();
1695 assert_eq!(config.http.port, 3000);
1696 assert_eq!(config.websocket.port, 3001);
1697 assert_eq!(config.grpc.port, 50051);
1698 assert_eq!(config.admin.port, 9080);
1699 }
1700
1701 #[test]
1702 fn test_config_validation() {
1703 let mut config = ServerConfig::default();
1704 assert!(validate_config(&config).is_ok());
1705
1706 config.websocket.port = config.http.port;
1708 assert!(validate_config(&config).is_err());
1709
1710 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
1713 assert!(validate_config(&config).is_err());
1714 }
1715
1716 #[test]
1717 fn test_apply_profile() {
1718 let mut base = ServerConfig::default();
1719 assert_eq!(base.http.port, 3000);
1720
1721 let mut profile = ProfileConfig::default();
1722 profile.http = Some(HttpConfig {
1723 port: 8080,
1724 ..Default::default()
1725 });
1726 profile.logging = Some(LoggingConfig {
1727 level: "debug".to_string(),
1728 ..Default::default()
1729 });
1730
1731 let merged = apply_profile(base, profile);
1732 assert_eq!(merged.http.port, 8080);
1733 assert_eq!(merged.logging.level, "debug");
1734 assert_eq!(merged.websocket.port, 3001); }
1736
1737 #[test]
1738 fn test_strip_typescript_types() {
1739 let ts_code = r#"
1740interface Config {
1741 port: number;
1742 host: string;
1743}
1744
1745const config: Config = {
1746 port: 3000,
1747 host: "localhost"
1748} as Config;
1749"#;
1750
1751 let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
1752 assert!(!stripped.contains("interface"));
1753 assert!(!stripped.contains(": Config"));
1754 assert!(!stripped.contains("as Config"));
1755 }
1756}