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 #[serde(skip_serializing_if = "Option::is_none")]
355 pub tls: Option<HttpTlsConfig>,
356}
357
358impl Default for HttpConfig {
359 fn default() -> Self {
360 Self {
361 enabled: true,
362 port: 3000,
363 host: "0.0.0.0".to_string(),
364 openapi_spec: None,
365 cors: Some(HttpCorsConfig {
366 enabled: true,
367 allowed_origins: vec!["*".to_string()],
368 allowed_methods: vec![
369 "GET".to_string(),
370 "POST".to_string(),
371 "PUT".to_string(),
372 "DELETE".to_string(),
373 "PATCH".to_string(),
374 "OPTIONS".to_string(),
375 ],
376 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
377 }),
378 request_timeout_secs: 30,
379 validation: Some(HttpValidationConfig {
380 mode: "enforce".to_string(),
381 }),
382 aggregate_validation_errors: true,
383 validate_responses: false,
384 response_template_expand: false,
385 validation_status: None,
386 validation_overrides: std::collections::HashMap::new(),
387 skip_admin_validation: true,
388 auth: None,
389 tls: None,
390 }
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct HttpTlsConfig {
397 pub enabled: bool,
399 pub cert_file: String,
401 pub key_file: String,
403 #[serde(skip_serializing_if = "Option::is_none")]
405 pub ca_file: Option<String>,
406 #[serde(default = "default_tls_min_version")]
408 pub min_version: String,
409 #[serde(default, skip_serializing_if = "Vec::is_empty")]
411 pub cipher_suites: Vec<String>,
412 #[serde(default)]
414 pub require_client_cert: bool,
415}
416
417fn default_tls_min_version() -> String {
418 "1.2".to_string()
419}
420
421impl Default for HttpTlsConfig {
422 fn default() -> Self {
423 Self {
424 enabled: true,
425 cert_file: String::new(),
426 key_file: String::new(),
427 ca_file: None,
428 min_version: "1.2".to_string(),
429 cipher_suites: Vec::new(),
430 require_client_cert: false,
431 }
432 }
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437#[serde(default)]
438pub struct WebSocketConfig {
439 pub enabled: bool,
441 pub port: u16,
443 pub host: String,
445 pub replay_file: Option<String>,
447 pub connection_timeout_secs: u64,
449}
450
451impl Default for WebSocketConfig {
452 fn default() -> Self {
453 Self {
454 enabled: true,
455 port: 3001,
456 host: "0.0.0.0".to_string(),
457 replay_file: None,
458 connection_timeout_secs: 300,
459 }
460 }
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
465#[serde(default)]
466pub struct GrpcConfig {
467 pub enabled: bool,
469 pub port: u16,
471 pub host: String,
473 pub proto_dir: Option<String>,
475 pub tls: Option<TlsConfig>,
477}
478
479impl Default for GrpcConfig {
480 fn default() -> Self {
481 Self {
482 enabled: true,
483 port: 50051,
484 host: "0.0.0.0".to_string(),
485 proto_dir: None,
486 tls: None,
487 }
488 }
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
493#[serde(default)]
494pub struct GraphQLConfig {
495 pub enabled: bool,
497 pub port: u16,
499 pub host: String,
501 pub schema_path: Option<String>,
503 pub handlers_dir: Option<String>,
505 pub playground_enabled: bool,
507 pub upstream_url: Option<String>,
509 pub introspection_enabled: bool,
511}
512
513impl Default for GraphQLConfig {
514 fn default() -> Self {
515 Self {
516 enabled: true,
517 port: 4000,
518 host: "0.0.0.0".to_string(),
519 schema_path: None,
520 handlers_dir: None,
521 playground_enabled: true,
522 upstream_url: None,
523 introspection_enabled: true,
524 }
525 }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct TlsConfig {
531 pub cert_path: String,
533 pub key_path: String,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
539#[serde(default)]
540pub struct MqttConfig {
541 pub enabled: bool,
543 pub port: u16,
545 pub host: String,
547 pub max_connections: usize,
549 pub max_packet_size: usize,
551 pub keep_alive_secs: u16,
553 pub fixtures_dir: Option<std::path::PathBuf>,
555 pub enable_retained_messages: bool,
557 pub max_retained_messages: usize,
559}
560
561impl Default for MqttConfig {
562 fn default() -> Self {
563 Self {
564 enabled: false,
565 port: 1883,
566 host: "0.0.0.0".to_string(),
567 max_connections: 1000,
568 max_packet_size: 268435456, keep_alive_secs: 60,
570 fixtures_dir: None,
571 enable_retained_messages: true,
572 max_retained_messages: 10000,
573 }
574 }
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
579#[serde(default)]
580pub struct SmtpConfig {
581 pub enabled: bool,
583 pub port: u16,
585 pub host: String,
587 pub hostname: String,
589 pub fixtures_dir: Option<std::path::PathBuf>,
591 pub timeout_secs: u64,
593 pub max_connections: usize,
595 pub enable_mailbox: bool,
597 pub max_mailbox_messages: usize,
599 pub enable_starttls: bool,
601 pub tls_cert_path: Option<std::path::PathBuf>,
603 pub tls_key_path: Option<std::path::PathBuf>,
605}
606
607impl Default for SmtpConfig {
608 fn default() -> Self {
609 Self {
610 enabled: false,
611 port: 1025,
612 host: "0.0.0.0".to_string(),
613 hostname: "mockforge-smtp".to_string(),
614 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
615 timeout_secs: 300,
616 max_connections: 10,
617 enable_mailbox: true,
618 max_mailbox_messages: 1000,
619 enable_starttls: false,
620 tls_cert_path: None,
621 tls_key_path: None,
622 }
623 }
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize)]
628#[serde(default)]
629pub struct FtpConfig {
630 pub enabled: bool,
632 pub port: u16,
634 pub host: String,
636 pub passive_ports: (u16, u16),
638 pub max_connections: usize,
640 pub timeout_secs: u64,
642 pub allow_anonymous: bool,
644 pub fixtures_dir: Option<std::path::PathBuf>,
646 pub virtual_root: std::path::PathBuf,
648}
649
650impl Default for FtpConfig {
651 fn default() -> Self {
652 Self {
653 enabled: false,
654 port: 2121,
655 host: "0.0.0.0".to_string(),
656 passive_ports: (50000, 51000),
657 max_connections: 100,
658 timeout_secs: 300,
659 allow_anonymous: true,
660 fixtures_dir: None,
661 virtual_root: std::path::PathBuf::from("/mockforge"),
662 }
663 }
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize)]
668#[serde(default)]
669pub struct KafkaConfig {
670 pub enabled: bool,
672 pub port: u16,
674 pub host: String,
676 pub broker_id: i32,
678 pub max_connections: usize,
680 pub log_retention_ms: i64,
682 pub log_segment_bytes: i64,
684 pub fixtures_dir: Option<std::path::PathBuf>,
686 pub auto_create_topics: bool,
688 pub default_partitions: i32,
690 pub default_replication_factor: i16,
692}
693
694impl Default for KafkaConfig {
695 fn default() -> Self {
696 Self {
697 enabled: false,
698 port: 9092, host: "0.0.0.0".to_string(),
700 broker_id: 1,
701 max_connections: 1000,
702 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
705 auto_create_topics: true,
706 default_partitions: 3,
707 default_replication_factor: 1,
708 }
709 }
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize)]
714#[serde(default)]
715pub struct AmqpConfig {
716 pub enabled: bool,
718 pub port: u16,
720 pub host: String,
722 pub max_connections: usize,
724 pub max_channels_per_connection: u16,
726 pub frame_max: u32,
728 pub heartbeat_interval: u16,
730 pub fixtures_dir: Option<std::path::PathBuf>,
732 pub virtual_hosts: Vec<String>,
734}
735
736impl Default for AmqpConfig {
737 fn default() -> Self {
738 Self {
739 enabled: false,
740 port: 5672, host: "0.0.0.0".to_string(),
742 max_connections: 1000,
743 max_channels_per_connection: 100,
744 frame_max: 131072, heartbeat_interval: 60,
746 fixtures_dir: None,
747 virtual_hosts: vec!["/".to_string()],
748 }
749 }
750}
751
752#[derive(Debug, Clone, Serialize, Deserialize)]
754#[serde(default)]
755pub struct TcpConfig {
756 pub enabled: bool,
758 pub port: u16,
760 pub host: String,
762 pub max_connections: usize,
764 pub timeout_secs: u64,
766 pub fixtures_dir: Option<std::path::PathBuf>,
768 pub echo_mode: bool,
770 pub enable_tls: bool,
772 pub tls_cert_path: Option<std::path::PathBuf>,
774 pub tls_key_path: Option<std::path::PathBuf>,
776}
777
778impl Default for TcpConfig {
779 fn default() -> Self {
780 Self {
781 enabled: false,
782 port: 9999,
783 host: "0.0.0.0".to_string(),
784 max_connections: 1000,
785 timeout_secs: 300,
786 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/tcp")),
787 echo_mode: true,
788 enable_tls: false,
789 tls_cert_path: None,
790 tls_key_path: None,
791 }
792 }
793}
794
795#[derive(Debug, Clone, Serialize, Deserialize)]
797#[serde(default)]
798pub struct AdminConfig {
799 pub enabled: bool,
801 pub port: u16,
803 pub host: String,
805 pub auth_required: bool,
807 pub username: Option<String>,
809 pub password: Option<String>,
811 pub mount_path: Option<String>,
813 pub api_enabled: bool,
815 pub prometheus_url: String,
817}
818
819impl Default for AdminConfig {
820 fn default() -> Self {
821 let default_host = if std::env::var("DOCKER_CONTAINER").is_ok()
824 || std::env::var("container").is_ok()
825 || std::path::Path::new("/.dockerenv").exists()
826 {
827 "0.0.0.0".to_string()
828 } else {
829 "127.0.0.1".to_string()
830 };
831
832 Self {
833 enabled: false,
834 port: 9080,
835 host: default_host,
836 auth_required: false,
837 username: None,
838 password: None,
839 mount_path: None,
840 api_enabled: true,
841 prometheus_url: "http://localhost:9090".to_string(),
842 }
843 }
844}
845
846#[derive(Debug, Clone, Serialize, Deserialize)]
848#[serde(default)]
849pub struct LoggingConfig {
850 pub level: String,
852 pub json_format: bool,
854 pub file_path: Option<String>,
856 pub max_file_size_mb: u64,
858 pub max_files: u32,
860}
861
862impl Default for LoggingConfig {
863 fn default() -> Self {
864 Self {
865 level: "info".to_string(),
866 json_format: false,
867 file_path: None,
868 max_file_size_mb: 10,
869 max_files: 5,
870 }
871 }
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize)]
876#[serde(default, rename_all = "camelCase")]
877pub struct ChainingConfig {
878 pub enabled: bool,
880 pub max_chain_length: usize,
882 pub global_timeout_secs: u64,
884 pub enable_parallel_execution: bool,
886}
887
888impl Default for ChainingConfig {
889 fn default() -> Self {
890 Self {
891 enabled: false,
892 max_chain_length: 20,
893 global_timeout_secs: 300,
894 enable_parallel_execution: false,
895 }
896 }
897}
898
899#[derive(Debug, Clone, Serialize, Deserialize)]
901#[serde(default)]
902pub struct DataConfig {
903 pub default_rows: usize,
905 pub default_format: String,
907 pub locale: String,
909 pub templates: HashMap<String, String>,
911 pub rag: RagConfig,
913}
914
915impl Default for DataConfig {
916 fn default() -> Self {
917 Self {
918 default_rows: 100,
919 default_format: "json".to_string(),
920 locale: "en".to_string(),
921 templates: HashMap::new(),
922 rag: RagConfig::default(),
923 }
924 }
925}
926
927#[derive(Debug, Clone, Serialize, Deserialize)]
929#[serde(default)]
930pub struct RagConfig {
931 pub enabled: bool,
933 #[serde(default)]
935 pub provider: String,
936 pub api_endpoint: Option<String>,
938 pub api_key: Option<String>,
940 pub model: Option<String>,
942 #[serde(default = "default_max_tokens")]
944 pub max_tokens: usize,
945 #[serde(default = "default_temperature")]
947 pub temperature: f64,
948 pub context_window: usize,
950 #[serde(default = "default_true")]
952 pub caching: bool,
953 #[serde(default = "default_cache_ttl")]
955 pub cache_ttl_secs: u64,
956 #[serde(default = "default_timeout")]
958 pub timeout_secs: u64,
959 #[serde(default = "default_max_retries")]
961 pub max_retries: usize,
962}
963
964fn default_max_tokens() -> usize {
965 1024
966}
967
968fn default_temperature() -> f64 {
969 0.7
970}
971
972fn default_true() -> bool {
973 true
974}
975
976fn default_cache_ttl() -> u64 {
977 3600
978}
979
980fn default_timeout() -> u64 {
981 30
982}
983
984fn default_max_retries() -> usize {
985 3
986}
987
988impl Default for RagConfig {
989 fn default() -> Self {
990 Self {
991 enabled: false,
992 provider: "openai".to_string(),
993 api_endpoint: None,
994 api_key: None,
995 model: Some("gpt-3.5-turbo".to_string()),
996 max_tokens: default_max_tokens(),
997 temperature: default_temperature(),
998 context_window: 4000,
999 caching: default_true(),
1000 cache_ttl_secs: default_cache_ttl(),
1001 timeout_secs: default_timeout(),
1002 max_retries: default_max_retries(),
1003 }
1004 }
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1009#[serde(default)]
1010pub struct ObservabilityConfig {
1011 pub prometheus: PrometheusConfig,
1013 pub opentelemetry: Option<OpenTelemetryConfig>,
1015 pub recorder: Option<RecorderConfig>,
1017 pub chaos: Option<ChaosEngConfig>,
1019}
1020
1021#[derive(Debug, Clone, Serialize, Deserialize)]
1023#[serde(default)]
1024pub struct PrometheusConfig {
1025 pub enabled: bool,
1027 pub port: u16,
1029 pub host: String,
1031 pub path: String,
1033}
1034
1035impl Default for PrometheusConfig {
1036 fn default() -> Self {
1037 Self {
1038 enabled: true,
1039 port: 9090,
1040 host: "0.0.0.0".to_string(),
1041 path: "/metrics".to_string(),
1042 }
1043 }
1044}
1045
1046#[derive(Debug, Clone, Serialize, Deserialize)]
1048#[serde(default)]
1049pub struct OpenTelemetryConfig {
1050 pub enabled: bool,
1052 pub service_name: String,
1054 pub environment: String,
1056 pub jaeger_endpoint: String,
1058 pub otlp_endpoint: Option<String>,
1060 pub protocol: String,
1062 pub sampling_rate: f64,
1064}
1065
1066impl Default for OpenTelemetryConfig {
1067 fn default() -> Self {
1068 Self {
1069 enabled: false,
1070 service_name: "mockforge".to_string(),
1071 environment: "development".to_string(),
1072 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
1073 otlp_endpoint: Some("http://localhost:4317".to_string()),
1074 protocol: "grpc".to_string(),
1075 sampling_rate: 1.0,
1076 }
1077 }
1078}
1079
1080#[derive(Debug, Clone, Serialize, Deserialize)]
1082#[serde(default)]
1083pub struct RecorderConfig {
1084 pub enabled: bool,
1086 pub database_path: String,
1088 pub api_enabled: bool,
1090 pub api_port: Option<u16>,
1092 pub max_requests: i64,
1094 pub retention_days: i64,
1096 pub record_http: bool,
1098 pub record_grpc: bool,
1100 pub record_websocket: bool,
1102 pub record_graphql: bool,
1104}
1105
1106impl Default for RecorderConfig {
1107 fn default() -> Self {
1108 Self {
1109 enabled: false,
1110 database_path: "./mockforge-recordings.db".to_string(),
1111 api_enabled: true,
1112 api_port: None,
1113 max_requests: 10000,
1114 retention_days: 7,
1115 record_http: true,
1116 record_grpc: true,
1117 record_websocket: true,
1118 record_graphql: true,
1119 }
1120 }
1121}
1122
1123#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1125#[serde(default)]
1126pub struct ChaosEngConfig {
1127 pub enabled: bool,
1129 pub latency: Option<LatencyInjectionConfig>,
1131 pub fault_injection: Option<FaultConfig>,
1133 pub rate_limit: Option<RateLimitingConfig>,
1135 pub traffic_shaping: Option<NetworkShapingConfig>,
1137 pub scenario: Option<String>,
1139}
1140
1141#[derive(Debug, Clone, Serialize, Deserialize)]
1143pub struct LatencyInjectionConfig {
1144 pub enabled: bool,
1146 pub fixed_delay_ms: Option<u64>,
1148 pub random_delay_range_ms: Option<(u64, u64)>,
1150 pub jitter_percent: f64,
1152 pub probability: f64,
1154}
1155
1156#[derive(Debug, Clone, Serialize, Deserialize)]
1158pub struct FaultConfig {
1159 pub enabled: bool,
1161 pub http_errors: Vec<u16>,
1163 pub http_error_probability: f64,
1165 pub connection_errors: bool,
1167 pub connection_error_probability: f64,
1169 pub timeout_errors: bool,
1171 pub timeout_ms: u64,
1173 pub timeout_probability: f64,
1175}
1176
1177#[derive(Debug, Clone, Serialize, Deserialize)]
1179pub struct RateLimitingConfig {
1180 pub enabled: bool,
1182 pub requests_per_second: u32,
1184 pub burst_size: u32,
1186 pub per_ip: bool,
1188 pub per_endpoint: bool,
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize)]
1194pub struct NetworkShapingConfig {
1195 pub enabled: bool,
1197 pub bandwidth_limit_bps: u64,
1199 pub packet_loss_percent: f64,
1201 pub max_connections: u32,
1203}
1204
1205pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1207 let content = fs::read_to_string(&path)
1208 .await
1209 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1210
1211 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1213 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1214 {
1215 serde_yaml::from_str(&content).map_err(|e| {
1216 let error_msg = e.to_string();
1218 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
1219
1220 if error_msg.contains("missing field") {
1222 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1223 full_msg.push_str(
1224 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1225 );
1226 full_msg.push_str("\n See config.template.yaml for all available options.");
1227 } else if error_msg.contains("unknown field") {
1228 full_msg.push_str("\n\n💡 Check for typos in field names.");
1229 full_msg.push_str("\n See config.template.yaml for valid field names.");
1230 }
1231
1232 Error::generic(full_msg)
1233 })?
1234 } else {
1235 serde_json::from_str(&content).map_err(|e| {
1236 let error_msg = e.to_string();
1238 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
1239
1240 if error_msg.contains("missing field") {
1242 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1243 full_msg.push_str(
1244 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1245 );
1246 full_msg.push_str("\n See config.template.yaml for all available options.");
1247 } else if error_msg.contains("unknown field") {
1248 full_msg.push_str("\n\n💡 Check for typos in field names.");
1249 full_msg.push_str("\n See config.template.yaml for valid field names.");
1250 }
1251
1252 Error::generic(full_msg)
1253 })?
1254 };
1255
1256 Ok(config)
1257}
1258
1259pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1261 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1262 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1263 {
1264 serde_yaml::to_string(config)
1265 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1266 } else {
1267 serde_json::to_string_pretty(config)
1268 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1269 };
1270
1271 fs::write(path, content)
1272 .await
1273 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1274
1275 Ok(())
1276}
1277
1278pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1280 match load_config(&path).await {
1281 Ok(config) => {
1282 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1283 config
1284 }
1285 Err(e) => {
1286 tracing::warn!(
1287 "Failed to load config from {:?}: {}. Using defaults.",
1288 path.as_ref(),
1289 e
1290 );
1291 ServerConfig::default()
1292 }
1293 }
1294}
1295
1296pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1298 let config = ServerConfig::default();
1299 save_config(path, &config).await?;
1300 Ok(())
1301}
1302
1303pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1305 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1307 if let Ok(port_num) = port.parse() {
1308 config.http.port = port_num;
1309 }
1310 }
1311
1312 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1313 config.http.host = host;
1314 }
1315
1316 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1318 if let Ok(port_num) = port.parse() {
1319 config.websocket.port = port_num;
1320 }
1321 }
1322
1323 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1325 if let Ok(port_num) = port.parse() {
1326 config.grpc.port = port_num;
1327 }
1328 }
1329
1330 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1332 if let Ok(port_num) = port.parse() {
1333 config.smtp.port = port_num;
1334 }
1335 }
1336
1337 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1338 config.smtp.host = host;
1339 }
1340
1341 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1342 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1343 }
1344
1345 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1346 config.smtp.hostname = hostname;
1347 }
1348
1349 if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
1351 if let Ok(port_num) = port.parse() {
1352 config.tcp.port = port_num;
1353 }
1354 }
1355
1356 if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
1357 config.tcp.host = host;
1358 }
1359
1360 if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
1361 config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1362 }
1363
1364 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1366 if let Ok(port_num) = port.parse() {
1367 config.admin.port = port_num;
1368 }
1369 }
1370
1371 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1372 config.admin.enabled = true;
1373 }
1374
1375 if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
1377 config.admin.host = host;
1378 }
1379
1380 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1381 if !mount_path.trim().is_empty() {
1382 config.admin.mount_path = Some(mount_path);
1383 }
1384 }
1385
1386 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1387 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1388 config.admin.api_enabled = on;
1389 }
1390
1391 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1392 config.admin.prometheus_url = prometheus_url;
1393 }
1394
1395 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1397 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1398 config.core.latency_enabled = enabled;
1399 }
1400
1401 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1402 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1403 config.core.failures_enabled = enabled;
1404 }
1405
1406 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1407 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1408 config.core.overrides_enabled = enabled;
1409 }
1410
1411 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1412 let enabled =
1413 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1414 config.core.traffic_shaping_enabled = enabled;
1415 }
1416
1417 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1419 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1420 config.core.traffic_shaping.bandwidth.enabled = enabled;
1421 }
1422
1423 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1424 if let Ok(bytes) = max_bytes_per_sec.parse() {
1425 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1426 config.core.traffic_shaping.bandwidth.enabled = true;
1427 }
1428 }
1429
1430 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1431 if let Ok(bytes) = burst_capacity.parse() {
1432 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1433 }
1434 }
1435
1436 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1437 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1438 config.core.traffic_shaping.burst_loss.enabled = enabled;
1439 }
1440
1441 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1442 if let Ok(prob) = burst_probability.parse::<f64>() {
1443 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1444 config.core.traffic_shaping.burst_loss.enabled = true;
1445 }
1446 }
1447
1448 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1449 if let Ok(ms) = burst_duration.parse() {
1450 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1451 }
1452 }
1453
1454 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1455 if let Ok(rate) = loss_rate.parse::<f64>() {
1456 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1457 }
1458 }
1459
1460 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1461 if let Ok(ms) = recovery_time.parse() {
1462 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1463 }
1464 }
1465
1466 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1468 config.logging.level = level;
1469 }
1470
1471 config
1472}
1473
1474pub fn validate_config(config: &ServerConfig) -> Result<()> {
1476 if config.http.port == 0 {
1478 return Err(Error::generic("HTTP port cannot be 0"));
1479 }
1480 if config.websocket.port == 0 {
1481 return Err(Error::generic("WebSocket port cannot be 0"));
1482 }
1483 if config.grpc.port == 0 {
1484 return Err(Error::generic("gRPC port cannot be 0"));
1485 }
1486 if config.admin.port == 0 {
1487 return Err(Error::generic("Admin port cannot be 0"));
1488 }
1489
1490 let ports = [
1492 ("HTTP", config.http.port),
1493 ("WebSocket", config.websocket.port),
1494 ("gRPC", config.grpc.port),
1495 ("Admin", config.admin.port),
1496 ];
1497
1498 for i in 0..ports.len() {
1499 for j in (i + 1)..ports.len() {
1500 if ports[i].1 == ports[j].1 {
1501 return Err(Error::generic(format!(
1502 "Port conflict: {} and {} both use port {}",
1503 ports[i].0, ports[j].0, ports[i].1
1504 )));
1505 }
1506 }
1507 }
1508
1509 let valid_levels = ["trace", "debug", "info", "warn", "error"];
1511 if !valid_levels.contains(&config.logging.level.as_str()) {
1512 return Err(Error::generic(format!(
1513 "Invalid log level: {}. Valid levels: {}",
1514 config.logging.level,
1515 valid_levels.join(", ")
1516 )));
1517 }
1518
1519 Ok(())
1520}
1521
1522pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1524 macro_rules! merge_field {
1526 ($field:ident) => {
1527 if let Some(override_val) = profile.$field {
1528 base.$field = override_val;
1529 }
1530 };
1531 }
1532
1533 merge_field!(http);
1534 merge_field!(websocket);
1535 merge_field!(graphql);
1536 merge_field!(grpc);
1537 merge_field!(mqtt);
1538 merge_field!(smtp);
1539 merge_field!(ftp);
1540 merge_field!(kafka);
1541 merge_field!(amqp);
1542 merge_field!(tcp);
1543 merge_field!(admin);
1544 merge_field!(chaining);
1545 merge_field!(core);
1546 merge_field!(logging);
1547 merge_field!(data);
1548 merge_field!(observability);
1549 merge_field!(multi_tenant);
1550 merge_field!(routes);
1551 merge_field!(protocols);
1552
1553 base
1554}
1555
1556pub async fn load_config_with_profile<P: AsRef<Path>>(
1558 path: P,
1559 profile_name: Option<&str>,
1560) -> Result<ServerConfig> {
1561 let mut config = load_config_auto(&path).await?;
1563
1564 if let Some(profile) = profile_name {
1566 if let Some(profile_config) = config.profiles.remove(profile) {
1567 tracing::info!("Applying profile: {}", profile);
1568 config = apply_profile(config, profile_config);
1569 } else {
1570 return Err(Error::generic(format!(
1571 "Profile '{}' not found in configuration. Available profiles: {}",
1572 profile,
1573 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
1574 )));
1575 }
1576 }
1577
1578 config.profiles.clear();
1580
1581 Ok(config)
1582}
1583
1584pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1586 use rquickjs::{Context, Runtime};
1587
1588 let content = fs::read_to_string(&path)
1589 .await
1590 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
1591
1592 let runtime = Runtime::new()
1594 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
1595 let context = Context::full(&runtime)
1596 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
1597
1598 context.with(|ctx| {
1599 let js_content = if path
1602 .as_ref()
1603 .extension()
1604 .and_then(|s| s.to_str())
1605 .map(|ext| ext == "ts")
1606 .unwrap_or(false)
1607 {
1608 strip_typescript_types(&content)?
1609 } else {
1610 content
1611 };
1612
1613 let result: rquickjs::Value = ctx
1615 .eval(js_content.as_bytes())
1616 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
1617
1618 let json_str: String = ctx
1620 .json_stringify(result)
1621 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
1622 .ok_or_else(|| Error::generic("JS config returned undefined"))?
1623 .get()
1624 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
1625
1626 serde_json::from_str(&json_str).map_err(|e| {
1628 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
1629 })
1630 })
1631}
1632
1633fn strip_typescript_types(content: &str) -> Result<String> {
1640 use regex::Regex;
1641
1642 let mut result = content.to_string();
1643
1644 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
1650 .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
1651 result = interface_re.replace_all(&result, "").to_string();
1652
1653 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
1655 .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
1656 result = type_alias_re.replace_all(&result, "").to_string();
1657
1658 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
1660 .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
1661 result = type_annotation_re.replace_all(&result, "").to_string();
1662
1663 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
1665 .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
1666 result = type_import_re.replace_all(&result, "").to_string();
1667
1668 let as_type_re = Regex::new(r"\s+as\s+\w+")
1670 .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
1671 result = as_type_re.replace_all(&result, "").to_string();
1672
1673 Ok(result)
1674}
1675
1676pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1678 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
1679
1680 match ext {
1681 "ts" | "js" => load_config_from_js(&path).await,
1682 "yaml" | "yml" | "json" => load_config(&path).await,
1683 _ => Err(Error::generic(format!(
1684 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
1685 ext
1686 ))),
1687 }
1688}
1689
1690pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
1692 let current_dir = std::env::current_dir()
1693 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
1694
1695 let config_names = vec![
1696 "mockforge.config.ts",
1697 "mockforge.config.js",
1698 "mockforge.yaml",
1699 "mockforge.yml",
1700 ".mockforge.yaml",
1701 ".mockforge.yml",
1702 ];
1703
1704 for name in &config_names {
1706 let path = current_dir.join(name);
1707 if tokio::fs::metadata(&path).await.is_ok() {
1708 return Ok(path);
1709 }
1710 }
1711
1712 let mut dir = current_dir.clone();
1714 for _ in 0..5 {
1715 if let Some(parent) = dir.parent() {
1716 for name in &config_names {
1717 let path = parent.join(name);
1718 if tokio::fs::metadata(&path).await.is_ok() {
1719 return Ok(path);
1720 }
1721 }
1722 dir = parent.to_path_buf();
1723 } else {
1724 break;
1725 }
1726 }
1727
1728 Err(Error::generic(
1729 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
1730 ))
1731}
1732
1733#[cfg(test)]
1734mod tests {
1735 use super::*;
1736
1737 #[test]
1738 fn test_default_config() {
1739 let config = ServerConfig::default();
1740 assert_eq!(config.http.port, 3000);
1741 assert_eq!(config.websocket.port, 3001);
1742 assert_eq!(config.grpc.port, 50051);
1743 assert_eq!(config.admin.port, 9080);
1744 }
1745
1746 #[test]
1747 fn test_config_validation() {
1748 let mut config = ServerConfig::default();
1749 assert!(validate_config(&config).is_ok());
1750
1751 config.websocket.port = config.http.port;
1753 assert!(validate_config(&config).is_err());
1754
1755 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
1758 assert!(validate_config(&config).is_err());
1759 }
1760
1761 #[test]
1762 fn test_apply_profile() {
1763 let mut base = ServerConfig::default();
1764 assert_eq!(base.http.port, 3000);
1765
1766 let mut profile = ProfileConfig::default();
1767 profile.http = Some(HttpConfig {
1768 port: 8080,
1769 ..Default::default()
1770 });
1771 profile.logging = Some(LoggingConfig {
1772 level: "debug".to_string(),
1773 ..Default::default()
1774 });
1775
1776 let merged = apply_profile(base, profile);
1777 assert_eq!(merged.http.port, 8080);
1778 assert_eq!(merged.logging.level, "debug");
1779 assert_eq!(merged.websocket.port, 3001); }
1781
1782 #[test]
1783 fn test_strip_typescript_types() {
1784 let ts_code = r#"
1785interface Config {
1786 port: number;
1787 host: string;
1788}
1789
1790const config: Config = {
1791 port: 3000,
1792 host: "localhost"
1793} as Config;
1794"#;
1795
1796 let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
1797 assert!(!stripped.contains("interface"));
1798 assert!(!stripped.contains(": Config"));
1799 assert!(!stripped.contains("as Config"));
1800 }
1801}