1use crate::{Config as CoreConfig, Error, RealityLevel, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7use tokio::fs;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(default)]
13pub struct IncidentConfig {
14 pub storage: IncidentStorageConfig,
16 pub external_integrations: crate::incidents::integrations::ExternalIntegrationConfig,
18 pub webhooks: Vec<crate::incidents::integrations::WebhookConfig>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25pub struct IncidentStorageConfig {
26 pub use_cache: bool,
28 pub use_database: bool,
30 pub retention_days: u32,
32}
33
34impl Default for IncidentStorageConfig {
35 fn default() -> Self {
36 Self {
37 use_cache: true,
38 use_database: true,
39 retention_days: 90,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, Default)]
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
47#[serde(default)]
48pub struct ConsumerContractsConfig {
49 pub enabled: bool,
51 pub auto_register: bool,
53 pub track_usage: bool,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[serde(default)]
61pub struct BehavioralCloningConfig {
62 pub enabled: bool,
64 pub database_path: Option<String>,
66 pub enable_middleware: bool,
68 pub min_sequence_frequency: f64,
70 pub min_requests_per_trace: Option<i32>,
72 #[serde(default)]
74 pub flow_recording: FlowRecordingConfig,
75 #[serde(default)]
77 pub scenario_replay: ScenarioReplayConfig,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
83#[serde(default)]
84pub struct FlowRecordingConfig {
85 pub enabled: bool,
87 pub group_by: String,
89 pub time_window_seconds: u64,
91}
92
93impl Default for FlowRecordingConfig {
94 fn default() -> Self {
95 Self {
96 enabled: true,
97 group_by: "trace_id".to_string(),
98 time_window_seconds: 300, }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[serde(default)]
107pub struct ScenarioReplayConfig {
108 pub enabled: bool,
110 pub default_mode: String,
112 pub active_scenarios: Vec<String>,
114}
115
116impl Default for ScenarioReplayConfig {
117 fn default() -> Self {
118 Self {
119 enabled: true,
120 default_mode: "strict".to_string(),
121 active_scenarios: Vec::new(),
122 }
123 }
124}
125
126impl Default for BehavioralCloningConfig {
127 fn default() -> Self {
128 Self {
129 enabled: false,
130 database_path: None,
131 enable_middleware: false,
132 min_sequence_frequency: 0.1, min_requests_per_trace: None,
134 flow_recording: FlowRecordingConfig::default(),
135 scenario_replay: ScenarioReplayConfig::default(),
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(default)]
143#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
144pub struct AuthConfig {
145 pub jwt: Option<JwtConfig>,
147 pub oauth2: Option<OAuth2Config>,
149 pub basic_auth: Option<BasicAuthConfig>,
151 pub api_key: Option<ApiKeyConfig>,
153 pub require_auth: bool,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
160pub struct JwtConfig {
161 pub secret: Option<String>,
163 pub rsa_public_key: Option<String>,
165 pub ecdsa_public_key: Option<String>,
167 pub issuer: Option<String>,
169 pub audience: Option<String>,
171 pub algorithms: Vec<String>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
178pub struct OAuth2Config {
179 pub client_id: String,
181 pub client_secret: String,
183 pub introspection_url: String,
185 pub auth_url: Option<String>,
187 pub token_url: Option<String>,
189 pub token_type_hint: Option<String>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
196pub struct BasicAuthConfig {
197 pub credentials: HashMap<String, String>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
204pub struct ApiKeyConfig {
205 pub header_name: String,
207 pub query_name: Option<String>,
209 pub keys: Vec<String>,
211}
212
213impl Default for AuthConfig {
214 fn default() -> Self {
215 Self {
216 jwt: None,
217 oauth2: None,
218 basic_auth: None,
219 api_key: Some(ApiKeyConfig {
220 header_name: "X-API-Key".to_string(),
221 query_name: None,
222 keys: vec![],
223 }),
224 require_auth: false,
225 }
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
232pub struct RouteConfig {
233 pub path: String,
235 pub method: String,
237 pub request: Option<RouteRequestConfig>,
239 pub response: RouteResponseConfig,
241 #[serde(default)]
243 pub fault_injection: Option<RouteFaultInjectionConfig>,
244 #[serde(default)]
246 pub latency: Option<RouteLatencyConfig>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
252pub struct RouteRequestConfig {
253 pub validation: Option<RouteValidationConfig>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260pub struct RouteResponseConfig {
261 pub status: u16,
263 #[serde(default)]
265 pub headers: HashMap<String, String>,
266 pub body: Option<serde_json::Value>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
273pub struct RouteValidationConfig {
274 pub schema: serde_json::Value,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
281pub struct RouteFaultInjectionConfig {
282 pub enabled: bool,
284 pub probability: f64,
286 pub fault_types: Vec<RouteFaultType>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
293#[serde(tag = "type", rename_all = "snake_case")]
294pub enum RouteFaultType {
295 HttpError {
297 status_code: u16,
299 message: Option<String>,
301 },
302 ConnectionError {
304 message: Option<String>,
306 },
307 Timeout {
309 duration_ms: u64,
311 message: Option<String>,
313 },
314 PartialResponse {
316 truncate_percent: f64,
318 },
319 PayloadCorruption {
321 corruption_type: String,
323 },
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
329pub struct RouteLatencyConfig {
330 pub enabled: bool,
332 pub probability: f64,
334 pub fixed_delay_ms: Option<u64>,
336 pub random_delay_range_ms: Option<(u64, u64)>,
338 pub jitter_percent: f64,
340 #[serde(default)]
342 pub distribution: LatencyDistribution,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
347#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
348#[serde(rename_all = "snake_case")]
349pub enum LatencyDistribution {
350 Fixed,
352 Normal {
354 mean_ms: f64,
356 std_dev_ms: f64,
358 },
359 Exponential {
361 lambda: f64,
363 },
364 Uniform,
366}
367
368impl Default for LatencyDistribution {
369 fn default() -> Self {
370 Self::Fixed
371 }
372}
373
374impl Default for RouteFaultInjectionConfig {
375 fn default() -> Self {
376 Self {
377 enabled: false,
378 probability: 0.0,
379 fault_types: Vec::new(),
380 }
381 }
382}
383
384impl Default for RouteLatencyConfig {
385 fn default() -> Self {
386 Self {
387 enabled: false,
388 probability: 1.0,
389 fixed_delay_ms: None,
390 random_delay_range_ms: None,
391 jitter_percent: 0.0,
392 distribution: LatencyDistribution::Fixed,
393 }
394 }
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399#[serde(default)]
400#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
401pub struct DeceptiveDeployConfig {
402 pub enabled: bool,
404 pub cors: Option<ProductionCorsConfig>,
406 pub rate_limit: Option<ProductionRateLimitConfig>,
408 #[serde(default)]
410 pub headers: HashMap<String, String>,
411 pub oauth: Option<ProductionOAuthConfig>,
413 pub custom_domain: Option<String>,
415 pub auto_tunnel: bool,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub canary: Option<crate::deceptive_canary::DeceptiveCanaryConfig>,
420}
421
422impl Default for DeceptiveDeployConfig {
423 fn default() -> Self {
424 Self {
425 enabled: false,
426 cors: None,
427 rate_limit: None,
428 headers: HashMap::new(),
429 oauth: None,
430 custom_domain: None,
431 auto_tunnel: false,
432 canary: None,
433 }
434 }
435}
436
437impl DeceptiveDeployConfig {
438 pub fn production_preset() -> Self {
440 let mut headers = HashMap::new();
441 headers.insert("X-API-Version".to_string(), "1.0".to_string());
442 headers.insert("X-Request-ID".to_string(), "{{uuid}}".to_string());
443 headers.insert("X-Powered-By".to_string(), "MockForge".to_string());
444
445 Self {
446 enabled: true,
447 cors: Some(ProductionCorsConfig {
448 allowed_origins: vec!["*".to_string()],
449 allowed_methods: vec![
450 "GET".to_string(),
451 "POST".to_string(),
452 "PUT".to_string(),
453 "DELETE".to_string(),
454 "PATCH".to_string(),
455 "OPTIONS".to_string(),
456 ],
457 allowed_headers: vec!["*".to_string()],
458 allow_credentials: true,
459 }),
460 rate_limit: Some(ProductionRateLimitConfig {
461 requests_per_minute: 1000,
462 burst: 2000,
463 per_ip: true,
464 }),
465 headers,
466 oauth: None, custom_domain: None,
468 auto_tunnel: true,
469 canary: None,
470 }
471 }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
477pub struct ProductionCorsConfig {
478 #[serde(default)]
480 pub allowed_origins: Vec<String>,
481 #[serde(default)]
483 pub allowed_methods: Vec<String>,
484 #[serde(default)]
486 pub allowed_headers: Vec<String>,
487 pub allow_credentials: bool,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
494pub struct ProductionRateLimitConfig {
495 pub requests_per_minute: u32,
497 pub burst: u32,
499 pub per_ip: bool,
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
505#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
506pub struct ProductionOAuthConfig {
507 pub client_id: String,
509 pub client_secret: String,
511 pub introspection_url: String,
513 pub auth_url: Option<String>,
515 pub token_url: Option<String>,
517 pub token_type_hint: Option<String>,
519}
520
521impl From<ProductionOAuthConfig> for OAuth2Config {
522 fn from(prod: ProductionOAuthConfig) -> Self {
524 OAuth2Config {
525 client_id: prod.client_id,
526 client_secret: prod.client_secret,
527 introspection_url: prod.introspection_url,
528 auth_url: prod.auth_url,
529 token_url: prod.token_url,
530 token_type_hint: prod.token_type_hint,
531 }
532 }
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
537#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
538pub struct ProtocolConfig {
539 pub enabled: bool,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
545#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
546pub struct ProtocolsConfig {
547 pub http: ProtocolConfig,
549 pub graphql: ProtocolConfig,
551 pub grpc: ProtocolConfig,
553 pub websocket: ProtocolConfig,
555 pub smtp: ProtocolConfig,
557 pub mqtt: ProtocolConfig,
559 pub ftp: ProtocolConfig,
561 pub kafka: ProtocolConfig,
563 pub rabbitmq: ProtocolConfig,
565 pub amqp: ProtocolConfig,
567 pub tcp: ProtocolConfig,
569}
570
571impl Default for ProtocolsConfig {
572 fn default() -> Self {
573 Self {
574 http: ProtocolConfig { enabled: true },
575 graphql: ProtocolConfig { enabled: true },
576 grpc: ProtocolConfig { enabled: true },
577 websocket: ProtocolConfig { enabled: true },
578 smtp: ProtocolConfig { enabled: false },
579 mqtt: ProtocolConfig { enabled: true },
580 ftp: ProtocolConfig { enabled: false },
581 kafka: ProtocolConfig { enabled: false },
582 rabbitmq: ProtocolConfig { enabled: false },
583 amqp: ProtocolConfig { enabled: false },
584 tcp: ProtocolConfig { enabled: false },
585 }
586 }
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize)]
595#[serde(default)]
596#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
597pub struct RealitySliderConfig {
598 pub level: RealityLevel,
600 pub enabled: bool,
602}
603
604impl Default for RealitySliderConfig {
605 fn default() -> Self {
606 Self {
607 level: RealityLevel::ModerateRealism,
608 enabled: true,
609 }
610 }
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, Default)]
615#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
616#[serde(default)]
617pub struct ServerConfig {
618 pub http: HttpConfig,
620 pub websocket: WebSocketConfig,
622 pub graphql: GraphQLConfig,
624 pub grpc: GrpcConfig,
626 pub mqtt: MqttConfig,
628 pub smtp: SmtpConfig,
630 pub ftp: FtpConfig,
632 pub kafka: KafkaConfig,
634 pub amqp: AmqpConfig,
636 pub tcp: TcpConfig,
638 pub admin: AdminConfig,
640 pub chaining: ChainingConfig,
642 pub core: CoreConfig,
644 pub logging: LoggingConfig,
646 pub data: DataConfig,
648 #[serde(default)]
650 pub mockai: MockAIConfig,
651 pub observability: ObservabilityConfig,
653 pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
655 #[serde(default)]
657 pub routes: Vec<RouteConfig>,
658 #[serde(default)]
660 pub protocols: ProtocolsConfig,
661 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
663 pub profiles: HashMap<String, ProfileConfig>,
664 #[serde(default)]
666 pub deceptive_deploy: DeceptiveDeployConfig,
667 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub behavioral_cloning: Option<BehavioralCloningConfig>,
670 #[serde(default)]
672 pub reality: RealitySliderConfig,
673 #[serde(default)]
675 pub reality_continuum: crate::reality_continuum::ContinuumConfig,
676 #[serde(default)]
678 pub security: SecurityConfig,
679 #[serde(default)]
681 pub drift_budget: crate::contract_drift::DriftBudgetConfig,
682 #[serde(default)]
684 pub incidents: IncidentConfig,
685 #[serde(default)]
687 pub pr_generation: crate::pr_generation::PRGenerationConfig,
688 #[serde(default)]
690 pub consumer_contracts: ConsumerContractsConfig,
691}
692
693#[derive(Debug, Clone, Serialize, Deserialize, Default)]
695#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
696#[serde(default)]
697pub struct ProfileConfig {
698 #[serde(skip_serializing_if = "Option::is_none")]
700 pub http: Option<HttpConfig>,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub websocket: Option<WebSocketConfig>,
704 #[serde(skip_serializing_if = "Option::is_none")]
706 pub graphql: Option<GraphQLConfig>,
707 #[serde(skip_serializing_if = "Option::is_none")]
709 pub grpc: Option<GrpcConfig>,
710 #[serde(skip_serializing_if = "Option::is_none")]
712 pub mqtt: Option<MqttConfig>,
713 #[serde(skip_serializing_if = "Option::is_none")]
715 pub smtp: Option<SmtpConfig>,
716 #[serde(skip_serializing_if = "Option::is_none")]
718 pub ftp: Option<FtpConfig>,
719 #[serde(skip_serializing_if = "Option::is_none")]
721 pub kafka: Option<KafkaConfig>,
722 #[serde(skip_serializing_if = "Option::is_none")]
724 pub amqp: Option<AmqpConfig>,
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub tcp: Option<TcpConfig>,
728 #[serde(skip_serializing_if = "Option::is_none")]
730 pub admin: Option<AdminConfig>,
731 #[serde(skip_serializing_if = "Option::is_none")]
733 pub chaining: Option<ChainingConfig>,
734 #[serde(skip_serializing_if = "Option::is_none")]
736 pub core: Option<CoreConfig>,
737 #[serde(skip_serializing_if = "Option::is_none")]
739 pub logging: Option<LoggingConfig>,
740 #[serde(skip_serializing_if = "Option::is_none")]
742 pub data: Option<DataConfig>,
743 #[serde(skip_serializing_if = "Option::is_none")]
745 pub mockai: Option<MockAIConfig>,
746 #[serde(skip_serializing_if = "Option::is_none")]
748 pub observability: Option<ObservabilityConfig>,
749 #[serde(skip_serializing_if = "Option::is_none")]
751 pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
752 #[serde(skip_serializing_if = "Option::is_none")]
754 pub routes: Option<Vec<RouteConfig>>,
755 #[serde(skip_serializing_if = "Option::is_none")]
757 pub protocols: Option<ProtocolsConfig>,
758 #[serde(skip_serializing_if = "Option::is_none")]
760 pub deceptive_deploy: Option<DeceptiveDeployConfig>,
761 #[serde(skip_serializing_if = "Option::is_none")]
763 pub reality: Option<RealitySliderConfig>,
764 #[serde(skip_serializing_if = "Option::is_none")]
766 pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
767 #[serde(skip_serializing_if = "Option::is_none")]
769 pub security: Option<SecurityConfig>,
770}
771
772#[derive(Debug, Clone, Serialize, Deserialize)]
776#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
777pub struct HttpValidationConfig {
778 pub mode: String,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize, Default)]
784#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
785pub struct HttpCorsConfig {
786 pub enabled: bool,
788 #[serde(default)]
790 pub allowed_origins: Vec<String>,
791 #[serde(default)]
793 pub allowed_methods: Vec<String>,
794 #[serde(default)]
796 pub allowed_headers: Vec<String>,
797 #[serde(default = "default_cors_allow_credentials")]
800 pub allow_credentials: bool,
801}
802
803fn default_cors_allow_credentials() -> bool {
804 false
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize)]
809#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
810#[serde(default)]
811pub struct HttpConfig {
812 pub enabled: bool,
814 pub port: u16,
816 pub host: String,
818 pub openapi_spec: Option<String>,
820 pub cors: Option<HttpCorsConfig>,
822 pub request_timeout_secs: u64,
824 pub validation: Option<HttpValidationConfig>,
826 pub aggregate_validation_errors: bool,
828 pub validate_responses: bool,
830 pub response_template_expand: bool,
832 pub validation_status: Option<u16>,
834 pub validation_overrides: std::collections::HashMap<String, String>,
836 pub skip_admin_validation: bool,
838 pub auth: Option<AuthConfig>,
840 #[serde(skip_serializing_if = "Option::is_none")]
842 pub tls: Option<HttpTlsConfig>,
843}
844
845impl Default for HttpConfig {
846 fn default() -> Self {
847 Self {
848 enabled: true,
849 port: 3000,
850 host: "0.0.0.0".to_string(),
851 openapi_spec: None,
852 cors: Some(HttpCorsConfig {
853 enabled: true,
854 allowed_origins: vec!["*".to_string()],
855 allowed_methods: vec![
856 "GET".to_string(),
857 "POST".to_string(),
858 "PUT".to_string(),
859 "DELETE".to_string(),
860 "PATCH".to_string(),
861 "OPTIONS".to_string(),
862 ],
863 allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
864 allow_credentials: false, }),
866 request_timeout_secs: 30,
867 validation: Some(HttpValidationConfig {
868 mode: "enforce".to_string(),
869 }),
870 aggregate_validation_errors: true,
871 validate_responses: false,
872 response_template_expand: false,
873 validation_status: None,
874 validation_overrides: std::collections::HashMap::new(),
875 skip_admin_validation: true,
876 auth: None,
877 tls: None,
878 }
879 }
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize)]
884#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
885pub struct HttpTlsConfig {
886 pub enabled: bool,
888 pub cert_file: String,
890 pub key_file: String,
892 #[serde(skip_serializing_if = "Option::is_none")]
894 pub ca_file: Option<String>,
895 #[serde(default = "default_tls_min_version")]
897 pub min_version: String,
898 #[serde(default, skip_serializing_if = "Vec::is_empty")]
900 pub cipher_suites: Vec<String>,
901 #[serde(default)]
903 pub require_client_cert: bool,
904}
905
906fn default_tls_min_version() -> String {
907 "1.2".to_string()
908}
909
910impl Default for HttpTlsConfig {
911 fn default() -> Self {
912 Self {
913 enabled: true,
914 cert_file: String::new(),
915 key_file: String::new(),
916 ca_file: None,
917 min_version: "1.2".to_string(),
918 cipher_suites: Vec::new(),
919 require_client_cert: false,
920 }
921 }
922}
923
924#[derive(Debug, Clone, Serialize, Deserialize)]
926#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
927#[serde(default)]
928pub struct WebSocketConfig {
929 pub enabled: bool,
931 pub port: u16,
933 pub host: String,
935 pub replay_file: Option<String>,
937 pub connection_timeout_secs: u64,
939}
940
941impl Default for WebSocketConfig {
942 fn default() -> Self {
943 Self {
944 enabled: true,
945 port: 3001,
946 host: "0.0.0.0".to_string(),
947 replay_file: None,
948 connection_timeout_secs: 300,
949 }
950 }
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize)]
955#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
956#[serde(default)]
957pub struct GrpcConfig {
958 pub enabled: bool,
960 pub port: u16,
962 pub host: String,
964 pub proto_dir: Option<String>,
966 pub tls: Option<TlsConfig>,
968}
969
970impl Default for GrpcConfig {
971 fn default() -> Self {
972 Self {
973 enabled: true,
974 port: 50051,
975 host: "0.0.0.0".to_string(),
976 proto_dir: None,
977 tls: None,
978 }
979 }
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
984#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
985#[serde(default)]
986pub struct GraphQLConfig {
987 pub enabled: bool,
989 pub port: u16,
991 pub host: String,
993 pub schema_path: Option<String>,
995 pub handlers_dir: Option<String>,
997 pub playground_enabled: bool,
999 pub upstream_url: Option<String>,
1001 pub introspection_enabled: bool,
1003}
1004
1005impl Default for GraphQLConfig {
1006 fn default() -> Self {
1007 Self {
1008 enabled: true,
1009 port: 4000,
1010 host: "0.0.0.0".to_string(),
1011 schema_path: None,
1012 handlers_dir: None,
1013 playground_enabled: true,
1014 upstream_url: None,
1015 introspection_enabled: true,
1016 }
1017 }
1018}
1019
1020#[derive(Debug, Clone, Serialize, Deserialize)]
1022#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1023pub struct TlsConfig {
1024 pub cert_path: String,
1026 pub key_path: String,
1028}
1029
1030#[derive(Debug, Clone, Serialize, Deserialize)]
1032#[serde(default)]
1033#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1034pub struct MqttConfig {
1035 pub enabled: bool,
1037 pub port: u16,
1039 pub host: String,
1041 pub max_connections: usize,
1043 pub max_packet_size: usize,
1045 pub keep_alive_secs: u16,
1047 pub fixtures_dir: Option<std::path::PathBuf>,
1049 pub enable_retained_messages: bool,
1051 pub max_retained_messages: usize,
1053}
1054
1055impl Default for MqttConfig {
1056 fn default() -> Self {
1057 Self {
1058 enabled: false,
1059 port: 1883,
1060 host: "0.0.0.0".to_string(),
1061 max_connections: 1000,
1062 max_packet_size: 268435456, keep_alive_secs: 60,
1064 fixtures_dir: None,
1065 enable_retained_messages: true,
1066 max_retained_messages: 10000,
1067 }
1068 }
1069}
1070
1071#[derive(Debug, Clone, Serialize, Deserialize)]
1073#[serde(default)]
1074#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1075pub struct SmtpConfig {
1076 pub enabled: bool,
1078 pub port: u16,
1080 pub host: String,
1082 pub hostname: String,
1084 pub fixtures_dir: Option<std::path::PathBuf>,
1086 pub timeout_secs: u64,
1088 pub max_connections: usize,
1090 pub enable_mailbox: bool,
1092 pub max_mailbox_messages: usize,
1094 pub enable_starttls: bool,
1096 pub tls_cert_path: Option<std::path::PathBuf>,
1098 pub tls_key_path: Option<std::path::PathBuf>,
1100}
1101
1102impl Default for SmtpConfig {
1103 fn default() -> Self {
1104 Self {
1105 enabled: false,
1106 port: 1025,
1107 host: "0.0.0.0".to_string(),
1108 hostname: "mockforge-smtp".to_string(),
1109 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
1110 timeout_secs: 300,
1111 max_connections: 10,
1112 enable_mailbox: true,
1113 max_mailbox_messages: 1000,
1114 enable_starttls: false,
1115 tls_cert_path: None,
1116 tls_key_path: None,
1117 }
1118 }
1119}
1120
1121#[derive(Debug, Clone, Serialize, Deserialize)]
1123#[serde(default)]
1124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1125pub struct FtpConfig {
1126 pub enabled: bool,
1128 pub port: u16,
1130 pub host: String,
1132 pub passive_ports: (u16, u16),
1134 pub max_connections: usize,
1136 pub timeout_secs: u64,
1138 pub allow_anonymous: bool,
1140 pub fixtures_dir: Option<std::path::PathBuf>,
1142 pub virtual_root: std::path::PathBuf,
1144}
1145
1146impl Default for FtpConfig {
1147 fn default() -> Self {
1148 Self {
1149 enabled: false,
1150 port: 2121,
1151 host: "0.0.0.0".to_string(),
1152 passive_ports: (50000, 51000),
1153 max_connections: 100,
1154 timeout_secs: 300,
1155 allow_anonymous: true,
1156 fixtures_dir: None,
1157 virtual_root: std::path::PathBuf::from("/mockforge"),
1158 }
1159 }
1160}
1161
1162#[derive(Debug, Clone, Serialize, Deserialize)]
1164#[serde(default)]
1165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1166pub struct KafkaConfig {
1167 pub enabled: bool,
1169 pub port: u16,
1171 pub host: String,
1173 pub broker_id: i32,
1175 pub max_connections: usize,
1177 pub log_retention_ms: i64,
1179 pub log_segment_bytes: i64,
1181 pub fixtures_dir: Option<std::path::PathBuf>,
1183 pub auto_create_topics: bool,
1185 pub default_partitions: i32,
1187 pub default_replication_factor: i16,
1189}
1190
1191impl Default for KafkaConfig {
1192 fn default() -> Self {
1193 Self {
1194 enabled: false,
1195 port: 9092, host: "0.0.0.0".to_string(),
1197 broker_id: 1,
1198 max_connections: 1000,
1199 log_retention_ms: 604800000, log_segment_bytes: 1073741824, fixtures_dir: None,
1202 auto_create_topics: true,
1203 default_partitions: 3,
1204 default_replication_factor: 1,
1205 }
1206 }
1207}
1208
1209#[derive(Debug, Clone, Serialize, Deserialize)]
1211#[serde(default)]
1212#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1213pub struct AmqpConfig {
1214 pub enabled: bool,
1216 pub port: u16,
1218 pub host: String,
1220 pub max_connections: usize,
1222 pub max_channels_per_connection: u16,
1224 pub frame_max: u32,
1226 pub heartbeat_interval: u16,
1228 pub fixtures_dir: Option<std::path::PathBuf>,
1230 pub virtual_hosts: Vec<String>,
1232}
1233
1234impl Default for AmqpConfig {
1235 fn default() -> Self {
1236 Self {
1237 enabled: false,
1238 port: 5672, host: "0.0.0.0".to_string(),
1240 max_connections: 1000,
1241 max_channels_per_connection: 100,
1242 frame_max: 131072, heartbeat_interval: 60,
1244 fixtures_dir: None,
1245 virtual_hosts: vec!["/".to_string()],
1246 }
1247 }
1248}
1249
1250#[derive(Debug, Clone, Serialize, Deserialize)]
1252#[serde(default)]
1253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1254pub struct TcpConfig {
1255 pub enabled: bool,
1257 pub port: u16,
1259 pub host: String,
1261 pub max_connections: usize,
1263 pub timeout_secs: u64,
1265 pub fixtures_dir: Option<std::path::PathBuf>,
1267 pub echo_mode: bool,
1269 pub enable_tls: bool,
1271 pub tls_cert_path: Option<std::path::PathBuf>,
1273 pub tls_key_path: Option<std::path::PathBuf>,
1275}
1276
1277impl Default for TcpConfig {
1278 fn default() -> Self {
1279 Self {
1280 enabled: false,
1281 port: 9999,
1282 host: "0.0.0.0".to_string(),
1283 max_connections: 1000,
1284 timeout_secs: 300,
1285 fixtures_dir: Some(std::path::PathBuf::from("./fixtures/tcp")),
1286 echo_mode: true,
1287 enable_tls: false,
1288 tls_cert_path: None,
1289 tls_key_path: None,
1290 }
1291 }
1292}
1293
1294#[derive(Debug, Clone, Serialize, Deserialize)]
1296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1297#[serde(default)]
1298pub struct AdminConfig {
1299 pub enabled: bool,
1301 pub port: u16,
1303 pub host: String,
1305 pub auth_required: bool,
1307 pub username: Option<String>,
1309 pub password: Option<String>,
1311 pub mount_path: Option<String>,
1313 pub api_enabled: bool,
1315 pub prometheus_url: String,
1317}
1318
1319impl Default for AdminConfig {
1320 fn default() -> Self {
1321 let default_host = if std::env::var("DOCKER_CONTAINER").is_ok()
1324 || std::env::var("container").is_ok()
1325 || std::path::Path::new("/.dockerenv").exists()
1326 {
1327 "0.0.0.0".to_string()
1328 } else {
1329 "127.0.0.1".to_string()
1330 };
1331
1332 Self {
1333 enabled: false,
1334 port: 9080,
1335 host: default_host,
1336 auth_required: false,
1337 username: None,
1338 password: None,
1339 mount_path: None,
1340 api_enabled: true,
1341 prometheus_url: "http://localhost:9090".to_string(),
1342 }
1343 }
1344}
1345
1346#[derive(Debug, Clone, Serialize, Deserialize)]
1348#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1349#[serde(default)]
1350pub struct LoggingConfig {
1351 pub level: String,
1353 pub json_format: bool,
1355 pub file_path: Option<String>,
1357 pub max_file_size_mb: u64,
1359 pub max_files: u32,
1361}
1362
1363impl Default for LoggingConfig {
1364 fn default() -> Self {
1365 Self {
1366 level: "info".to_string(),
1367 json_format: false,
1368 file_path: None,
1369 max_file_size_mb: 10,
1370 max_files: 5,
1371 }
1372 }
1373}
1374
1375#[derive(Debug, Clone, Serialize, Deserialize)]
1377#[serde(default, rename_all = "camelCase")]
1378#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1379pub struct ChainingConfig {
1380 pub enabled: bool,
1382 pub max_chain_length: usize,
1384 pub global_timeout_secs: u64,
1386 pub enable_parallel_execution: bool,
1388}
1389
1390impl Default for ChainingConfig {
1391 fn default() -> Self {
1392 Self {
1393 enabled: false,
1394 max_chain_length: 20,
1395 global_timeout_secs: 300,
1396 enable_parallel_execution: false,
1397 }
1398 }
1399}
1400
1401#[derive(Debug, Clone, Serialize, Deserialize)]
1403#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1404#[serde(default)]
1405pub struct DataConfig {
1406 pub default_rows: usize,
1408 pub default_format: String,
1410 pub locale: String,
1412 pub templates: HashMap<String, String>,
1414 pub rag: RagConfig,
1416 #[serde(skip_serializing_if = "Option::is_none")]
1418 pub persona_domain: Option<String>,
1419 #[serde(default = "default_false")]
1421 pub persona_consistency_enabled: bool,
1422 #[serde(skip_serializing_if = "Option::is_none")]
1424 pub persona_registry: Option<PersonaRegistryConfig>,
1425}
1426
1427impl Default for DataConfig {
1428 fn default() -> Self {
1429 Self {
1430 default_rows: 100,
1431 default_format: "json".to_string(),
1432 locale: "en".to_string(),
1433 templates: HashMap::new(),
1434 rag: RagConfig::default(),
1435 persona_domain: None,
1436 persona_consistency_enabled: false,
1437 persona_registry: None,
1438 }
1439 }
1440}
1441
1442#[derive(Debug, Clone, Serialize, Deserialize)]
1444#[serde(default)]
1445#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1446pub struct RagConfig {
1447 pub enabled: bool,
1449 #[serde(default)]
1451 pub provider: String,
1452 pub api_endpoint: Option<String>,
1454 pub api_key: Option<String>,
1456 pub model: Option<String>,
1458 #[serde(default = "default_max_tokens")]
1460 pub max_tokens: usize,
1461 #[serde(default = "default_temperature")]
1463 pub temperature: f64,
1464 pub context_window: usize,
1466 #[serde(default = "default_true")]
1468 pub caching: bool,
1469 #[serde(default = "default_cache_ttl")]
1471 pub cache_ttl_secs: u64,
1472 #[serde(default = "default_timeout")]
1474 pub timeout_secs: u64,
1475 #[serde(default = "default_max_retries")]
1477 pub max_retries: usize,
1478}
1479
1480fn default_max_tokens() -> usize {
1481 1024
1482}
1483
1484fn default_temperature() -> f64 {
1485 0.7
1486}
1487
1488fn default_true() -> bool {
1489 true
1490}
1491
1492fn default_cache_ttl() -> u64 {
1493 3600
1494}
1495
1496fn default_timeout() -> u64 {
1497 30
1498}
1499
1500fn default_max_retries() -> usize {
1501 3
1502}
1503
1504fn default_false() -> bool {
1505 false
1506}
1507
1508impl Default for RagConfig {
1509 fn default() -> Self {
1510 Self {
1511 enabled: false,
1512 provider: "openai".to_string(),
1513 api_endpoint: None,
1514 api_key: None,
1515 model: Some("gpt-3.5-turbo".to_string()),
1516 max_tokens: default_max_tokens(),
1517 temperature: default_temperature(),
1518 context_window: 4000,
1519 caching: default_true(),
1520 cache_ttl_secs: default_cache_ttl(),
1521 timeout_secs: default_timeout(),
1522 max_retries: default_max_retries(),
1523 }
1524 }
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize)]
1529#[serde(default)]
1530#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1531pub struct PersonaRegistryConfig {
1532 #[serde(default = "default_false")]
1534 pub persistent: bool,
1535 #[serde(skip_serializing_if = "Option::is_none")]
1537 pub storage_path: Option<String>,
1538 #[serde(default)]
1540 pub default_traits: HashMap<String, String>,
1541}
1542
1543impl Default for PersonaRegistryConfig {
1544 fn default() -> Self {
1545 Self {
1546 persistent: false,
1547 storage_path: None,
1548 default_traits: HashMap::new(),
1549 }
1550 }
1551}
1552
1553#[derive(Debug, Clone, Serialize, Deserialize)]
1555#[serde(default)]
1556#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1557pub struct MockAIConfig {
1558 pub enabled: bool,
1560 pub intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig,
1562 pub auto_learn: bool,
1564 pub mutation_detection: bool,
1566 pub ai_validation_errors: bool,
1568 pub intelligent_pagination: bool,
1570 #[serde(default)]
1572 pub enabled_endpoints: Vec<String>,
1573}
1574
1575impl Default for MockAIConfig {
1576 fn default() -> Self {
1577 Self {
1578 enabled: false,
1579 intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig::default(),
1580 auto_learn: true,
1581 mutation_detection: true,
1582 ai_validation_errors: true,
1583 intelligent_pagination: true,
1584 enabled_endpoints: Vec::new(),
1585 }
1586 }
1587}
1588
1589#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1592#[serde(default)]
1593pub struct ObservabilityConfig {
1594 pub prometheus: PrometheusConfig,
1596 pub opentelemetry: Option<OpenTelemetryConfig>,
1598 pub recorder: Option<RecorderConfig>,
1600 pub chaos: Option<ChaosEngConfig>,
1602}
1603
1604#[derive(Debug, Clone, Serialize, Deserialize)]
1606#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1607#[serde(default)]
1608pub struct SecurityConfig {
1609 pub monitoring: SecurityMonitoringConfig,
1611}
1612
1613impl Default for SecurityConfig {
1614 fn default() -> Self {
1615 Self {
1616 monitoring: SecurityMonitoringConfig::default(),
1617 }
1618 }
1619}
1620
1621#[derive(Debug, Clone, Serialize, Deserialize)]
1623#[serde(default)]
1624#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1625pub struct SecurityMonitoringConfig {
1626 pub siem: crate::security::siem::SiemConfig,
1628 pub access_review: crate::security::access_review::AccessReviewConfig,
1630 pub privileged_access: crate::security::privileged_access::PrivilegedAccessConfig,
1632 pub change_management: crate::security::change_management::ChangeManagementConfig,
1634 pub compliance_dashboard: crate::security::compliance_dashboard::ComplianceDashboardConfig,
1636 pub risk_assessment: crate::security::risk_assessment::RiskAssessmentConfig,
1638}
1639
1640impl Default for SecurityMonitoringConfig {
1641 fn default() -> Self {
1642 Self {
1643 siem: crate::security::siem::SiemConfig::default(),
1644 access_review: crate::security::access_review::AccessReviewConfig::default(),
1645 privileged_access: crate::security::privileged_access::PrivilegedAccessConfig::default(
1646 ),
1647 change_management: crate::security::change_management::ChangeManagementConfig::default(
1648 ),
1649 compliance_dashboard:
1650 crate::security::compliance_dashboard::ComplianceDashboardConfig::default(),
1651 risk_assessment: crate::security::risk_assessment::RiskAssessmentConfig::default(),
1652 }
1653 }
1654}
1655
1656#[derive(Debug, Clone, Serialize, Deserialize)]
1658#[serde(default)]
1659#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1660pub struct PrometheusConfig {
1661 pub enabled: bool,
1663 pub port: u16,
1665 pub host: String,
1667 pub path: String,
1669}
1670
1671impl Default for PrometheusConfig {
1672 fn default() -> Self {
1673 Self {
1674 enabled: true,
1675 port: 9090,
1676 host: "0.0.0.0".to_string(),
1677 path: "/metrics".to_string(),
1678 }
1679 }
1680}
1681
1682#[derive(Debug, Clone, Serialize, Deserialize)]
1684#[serde(default)]
1685#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1686pub struct OpenTelemetryConfig {
1687 pub enabled: bool,
1689 pub service_name: String,
1691 pub environment: String,
1693 pub jaeger_endpoint: String,
1695 pub otlp_endpoint: Option<String>,
1697 pub protocol: String,
1699 pub sampling_rate: f64,
1701}
1702
1703impl Default for OpenTelemetryConfig {
1704 fn default() -> Self {
1705 Self {
1706 enabled: false,
1707 service_name: "mockforge".to_string(),
1708 environment: "development".to_string(),
1709 jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
1710 otlp_endpoint: Some("http://localhost:4317".to_string()),
1711 protocol: "grpc".to_string(),
1712 sampling_rate: 1.0,
1713 }
1714 }
1715}
1716
1717#[derive(Debug, Clone, Serialize, Deserialize)]
1719#[serde(default)]
1720#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1721pub struct RecorderConfig {
1722 pub enabled: bool,
1724 pub database_path: String,
1726 pub api_enabled: bool,
1728 pub api_port: Option<u16>,
1730 pub max_requests: i64,
1732 pub retention_days: i64,
1734 pub record_http: bool,
1736 pub record_grpc: bool,
1738 pub record_websocket: bool,
1740 pub record_graphql: bool,
1742 #[serde(default = "default_true")]
1745 pub record_proxy: bool,
1746}
1747
1748impl Default for RecorderConfig {
1749 fn default() -> Self {
1750 Self {
1751 enabled: false,
1752 database_path: "./mockforge-recordings.db".to_string(),
1753 api_enabled: true,
1754 api_port: None,
1755 max_requests: 10000,
1756 retention_days: 7,
1757 record_http: true,
1758 record_grpc: true,
1759 record_websocket: true,
1760 record_graphql: true,
1761 record_proxy: true,
1762 }
1763 }
1764}
1765
1766#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1768#[serde(default)]
1769#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1770pub struct ChaosEngConfig {
1771 pub enabled: bool,
1773 pub latency: Option<LatencyInjectionConfig>,
1775 pub fault_injection: Option<FaultConfig>,
1777 pub rate_limit: Option<RateLimitingConfig>,
1779 pub traffic_shaping: Option<NetworkShapingConfig>,
1781 pub scenario: Option<String>,
1783}
1784
1785#[derive(Debug, Clone, Serialize, Deserialize)]
1787#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1788pub struct LatencyInjectionConfig {
1789 pub enabled: bool,
1791 pub fixed_delay_ms: Option<u64>,
1793 pub random_delay_range_ms: Option<(u64, u64)>,
1795 pub jitter_percent: f64,
1797 pub probability: f64,
1799}
1800
1801#[derive(Debug, Clone, Serialize, Deserialize)]
1803#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1804pub struct FaultConfig {
1805 pub enabled: bool,
1807 pub http_errors: Vec<u16>,
1809 pub http_error_probability: f64,
1811 pub connection_errors: bool,
1813 pub connection_error_probability: f64,
1815 pub timeout_errors: bool,
1817 pub timeout_ms: u64,
1819 pub timeout_probability: f64,
1821}
1822
1823#[derive(Debug, Clone, Serialize, Deserialize)]
1825#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1826pub struct RateLimitingConfig {
1827 pub enabled: bool,
1829 pub requests_per_second: u32,
1831 pub burst_size: u32,
1833 pub per_ip: bool,
1835 pub per_endpoint: bool,
1837}
1838
1839#[derive(Debug, Clone, Serialize, Deserialize)]
1841#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1842pub struct NetworkShapingConfig {
1843 pub enabled: bool,
1845 pub bandwidth_limit_bps: u64,
1847 pub packet_loss_percent: f64,
1849 pub max_connections: u32,
1851}
1852
1853pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1855 let content = fs::read_to_string(&path)
1856 .await
1857 .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1858
1859 let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1861 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1862 {
1863 serde_yaml::from_str(&content).map_err(|e| {
1864 let error_msg = e.to_string();
1866 let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
1867
1868 if error_msg.contains("missing field") {
1870 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1871 full_msg.push_str(
1872 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1873 );
1874 full_msg.push_str("\n See config.template.yaml for all available options.");
1875 } else if error_msg.contains("unknown field") {
1876 full_msg.push_str("\n\n💡 Check for typos in field names.");
1877 full_msg.push_str("\n See config.template.yaml for valid field names.");
1878 }
1879
1880 Error::generic(full_msg)
1881 })?
1882 } else {
1883 serde_json::from_str(&content).map_err(|e| {
1884 let error_msg = e.to_string();
1886 let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
1887
1888 if error_msg.contains("missing field") {
1890 full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1891 full_msg.push_str(
1892 "\n Omit fields you don't need - MockForge will use sensible defaults.",
1893 );
1894 full_msg.push_str("\n See config.template.yaml for all available options.");
1895 } else if error_msg.contains("unknown field") {
1896 full_msg.push_str("\n\n💡 Check for typos in field names.");
1897 full_msg.push_str("\n See config.template.yaml for valid field names.");
1898 }
1899
1900 Error::generic(full_msg)
1901 })?
1902 };
1903
1904 Ok(config)
1905}
1906
1907pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1909 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1910 || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1911 {
1912 serde_yaml::to_string(config)
1913 .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1914 } else {
1915 serde_json::to_string_pretty(config)
1916 .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1917 };
1918
1919 fs::write(path, content)
1920 .await
1921 .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1922
1923 Ok(())
1924}
1925
1926pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1928 match load_config(&path).await {
1929 Ok(config) => {
1930 tracing::info!("Loaded configuration from {:?}", path.as_ref());
1931 config
1932 }
1933 Err(e) => {
1934 tracing::warn!(
1935 "Failed to load config from {:?}: {}. Using defaults.",
1936 path.as_ref(),
1937 e
1938 );
1939 ServerConfig::default()
1940 }
1941 }
1942}
1943
1944pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1946 let config = ServerConfig::default();
1947 save_config(path, &config).await?;
1948 Ok(())
1949}
1950
1951pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1953 if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1955 if let Ok(port_num) = port.parse() {
1956 config.http.port = port_num;
1957 }
1958 }
1959
1960 if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1961 config.http.host = host;
1962 }
1963
1964 if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1966 if let Ok(port_num) = port.parse() {
1967 config.websocket.port = port_num;
1968 }
1969 }
1970
1971 if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1973 if let Ok(port_num) = port.parse() {
1974 config.grpc.port = port_num;
1975 }
1976 }
1977
1978 if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1980 if let Ok(port_num) = port.parse() {
1981 config.smtp.port = port_num;
1982 }
1983 }
1984
1985 if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1986 config.smtp.host = host;
1987 }
1988
1989 if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1990 config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1991 }
1992
1993 if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1994 config.smtp.hostname = hostname;
1995 }
1996
1997 if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
1999 if let Ok(port_num) = port.parse() {
2000 config.tcp.port = port_num;
2001 }
2002 }
2003
2004 if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
2005 config.tcp.host = host;
2006 }
2007
2008 if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
2009 config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
2010 }
2011
2012 if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
2014 if let Ok(port_num) = port.parse() {
2015 config.admin.port = port_num;
2016 }
2017 }
2018
2019 if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
2020 config.admin.enabled = true;
2021 }
2022
2023 if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
2025 config.admin.host = host;
2026 }
2027
2028 if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
2029 if !mount_path.trim().is_empty() {
2030 config.admin.mount_path = Some(mount_path);
2031 }
2032 }
2033
2034 if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
2035 let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
2036 config.admin.api_enabled = on;
2037 }
2038
2039 if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
2040 config.admin.prometheus_url = prometheus_url;
2041 }
2042
2043 if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
2045 let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
2046 config.core.latency_enabled = enabled;
2047 }
2048
2049 if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
2050 let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
2051 config.core.failures_enabled = enabled;
2052 }
2053
2054 if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
2055 let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
2056 config.core.overrides_enabled = enabled;
2057 }
2058
2059 if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
2060 let enabled =
2061 traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
2062 config.core.traffic_shaping_enabled = enabled;
2063 }
2064
2065 if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
2067 let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
2068 config.core.traffic_shaping.bandwidth.enabled = enabled;
2069 }
2070
2071 if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
2072 if let Ok(bytes) = max_bytes_per_sec.parse() {
2073 config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
2074 config.core.traffic_shaping.bandwidth.enabled = true;
2075 }
2076 }
2077
2078 if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
2079 if let Ok(bytes) = burst_capacity.parse() {
2080 config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
2081 }
2082 }
2083
2084 if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
2085 let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
2086 config.core.traffic_shaping.burst_loss.enabled = enabled;
2087 }
2088
2089 if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
2090 if let Ok(prob) = burst_probability.parse::<f64>() {
2091 config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
2092 config.core.traffic_shaping.burst_loss.enabled = true;
2093 }
2094 }
2095
2096 if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
2097 if let Ok(ms) = burst_duration.parse() {
2098 config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
2099 }
2100 }
2101
2102 if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
2103 if let Ok(rate) = loss_rate.parse::<f64>() {
2104 config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
2105 }
2106 }
2107
2108 if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
2109 if let Ok(ms) = recovery_time.parse() {
2110 config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
2111 }
2112 }
2113
2114 if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
2116 config.logging.level = level;
2117 }
2118
2119 config
2120}
2121
2122pub fn validate_config(config: &ServerConfig) -> Result<()> {
2124 if config.http.port == 0 {
2126 return Err(Error::generic("HTTP port cannot be 0"));
2127 }
2128 if config.websocket.port == 0 {
2129 return Err(Error::generic("WebSocket port cannot be 0"));
2130 }
2131 if config.grpc.port == 0 {
2132 return Err(Error::generic("gRPC port cannot be 0"));
2133 }
2134 if config.admin.port == 0 {
2135 return Err(Error::generic("Admin port cannot be 0"));
2136 }
2137
2138 let ports = [
2140 ("HTTP", config.http.port),
2141 ("WebSocket", config.websocket.port),
2142 ("gRPC", config.grpc.port),
2143 ("Admin", config.admin.port),
2144 ];
2145
2146 for i in 0..ports.len() {
2147 for j in (i + 1)..ports.len() {
2148 if ports[i].1 == ports[j].1 {
2149 return Err(Error::generic(format!(
2150 "Port conflict: {} and {} both use port {}",
2151 ports[i].0, ports[j].0, ports[i].1
2152 )));
2153 }
2154 }
2155 }
2156
2157 let valid_levels = ["trace", "debug", "info", "warn", "error"];
2159 if !valid_levels.contains(&config.logging.level.as_str()) {
2160 return Err(Error::generic(format!(
2161 "Invalid log level: {}. Valid levels: {}",
2162 config.logging.level,
2163 valid_levels.join(", ")
2164 )));
2165 }
2166
2167 Ok(())
2168}
2169
2170pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
2172 macro_rules! merge_field {
2174 ($field:ident) => {
2175 if let Some(override_val) = profile.$field {
2176 base.$field = override_val;
2177 }
2178 };
2179 }
2180
2181 merge_field!(http);
2182 merge_field!(websocket);
2183 merge_field!(graphql);
2184 merge_field!(grpc);
2185 merge_field!(mqtt);
2186 merge_field!(smtp);
2187 merge_field!(ftp);
2188 merge_field!(kafka);
2189 merge_field!(amqp);
2190 merge_field!(tcp);
2191 merge_field!(admin);
2192 merge_field!(chaining);
2193 merge_field!(core);
2194 merge_field!(logging);
2195 merge_field!(data);
2196 merge_field!(mockai);
2197 merge_field!(observability);
2198 merge_field!(multi_tenant);
2199 merge_field!(routes);
2200 merge_field!(protocols);
2201
2202 base
2203}
2204
2205pub async fn load_config_with_profile<P: AsRef<Path>>(
2207 path: P,
2208 profile_name: Option<&str>,
2209) -> Result<ServerConfig> {
2210 let mut config = load_config_auto(&path).await?;
2212
2213 if let Some(profile) = profile_name {
2215 if let Some(profile_config) = config.profiles.remove(profile) {
2216 tracing::info!("Applying profile: {}", profile);
2217 config = apply_profile(config, profile_config);
2218 } else {
2219 return Err(Error::generic(format!(
2220 "Profile '{}' not found in configuration. Available profiles: {}",
2221 profile,
2222 config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
2223 )));
2224 }
2225 }
2226
2227 config.profiles.clear();
2229
2230 Ok(config)
2231}
2232
2233pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
2235 use rquickjs::{Context, Runtime};
2236
2237 let content = fs::read_to_string(&path)
2238 .await
2239 .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
2240
2241 let runtime = Runtime::new()
2243 .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
2244 let context = Context::full(&runtime)
2245 .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
2246
2247 context.with(|ctx| {
2248 let js_content = if path
2251 .as_ref()
2252 .extension()
2253 .and_then(|s| s.to_str())
2254 .map(|ext| ext == "ts")
2255 .unwrap_or(false)
2256 {
2257 strip_typescript_types(&content)?
2258 } else {
2259 content
2260 };
2261
2262 let result: rquickjs::Value = ctx
2264 .eval(js_content.as_bytes())
2265 .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
2266
2267 let json_str: String = ctx
2269 .json_stringify(result)
2270 .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
2271 .ok_or_else(|| Error::generic("JS config returned undefined"))?
2272 .get()
2273 .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
2274
2275 serde_json::from_str(&json_str).map_err(|e| {
2277 Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
2278 })
2279 })
2280}
2281
2282fn strip_typescript_types(content: &str) -> Result<String> {
2289 use regex::Regex;
2290
2291 let mut result = content.to_string();
2292
2293 let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
2299 .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
2300 result = interface_re.replace_all(&result, "").to_string();
2301
2302 let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
2304 .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
2305 result = type_alias_re.replace_all(&result, "").to_string();
2306
2307 let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
2309 .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
2310 result = type_annotation_re.replace_all(&result, "").to_string();
2311
2312 let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
2314 .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
2315 result = type_import_re.replace_all(&result, "").to_string();
2316
2317 let as_type_re = Regex::new(r"\s+as\s+\w+")
2319 .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
2320 result = as_type_re.replace_all(&result, "").to_string();
2321
2322 Ok(result)
2323}
2324
2325pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
2327 let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
2328
2329 match ext {
2330 "ts" | "js" => load_config_from_js(&path).await,
2331 "yaml" | "yml" | "json" => load_config(&path).await,
2332 _ => Err(Error::generic(format!(
2333 "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
2334 ext
2335 ))),
2336 }
2337}
2338
2339pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
2341 let current_dir = std::env::current_dir()
2342 .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
2343
2344 let config_names = vec![
2345 "mockforge.config.ts",
2346 "mockforge.config.js",
2347 "mockforge.yaml",
2348 "mockforge.yml",
2349 ".mockforge.yaml",
2350 ".mockforge.yml",
2351 ];
2352
2353 for name in &config_names {
2355 let path = current_dir.join(name);
2356 if tokio::fs::metadata(&path).await.is_ok() {
2357 return Ok(path);
2358 }
2359 }
2360
2361 let mut dir = current_dir.clone();
2363 for _ in 0..5 {
2364 if let Some(parent) = dir.parent() {
2365 for name in &config_names {
2366 let path = parent.join(name);
2367 if tokio::fs::metadata(&path).await.is_ok() {
2368 return Ok(path);
2369 }
2370 }
2371 dir = parent.to_path_buf();
2372 } else {
2373 break;
2374 }
2375 }
2376
2377 Err(Error::generic(
2378 "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
2379 ))
2380}
2381
2382#[cfg(test)]
2383mod tests {
2384 use super::*;
2385
2386 #[test]
2387 fn test_default_config() {
2388 let config = ServerConfig::default();
2389 assert_eq!(config.http.port, 3000);
2390 assert_eq!(config.websocket.port, 3001);
2391 assert_eq!(config.grpc.port, 50051);
2392 assert_eq!(config.admin.port, 9080);
2393 }
2394
2395 #[test]
2396 fn test_config_validation() {
2397 let mut config = ServerConfig::default();
2398 assert!(validate_config(&config).is_ok());
2399
2400 config.websocket.port = config.http.port;
2402 assert!(validate_config(&config).is_err());
2403
2404 config.websocket.port = 3001; config.logging.level = "invalid".to_string();
2407 assert!(validate_config(&config).is_err());
2408 }
2409
2410 #[test]
2411 fn test_apply_profile() {
2412 let mut base = ServerConfig::default();
2413 assert_eq!(base.http.port, 3000);
2414
2415 let mut profile = ProfileConfig::default();
2416 profile.http = Some(HttpConfig {
2417 port: 8080,
2418 ..Default::default()
2419 });
2420 profile.logging = Some(LoggingConfig {
2421 level: "debug".to_string(),
2422 ..Default::default()
2423 });
2424
2425 let merged = apply_profile(base, profile);
2426 assert_eq!(merged.http.port, 8080);
2427 assert_eq!(merged.logging.level, "debug");
2428 assert_eq!(merged.websocket.port, 3001); }
2430
2431 #[test]
2432 fn test_strip_typescript_types() {
2433 let ts_code = r#"
2434interface Config {
2435 port: number;
2436 host: string;
2437}
2438
2439const config: Config = {
2440 port: 3000,
2441 host: "localhost"
2442} as Config;
2443"#;
2444
2445 let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
2446 assert!(!stripped.contains("interface"));
2447 assert!(!stripped.contains(": Config"));
2448 assert!(!stripped.contains("as Config"));
2449 }
2450}