mockforge_core/
config.rs

1//! Configuration management for MockForge
2
3use crate::{Config as CoreConfig, Error, RealityLevel, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7use tokio::fs;
8
9/// Authentication configuration for HTTP requests
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(default)]
12pub struct AuthConfig {
13    /// JWT configuration
14    pub jwt: Option<JwtConfig>,
15    /// OAuth2 configuration
16    pub oauth2: Option<OAuth2Config>,
17    /// Basic auth configuration
18    pub basic_auth: Option<BasicAuthConfig>,
19    /// API key configuration
20    pub api_key: Option<ApiKeyConfig>,
21    /// Whether to require authentication for all requests
22    pub require_auth: bool,
23}
24
25/// JWT authentication configuration
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct JwtConfig {
28    /// JWT secret key for HMAC algorithms
29    pub secret: Option<String>,
30    /// RSA public key PEM for RSA algorithms
31    pub rsa_public_key: Option<String>,
32    /// ECDSA public key PEM for ECDSA algorithms
33    pub ecdsa_public_key: Option<String>,
34    /// Expected issuer
35    pub issuer: Option<String>,
36    /// Expected audience
37    pub audience: Option<String>,
38    /// Supported algorithms (defaults to HS256, RS256, ES256)
39    pub algorithms: Vec<String>,
40}
41
42/// OAuth2 configuration
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct OAuth2Config {
45    /// OAuth2 client ID
46    pub client_id: String,
47    /// OAuth2 client secret
48    pub client_secret: String,
49    /// Token introspection URL
50    pub introspection_url: String,
51    /// Authorization server URL
52    pub auth_url: Option<String>,
53    /// Token URL
54    pub token_url: Option<String>,
55    /// Expected token type
56    pub token_type_hint: Option<String>,
57}
58
59/// Basic authentication configuration
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct BasicAuthConfig {
62    /// Username/password pairs
63    pub credentials: HashMap<String, String>,
64}
65
66/// API key configuration
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ApiKeyConfig {
69    /// Expected header name (default: X-API-Key)
70    pub header_name: String,
71    /// Expected query parameter name
72    pub query_name: Option<String>,
73    /// Valid API keys
74    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/// Route configuration for custom HTTP routes
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct RouteConfig {
96    /// Route path (supports path parameters like /users/{id})
97    pub path: String,
98    /// HTTP method
99    pub method: String,
100    /// Request configuration
101    pub request: Option<RouteRequestConfig>,
102    /// Response configuration
103    pub response: RouteResponseConfig,
104    /// Per-route fault injection configuration
105    #[serde(default)]
106    pub fault_injection: Option<RouteFaultInjectionConfig>,
107    /// Per-route latency configuration
108    #[serde(default)]
109    pub latency: Option<RouteLatencyConfig>,
110}
111
112/// Request configuration for routes
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RouteRequestConfig {
115    /// Request validation configuration
116    pub validation: Option<RouteValidationConfig>,
117}
118
119/// Response configuration for routes
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct RouteResponseConfig {
122    /// HTTP status code
123    pub status: u16,
124    /// Response headers
125    #[serde(default)]
126    pub headers: HashMap<String, String>,
127    /// Response body
128    pub body: Option<serde_json::Value>,
129}
130
131/// Validation configuration for routes
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RouteValidationConfig {
134    /// JSON schema for request validation
135    pub schema: serde_json::Value,
136}
137
138/// Per-route fault injection configuration
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct RouteFaultInjectionConfig {
141    /// Enable fault injection for this route
142    pub enabled: bool,
143    /// Probability of injecting a fault (0.0-1.0)
144    pub probability: f64,
145    /// Fault types to inject
146    pub fault_types: Vec<RouteFaultType>,
147}
148
149/// Fault types that can be injected per route
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(tag = "type", rename_all = "snake_case")]
152pub enum RouteFaultType {
153    /// HTTP error with status code
154    HttpError {
155        /// HTTP status code to return
156        status_code: u16,
157        /// Optional error message
158        message: Option<String>,
159    },
160    /// Connection error
161    ConnectionError {
162        /// Optional error message
163        message: Option<String>,
164    },
165    /// Timeout error
166    Timeout {
167        /// Timeout duration in milliseconds
168        duration_ms: u64,
169        /// Optional error message
170        message: Option<String>,
171    },
172    /// Partial response (truncate at percentage)
173    PartialResponse {
174        /// Percentage of response to truncate (0.0-100.0)
175        truncate_percent: f64,
176    },
177    /// Payload corruption
178    PayloadCorruption {
179        /// Type of corruption to apply
180        corruption_type: String,
181    },
182}
183
184/// Per-route latency configuration
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct RouteLatencyConfig {
187    /// Enable latency injection for this route
188    pub enabled: bool,
189    /// Probability of applying latency (0.0-1.0)
190    pub probability: f64,
191    /// Fixed delay in milliseconds
192    pub fixed_delay_ms: Option<u64>,
193    /// Random delay range (min_ms, max_ms)
194    pub random_delay_range_ms: Option<(u64, u64)>,
195    /// Jitter percentage (0.0-100.0)
196    pub jitter_percent: f64,
197    /// Latency distribution type
198    #[serde(default)]
199    pub distribution: LatencyDistribution,
200}
201
202/// Latency distribution type
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
204#[serde(rename_all = "snake_case")]
205pub enum LatencyDistribution {
206    /// Fixed delay
207    Fixed,
208    /// Normal distribution (requires mean and std_dev)
209    Normal {
210        /// Mean delay in milliseconds
211        mean_ms: f64,
212        /// Standard deviation in milliseconds
213        std_dev_ms: f64,
214    },
215    /// Exponential distribution (requires lambda)
216    Exponential {
217        /// Lambda parameter for exponential distribution
218        lambda: f64,
219    },
220    /// Uniform distribution (uses random_delay_range_ms)
221    Uniform,
222}
223
224impl Default for LatencyDistribution {
225    fn default() -> Self {
226        Self::Fixed
227    }
228}
229
230impl Default for RouteFaultInjectionConfig {
231    fn default() -> Self {
232        Self {
233            enabled: false,
234            probability: 0.0,
235            fault_types: Vec::new(),
236        }
237    }
238}
239
240impl Default for RouteLatencyConfig {
241    fn default() -> Self {
242        Self {
243            enabled: false,
244            probability: 1.0,
245            fixed_delay_ms: None,
246            random_delay_range_ms: None,
247            jitter_percent: 0.0,
248            distribution: LatencyDistribution::Fixed,
249        }
250    }
251}
252
253/// Deceptive deploy configuration for production-like mock APIs
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(default)]
256pub struct DeceptiveDeployConfig {
257    /// Enable deceptive deploy mode
258    pub enabled: bool,
259    /// Production-like CORS configuration
260    pub cors: Option<ProductionCorsConfig>,
261    /// Production-like rate limiting
262    pub rate_limit: Option<ProductionRateLimitConfig>,
263    /// Production-like headers to add to all responses
264    #[serde(default)]
265    pub headers: HashMap<String, String>,
266    /// OAuth configuration for production-like auth flows
267    pub oauth: Option<ProductionOAuthConfig>,
268    /// Custom domain for deployment
269    pub custom_domain: Option<String>,
270    /// Auto-start tunnel when deploying
271    pub auto_tunnel: bool,
272}
273
274impl Default for DeceptiveDeployConfig {
275    fn default() -> Self {
276        Self {
277            enabled: false,
278            cors: None,
279            rate_limit: None,
280            headers: HashMap::new(),
281            oauth: None,
282            custom_domain: None,
283            auto_tunnel: false,
284        }
285    }
286}
287
288impl DeceptiveDeployConfig {
289    /// Generate production-like configuration preset
290    pub fn production_preset() -> Self {
291        let mut headers = HashMap::new();
292        headers.insert("X-API-Version".to_string(), "1.0".to_string());
293        headers.insert("X-Request-ID".to_string(), "{{uuid}}".to_string());
294        headers.insert("X-Powered-By".to_string(), "MockForge".to_string());
295
296        Self {
297            enabled: true,
298            cors: Some(ProductionCorsConfig {
299                allowed_origins: vec!["*".to_string()],
300                allowed_methods: vec![
301                    "GET".to_string(),
302                    "POST".to_string(),
303                    "PUT".to_string(),
304                    "DELETE".to_string(),
305                    "PATCH".to_string(),
306                    "OPTIONS".to_string(),
307                ],
308                allowed_headers: vec!["*".to_string()],
309                allow_credentials: true,
310            }),
311            rate_limit: Some(ProductionRateLimitConfig {
312                requests_per_minute: 1000,
313                burst: 2000,
314                per_ip: true,
315            }),
316            headers,
317            oauth: None, // Configured separately
318            custom_domain: None,
319            auto_tunnel: true,
320        }
321    }
322}
323
324/// Production-like CORS configuration
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct ProductionCorsConfig {
327    /// Allowed origins (use "*" for all origins)
328    #[serde(default)]
329    pub allowed_origins: Vec<String>,
330    /// Allowed HTTP methods
331    #[serde(default)]
332    pub allowed_methods: Vec<String>,
333    /// Allowed headers (use "*" for all headers)
334    #[serde(default)]
335    pub allowed_headers: Vec<String>,
336    /// Allow credentials (cookies, authorization headers)
337    pub allow_credentials: bool,
338}
339
340/// Production-like rate limiting configuration
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct ProductionRateLimitConfig {
343    /// Requests per minute allowed
344    pub requests_per_minute: u32,
345    /// Burst capacity (maximum requests in a short burst)
346    pub burst: u32,
347    /// Enable per-IP rate limiting
348    pub per_ip: bool,
349}
350
351/// Production-like OAuth configuration
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ProductionOAuthConfig {
354    /// OAuth2 client ID
355    pub client_id: String,
356    /// OAuth2 client secret
357    pub client_secret: String,
358    /// Token introspection URL
359    pub introspection_url: String,
360    /// Authorization server URL
361    pub auth_url: Option<String>,
362    /// Token URL
363    pub token_url: Option<String>,
364    /// Expected token type hint
365    pub token_type_hint: Option<String>,
366}
367
368impl From<ProductionOAuthConfig> for OAuth2Config {
369    /// Convert ProductionOAuthConfig to OAuth2Config for use in auth middleware
370    fn from(prod: ProductionOAuthConfig) -> Self {
371        OAuth2Config {
372            client_id: prod.client_id,
373            client_secret: prod.client_secret,
374            introspection_url: prod.introspection_url,
375            auth_url: prod.auth_url,
376            token_url: prod.token_url,
377            token_type_hint: prod.token_type_hint,
378        }
379    }
380}
381
382/// Protocol enable/disable configuration
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct ProtocolConfig {
385    /// Enable this protocol
386    pub enabled: bool,
387}
388
389/// Protocols configuration
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ProtocolsConfig {
392    /// HTTP protocol configuration
393    pub http: ProtocolConfig,
394    /// GraphQL protocol configuration
395    pub graphql: ProtocolConfig,
396    /// gRPC protocol configuration
397    pub grpc: ProtocolConfig,
398    /// WebSocket protocol configuration
399    pub websocket: ProtocolConfig,
400    /// SMTP protocol configuration
401    pub smtp: ProtocolConfig,
402    /// MQTT protocol configuration
403    pub mqtt: ProtocolConfig,
404    /// FTP protocol configuration
405    pub ftp: ProtocolConfig,
406    /// Kafka protocol configuration
407    pub kafka: ProtocolConfig,
408    /// RabbitMQ protocol configuration
409    pub rabbitmq: ProtocolConfig,
410    /// AMQP protocol configuration
411    pub amqp: ProtocolConfig,
412    /// TCP protocol configuration
413    pub tcp: ProtocolConfig,
414}
415
416impl Default for ProtocolsConfig {
417    fn default() -> Self {
418        Self {
419            http: ProtocolConfig { enabled: true },
420            graphql: ProtocolConfig { enabled: true },
421            grpc: ProtocolConfig { enabled: true },
422            websocket: ProtocolConfig { enabled: true },
423            smtp: ProtocolConfig { enabled: false },
424            mqtt: ProtocolConfig { enabled: true },
425            ftp: ProtocolConfig { enabled: false },
426            kafka: ProtocolConfig { enabled: false },
427            rabbitmq: ProtocolConfig { enabled: false },
428            amqp: ProtocolConfig { enabled: false },
429            tcp: ProtocolConfig { enabled: false },
430        }
431    }
432}
433
434/// Reality slider configuration for YAML config files
435///
436/// This is a simplified configuration that stores just the level.
437/// The full RealityConfig with all subsystem settings is generated
438/// automatically from the level via the RealityEngine.
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(default)]
441pub struct RealitySliderConfig {
442    /// Reality level (1-5)
443    pub level: RealityLevel,
444    /// Whether to enable reality slider (if false, uses individual subsystem configs)
445    pub enabled: bool,
446}
447
448impl Default for RealitySliderConfig {
449    fn default() -> Self {
450        Self {
451            level: RealityLevel::ModerateRealism,
452            enabled: true,
453        }
454    }
455}
456
457/// Server configuration
458#[derive(Debug, Clone, Serialize, Deserialize, Default)]
459#[serde(default)]
460pub struct ServerConfig {
461    /// HTTP server configuration
462    pub http: HttpConfig,
463    /// WebSocket server configuration
464    pub websocket: WebSocketConfig,
465    /// GraphQL server configuration
466    pub graphql: GraphQLConfig,
467    /// gRPC server configuration
468    pub grpc: GrpcConfig,
469    /// MQTT server configuration
470    pub mqtt: MqttConfig,
471    /// SMTP server configuration
472    pub smtp: SmtpConfig,
473    /// FTP server configuration
474    pub ftp: FtpConfig,
475    /// Kafka server configuration
476    pub kafka: KafkaConfig,
477    /// AMQP server configuration
478    pub amqp: AmqpConfig,
479    /// TCP server configuration
480    pub tcp: TcpConfig,
481    /// Admin UI configuration
482    pub admin: AdminConfig,
483    /// Request chaining configuration
484    pub chaining: ChainingConfig,
485    /// Core MockForge configuration
486    pub core: CoreConfig,
487    /// Logging configuration
488    pub logging: LoggingConfig,
489    /// Data generation configuration
490    pub data: DataConfig,
491    /// MockAI (Behavioral Mock Intelligence) configuration
492    #[serde(default)]
493    pub mockai: MockAIConfig,
494    /// Observability configuration (metrics, tracing)
495    pub observability: ObservabilityConfig,
496    /// Multi-tenant workspace configuration
497    pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
498    /// Custom routes configuration
499    #[serde(default)]
500    pub routes: Vec<RouteConfig>,
501    /// Protocol enable/disable configuration
502    #[serde(default)]
503    pub protocols: ProtocolsConfig,
504    /// Named configuration profiles (dev, ci, demo, etc.)
505    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
506    pub profiles: HashMap<String, ProfileConfig>,
507    /// Deceptive deploy configuration for production-like mock APIs
508    #[serde(default)]
509    pub deceptive_deploy: DeceptiveDeployConfig,
510    /// Reality slider configuration for unified realism control
511    #[serde(default)]
512    pub reality: RealitySliderConfig,
513    /// Reality Continuum configuration for blending mock and real data sources
514    #[serde(default)]
515    pub reality_continuum: crate::reality_continuum::ContinuumConfig,
516}
517
518/// Profile configuration - a partial ServerConfig that overrides base settings
519#[derive(Debug, Clone, Serialize, Deserialize, Default)]
520#[serde(default)]
521pub struct ProfileConfig {
522    /// HTTP server configuration overrides
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub http: Option<HttpConfig>,
525    /// WebSocket server configuration overrides
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub websocket: Option<WebSocketConfig>,
528    /// GraphQL server configuration overrides
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub graphql: Option<GraphQLConfig>,
531    /// gRPC server configuration overrides
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub grpc: Option<GrpcConfig>,
534    /// MQTT server configuration overrides
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub mqtt: Option<MqttConfig>,
537    /// SMTP server configuration overrides
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub smtp: Option<SmtpConfig>,
540    /// FTP server configuration overrides
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub ftp: Option<FtpConfig>,
543    /// Kafka server configuration overrides
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub kafka: Option<KafkaConfig>,
546    /// AMQP server configuration overrides
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub amqp: Option<AmqpConfig>,
549    /// TCP server configuration overrides
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub tcp: Option<TcpConfig>,
552    /// Admin UI configuration overrides
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub admin: Option<AdminConfig>,
555    /// Request chaining configuration overrides
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub chaining: Option<ChainingConfig>,
558    /// Core MockForge configuration overrides
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub core: Option<CoreConfig>,
561    /// Logging configuration overrides
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub logging: Option<LoggingConfig>,
564    /// Data generation configuration overrides
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub data: Option<DataConfig>,
567    /// MockAI configuration overrides
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub mockai: Option<MockAIConfig>,
570    /// Observability configuration overrides
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub observability: Option<ObservabilityConfig>,
573    /// Multi-tenant workspace configuration overrides
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub multi_tenant: Option<crate::multi_tenant::MultiTenantConfig>,
576    /// Custom routes configuration overrides
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub routes: Option<Vec<RouteConfig>>,
579    /// Protocol enable/disable configuration overrides
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub protocols: Option<ProtocolsConfig>,
582    /// Deceptive deploy configuration overrides
583    #[serde(skip_serializing_if = "Option::is_none")]
584    pub deceptive_deploy: Option<DeceptiveDeployConfig>,
585    /// Reality slider configuration overrides
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub reality: Option<RealitySliderConfig>,
588    /// Reality Continuum configuration overrides
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub reality_continuum: Option<crate::reality_continuum::ContinuumConfig>,
591}
592
593// Default is derived for ServerConfig
594
595/// HTTP validation configuration
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct HttpValidationConfig {
598    /// Request validation mode: off, warn, enforce
599    pub mode: String,
600}
601
602/// HTTP CORS configuration
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct HttpCorsConfig {
605    /// Enable CORS
606    pub enabled: bool,
607    /// Allowed origins
608    #[serde(default)]
609    pub allowed_origins: Vec<String>,
610    /// Allowed methods
611    #[serde(default)]
612    pub allowed_methods: Vec<String>,
613    /// Allowed headers
614    #[serde(default)]
615    pub allowed_headers: Vec<String>,
616}
617
618/// HTTP server configuration
619#[derive(Debug, Clone, Serialize, Deserialize)]
620#[serde(default)]
621pub struct HttpConfig {
622    /// Enable HTTP server
623    pub enabled: bool,
624    /// Server port
625    pub port: u16,
626    /// Host address
627    pub host: String,
628    /// Path to OpenAPI spec file for HTTP server
629    pub openapi_spec: Option<String>,
630    /// CORS configuration
631    pub cors: Option<HttpCorsConfig>,
632    /// Request timeout in seconds
633    pub request_timeout_secs: u64,
634    /// Request validation configuration
635    pub validation: Option<HttpValidationConfig>,
636    /// Aggregate validation errors into JSON array
637    pub aggregate_validation_errors: bool,
638    /// Validate responses (warn-only logging)
639    pub validate_responses: bool,
640    /// Expand templating tokens in responses/examples
641    pub response_template_expand: bool,
642    /// Validation error HTTP status (e.g., 400 or 422)
643    pub validation_status: Option<u16>,
644    /// Per-route overrides: key "METHOD path" => mode (off/warn/enforce)
645    pub validation_overrides: std::collections::HashMap<String, String>,
646    /// When embedding Admin UI under HTTP, skip validation for the mounted prefix
647    pub skip_admin_validation: bool,
648    /// Authentication configuration
649    pub auth: Option<AuthConfig>,
650    /// TLS/HTTPS configuration
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub tls: Option<HttpTlsConfig>,
653}
654
655impl Default for HttpConfig {
656    fn default() -> Self {
657        Self {
658            enabled: true,
659            port: 3000,
660            host: "0.0.0.0".to_string(),
661            openapi_spec: None,
662            cors: Some(HttpCorsConfig {
663                enabled: true,
664                allowed_origins: vec!["*".to_string()],
665                allowed_methods: vec![
666                    "GET".to_string(),
667                    "POST".to_string(),
668                    "PUT".to_string(),
669                    "DELETE".to_string(),
670                    "PATCH".to_string(),
671                    "OPTIONS".to_string(),
672                ],
673                allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
674            }),
675            request_timeout_secs: 30,
676            validation: Some(HttpValidationConfig {
677                mode: "enforce".to_string(),
678            }),
679            aggregate_validation_errors: true,
680            validate_responses: false,
681            response_template_expand: false,
682            validation_status: None,
683            validation_overrides: std::collections::HashMap::new(),
684            skip_admin_validation: true,
685            auth: None,
686            tls: None,
687        }
688    }
689}
690
691/// HTTP TLS/HTTPS configuration
692#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct HttpTlsConfig {
694    /// Enable TLS/HTTPS
695    pub enabled: bool,
696    /// Path to TLS certificate file (PEM format)
697    pub cert_file: String,
698    /// Path to TLS private key file (PEM format)
699    pub key_file: String,
700    /// Path to CA certificate file for mutual TLS (optional)
701    #[serde(skip_serializing_if = "Option::is_none")]
702    pub ca_file: Option<String>,
703    /// Minimum TLS version (default: "1.2")
704    #[serde(default = "default_tls_min_version")]
705    pub min_version: String,
706    /// Cipher suites to use (default: safe defaults)
707    #[serde(default, skip_serializing_if = "Vec::is_empty")]
708    pub cipher_suites: Vec<String>,
709    /// Require client certificate (mutual TLS)
710    #[serde(default)]
711    pub require_client_cert: bool,
712}
713
714fn default_tls_min_version() -> String {
715    "1.2".to_string()
716}
717
718impl Default for HttpTlsConfig {
719    fn default() -> Self {
720        Self {
721            enabled: true,
722            cert_file: String::new(),
723            key_file: String::new(),
724            ca_file: None,
725            min_version: "1.2".to_string(),
726            cipher_suites: Vec::new(),
727            require_client_cert: false,
728        }
729    }
730}
731
732/// WebSocket server configuration
733#[derive(Debug, Clone, Serialize, Deserialize)]
734#[serde(default)]
735pub struct WebSocketConfig {
736    /// Enable WebSocket server
737    pub enabled: bool,
738    /// Server port
739    pub port: u16,
740    /// Host address
741    pub host: String,
742    /// Replay file path
743    pub replay_file: Option<String>,
744    /// Connection timeout in seconds
745    pub connection_timeout_secs: u64,
746}
747
748impl Default for WebSocketConfig {
749    fn default() -> Self {
750        Self {
751            enabled: true,
752            port: 3001,
753            host: "0.0.0.0".to_string(),
754            replay_file: None,
755            connection_timeout_secs: 300,
756        }
757    }
758}
759
760/// gRPC server configuration
761#[derive(Debug, Clone, Serialize, Deserialize)]
762#[serde(default)]
763pub struct GrpcConfig {
764    /// Enable gRPC server
765    pub enabled: bool,
766    /// Server port
767    pub port: u16,
768    /// Host address
769    pub host: String,
770    /// Proto files directory
771    pub proto_dir: Option<String>,
772    /// TLS configuration
773    pub tls: Option<TlsConfig>,
774}
775
776impl Default for GrpcConfig {
777    fn default() -> Self {
778        Self {
779            enabled: true,
780            port: 50051,
781            host: "0.0.0.0".to_string(),
782            proto_dir: None,
783            tls: None,
784        }
785    }
786}
787
788/// GraphQL server configuration
789#[derive(Debug, Clone, Serialize, Deserialize)]
790#[serde(default)]
791pub struct GraphQLConfig {
792    /// Enable GraphQL server
793    pub enabled: bool,
794    /// Server port
795    pub port: u16,
796    /// Host address
797    pub host: String,
798    /// GraphQL schema file path (.graphql or .gql)
799    pub schema_path: Option<String>,
800    /// Handlers directory for custom resolvers
801    pub handlers_dir: Option<String>,
802    /// Enable GraphQL Playground UI
803    pub playground_enabled: bool,
804    /// Upstream GraphQL server URL for passthrough
805    pub upstream_url: Option<String>,
806    /// Enable introspection queries
807    pub introspection_enabled: bool,
808}
809
810impl Default for GraphQLConfig {
811    fn default() -> Self {
812        Self {
813            enabled: true,
814            port: 4000,
815            host: "0.0.0.0".to_string(),
816            schema_path: None,
817            handlers_dir: None,
818            playground_enabled: true,
819            upstream_url: None,
820            introspection_enabled: true,
821        }
822    }
823}
824
825/// TLS configuration for gRPC
826#[derive(Debug, Clone, Serialize, Deserialize)]
827pub struct TlsConfig {
828    /// Certificate file path
829    pub cert_path: String,
830    /// Private key file path
831    pub key_path: String,
832}
833
834/// MQTT server configuration
835#[derive(Debug, Clone, Serialize, Deserialize)]
836#[serde(default)]
837pub struct MqttConfig {
838    /// Enable MQTT server
839    pub enabled: bool,
840    /// Server port
841    pub port: u16,
842    /// Host address
843    pub host: String,
844    /// Maximum connections
845    pub max_connections: usize,
846    /// Maximum packet size
847    pub max_packet_size: usize,
848    /// Keep-alive timeout in seconds
849    pub keep_alive_secs: u16,
850    /// Directory containing fixture files
851    pub fixtures_dir: Option<std::path::PathBuf>,
852    /// Enable retained messages
853    pub enable_retained_messages: bool,
854    /// Maximum retained messages
855    pub max_retained_messages: usize,
856}
857
858impl Default for MqttConfig {
859    fn default() -> Self {
860        Self {
861            enabled: false,
862            port: 1883,
863            host: "0.0.0.0".to_string(),
864            max_connections: 1000,
865            max_packet_size: 268435456, // 256 MB
866            keep_alive_secs: 60,
867            fixtures_dir: None,
868            enable_retained_messages: true,
869            max_retained_messages: 10000,
870        }
871    }
872}
873
874/// SMTP server configuration
875#[derive(Debug, Clone, Serialize, Deserialize)]
876#[serde(default)]
877pub struct SmtpConfig {
878    /// Enable SMTP server
879    pub enabled: bool,
880    /// Server port
881    pub port: u16,
882    /// Host address
883    pub host: String,
884    /// Server hostname for SMTP greeting
885    pub hostname: String,
886    /// Directory containing fixture files
887    pub fixtures_dir: Option<std::path::PathBuf>,
888    /// Connection timeout in seconds
889    pub timeout_secs: u64,
890    /// Maximum connections
891    pub max_connections: usize,
892    /// Enable mailbox storage
893    pub enable_mailbox: bool,
894    /// Maximum mailbox size
895    pub max_mailbox_messages: usize,
896    /// Enable STARTTLS support
897    pub enable_starttls: bool,
898    /// Path to TLS certificate file
899    pub tls_cert_path: Option<std::path::PathBuf>,
900    /// Path to TLS private key file
901    pub tls_key_path: Option<std::path::PathBuf>,
902}
903
904impl Default for SmtpConfig {
905    fn default() -> Self {
906        Self {
907            enabled: false,
908            port: 1025,
909            host: "0.0.0.0".to_string(),
910            hostname: "mockforge-smtp".to_string(),
911            fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
912            timeout_secs: 300,
913            max_connections: 10,
914            enable_mailbox: true,
915            max_mailbox_messages: 1000,
916            enable_starttls: false,
917            tls_cert_path: None,
918            tls_key_path: None,
919        }
920    }
921}
922
923/// FTP server configuration
924#[derive(Debug, Clone, Serialize, Deserialize)]
925#[serde(default)]
926pub struct FtpConfig {
927    /// Enable FTP server
928    pub enabled: bool,
929    /// Server port
930    pub port: u16,
931    /// Host address
932    pub host: String,
933    /// Passive mode port range
934    pub passive_ports: (u16, u16),
935    /// Maximum connections
936    pub max_connections: usize,
937    /// Connection timeout in seconds
938    pub timeout_secs: u64,
939    /// Allow anonymous access
940    pub allow_anonymous: bool,
941    /// Fixtures directory
942    pub fixtures_dir: Option<std::path::PathBuf>,
943    /// Virtual root directory
944    pub virtual_root: std::path::PathBuf,
945}
946
947impl Default for FtpConfig {
948    fn default() -> Self {
949        Self {
950            enabled: false,
951            port: 2121,
952            host: "0.0.0.0".to_string(),
953            passive_ports: (50000, 51000),
954            max_connections: 100,
955            timeout_secs: 300,
956            allow_anonymous: true,
957            fixtures_dir: None,
958            virtual_root: std::path::PathBuf::from("/mockforge"),
959        }
960    }
961}
962
963/// Kafka server configuration
964#[derive(Debug, Clone, Serialize, Deserialize)]
965#[serde(default)]
966pub struct KafkaConfig {
967    /// Enable Kafka server
968    pub enabled: bool,
969    /// Server port
970    pub port: u16,
971    /// Host address
972    pub host: String,
973    /// Broker ID
974    pub broker_id: i32,
975    /// Maximum connections
976    pub max_connections: usize,
977    /// Log retention time in milliseconds
978    pub log_retention_ms: i64,
979    /// Log segment size in bytes
980    pub log_segment_bytes: i64,
981    /// Fixtures directory
982    pub fixtures_dir: Option<std::path::PathBuf>,
983    /// Auto-create topics
984    pub auto_create_topics: bool,
985    /// Default number of partitions for new topics
986    pub default_partitions: i32,
987    /// Default replication factor for new topics
988    pub default_replication_factor: i16,
989}
990
991impl Default for KafkaConfig {
992    fn default() -> Self {
993        Self {
994            enabled: false,
995            port: 9092, // Standard Kafka port
996            host: "0.0.0.0".to_string(),
997            broker_id: 1,
998            max_connections: 1000,
999            log_retention_ms: 604800000,   // 7 days
1000            log_segment_bytes: 1073741824, // 1 GB
1001            fixtures_dir: None,
1002            auto_create_topics: true,
1003            default_partitions: 3,
1004            default_replication_factor: 1,
1005        }
1006    }
1007}
1008
1009/// AMQP server configuration
1010#[derive(Debug, Clone, Serialize, Deserialize)]
1011#[serde(default)]
1012pub struct AmqpConfig {
1013    /// Enable AMQP server
1014    pub enabled: bool,
1015    /// Server port
1016    pub port: u16,
1017    /// Host address
1018    pub host: String,
1019    /// Maximum connections
1020    pub max_connections: usize,
1021    /// Maximum channels per connection
1022    pub max_channels_per_connection: u16,
1023    /// Frame max size
1024    pub frame_max: u32,
1025    /// Heartbeat interval in seconds
1026    pub heartbeat_interval: u16,
1027    /// Fixtures directory
1028    pub fixtures_dir: Option<std::path::PathBuf>,
1029    /// Virtual hosts
1030    pub virtual_hosts: Vec<String>,
1031}
1032
1033impl Default for AmqpConfig {
1034    fn default() -> Self {
1035        Self {
1036            enabled: false,
1037            port: 5672, // Standard AMQP port
1038            host: "0.0.0.0".to_string(),
1039            max_connections: 1000,
1040            max_channels_per_connection: 100,
1041            frame_max: 131072, // 128 KB
1042            heartbeat_interval: 60,
1043            fixtures_dir: None,
1044            virtual_hosts: vec!["/".to_string()],
1045        }
1046    }
1047}
1048
1049/// TCP server configuration
1050#[derive(Debug, Clone, Serialize, Deserialize)]
1051#[serde(default)]
1052pub struct TcpConfig {
1053    /// Enable TCP server
1054    pub enabled: bool,
1055    /// Server port
1056    pub port: u16,
1057    /// Host address
1058    pub host: String,
1059    /// Maximum connections
1060    pub max_connections: usize,
1061    /// Connection timeout in seconds
1062    pub timeout_secs: u64,
1063    /// Directory containing fixture files
1064    pub fixtures_dir: Option<std::path::PathBuf>,
1065    /// Enable echo mode (echo received data back)
1066    pub echo_mode: bool,
1067    /// Enable TLS support
1068    pub enable_tls: bool,
1069    /// Path to TLS certificate file
1070    pub tls_cert_path: Option<std::path::PathBuf>,
1071    /// Path to TLS private key file
1072    pub tls_key_path: Option<std::path::PathBuf>,
1073}
1074
1075impl Default for TcpConfig {
1076    fn default() -> Self {
1077        Self {
1078            enabled: false,
1079            port: 9999,
1080            host: "0.0.0.0".to_string(),
1081            max_connections: 1000,
1082            timeout_secs: 300,
1083            fixtures_dir: Some(std::path::PathBuf::from("./fixtures/tcp")),
1084            echo_mode: true,
1085            enable_tls: false,
1086            tls_cert_path: None,
1087            tls_key_path: None,
1088        }
1089    }
1090}
1091
1092/// Admin UI configuration
1093#[derive(Debug, Clone, Serialize, Deserialize)]
1094#[serde(default)]
1095pub struct AdminConfig {
1096    /// Enable admin UI
1097    pub enabled: bool,
1098    /// Admin UI port
1099    pub port: u16,
1100    /// Host address
1101    pub host: String,
1102    /// Authentication required
1103    pub auth_required: bool,
1104    /// Admin username (if auth required)
1105    pub username: Option<String>,
1106    /// Admin password (if auth required)
1107    pub password: Option<String>,
1108    /// Optional mount path to embed Admin UI under HTTP server (e.g., "/admin")
1109    pub mount_path: Option<String>,
1110    /// Enable Admin API endpoints (under `__mockforge`)
1111    pub api_enabled: bool,
1112    /// Prometheus server URL for analytics queries
1113    pub prometheus_url: String,
1114}
1115
1116impl Default for AdminConfig {
1117    fn default() -> Self {
1118        // Default to 0.0.0.0 if running in Docker (detected via common Docker env vars)
1119        // This makes Admin UI accessible from outside the container by default
1120        let default_host = if std::env::var("DOCKER_CONTAINER").is_ok()
1121            || std::env::var("container").is_ok()
1122            || std::path::Path::new("/.dockerenv").exists()
1123        {
1124            "0.0.0.0".to_string()
1125        } else {
1126            "127.0.0.1".to_string()
1127        };
1128
1129        Self {
1130            enabled: false,
1131            port: 9080,
1132            host: default_host,
1133            auth_required: false,
1134            username: None,
1135            password: None,
1136            mount_path: None,
1137            api_enabled: true,
1138            prometheus_url: "http://localhost:9090".to_string(),
1139        }
1140    }
1141}
1142
1143/// Logging configuration
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1145#[serde(default)]
1146pub struct LoggingConfig {
1147    /// Log level
1148    pub level: String,
1149    /// Enable JSON logging
1150    pub json_format: bool,
1151    /// Log file path (optional)
1152    pub file_path: Option<String>,
1153    /// Maximum log file size in MB
1154    pub max_file_size_mb: u64,
1155    /// Maximum number of log files to keep
1156    pub max_files: u32,
1157}
1158
1159impl Default for LoggingConfig {
1160    fn default() -> Self {
1161        Self {
1162            level: "info".to_string(),
1163            json_format: false,
1164            file_path: None,
1165            max_file_size_mb: 10,
1166            max_files: 5,
1167        }
1168    }
1169}
1170
1171/// Request chaining configuration for multi-step request workflows
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1173#[serde(default, rename_all = "camelCase")]
1174pub struct ChainingConfig {
1175    /// Enable request chaining
1176    pub enabled: bool,
1177    /// Maximum chain length to prevent infinite loops
1178    pub max_chain_length: usize,
1179    /// Global timeout for chain execution in seconds
1180    pub global_timeout_secs: u64,
1181    /// Enable parallel execution when dependencies allow
1182    pub enable_parallel_execution: bool,
1183}
1184
1185impl Default for ChainingConfig {
1186    fn default() -> Self {
1187        Self {
1188            enabled: false,
1189            max_chain_length: 20,
1190            global_timeout_secs: 300,
1191            enable_parallel_execution: false,
1192        }
1193    }
1194}
1195
1196/// Data generation configuration
1197#[derive(Debug, Clone, Serialize, Deserialize)]
1198#[serde(default)]
1199pub struct DataConfig {
1200    /// Default number of rows to generate
1201    pub default_rows: usize,
1202    /// Default output format
1203    pub default_format: String,
1204    /// Faker locale
1205    pub locale: String,
1206    /// Custom faker templates
1207    pub templates: HashMap<String, String>,
1208    /// RAG configuration
1209    pub rag: RagConfig,
1210    /// Active persona profile domain (e.g., "finance", "ecommerce", "healthcare")
1211    #[serde(skip_serializing_if = "Option::is_none")]
1212    pub persona_domain: Option<String>,
1213    /// Enable persona-based consistency
1214    #[serde(default = "default_false")]
1215    pub persona_consistency_enabled: bool,
1216    /// Persona registry configuration
1217    #[serde(skip_serializing_if = "Option::is_none")]
1218    pub persona_registry: Option<PersonaRegistryConfig>,
1219}
1220
1221impl Default for DataConfig {
1222    fn default() -> Self {
1223        Self {
1224            default_rows: 100,
1225            default_format: "json".to_string(),
1226            locale: "en".to_string(),
1227            templates: HashMap::new(),
1228            rag: RagConfig::default(),
1229            persona_domain: None,
1230            persona_consistency_enabled: false,
1231            persona_registry: None,
1232        }
1233    }
1234}
1235
1236/// RAG configuration
1237#[derive(Debug, Clone, Serialize, Deserialize)]
1238#[serde(default)]
1239pub struct RagConfig {
1240    /// Enable RAG by default
1241    pub enabled: bool,
1242    /// LLM provider (openai, anthropic, ollama, openai_compatible)
1243    #[serde(default)]
1244    pub provider: String,
1245    /// API endpoint for LLM
1246    pub api_endpoint: Option<String>,
1247    /// API key for LLM
1248    pub api_key: Option<String>,
1249    /// Model name
1250    pub model: Option<String>,
1251    /// Maximum tokens for generation
1252    #[serde(default = "default_max_tokens")]
1253    pub max_tokens: usize,
1254    /// Temperature for generation (0.0 to 2.0)
1255    #[serde(default = "default_temperature")]
1256    pub temperature: f64,
1257    /// Context window size
1258    pub context_window: usize,
1259    /// Enable caching
1260    #[serde(default = "default_true")]
1261    pub caching: bool,
1262    /// Cache TTL in seconds
1263    #[serde(default = "default_cache_ttl")]
1264    pub cache_ttl_secs: u64,
1265    /// Request timeout in seconds
1266    #[serde(default = "default_timeout")]
1267    pub timeout_secs: u64,
1268    /// Maximum retries for failed requests
1269    #[serde(default = "default_max_retries")]
1270    pub max_retries: usize,
1271}
1272
1273fn default_max_tokens() -> usize {
1274    1024
1275}
1276
1277fn default_temperature() -> f64 {
1278    0.7
1279}
1280
1281fn default_true() -> bool {
1282    true
1283}
1284
1285fn default_cache_ttl() -> u64 {
1286    3600
1287}
1288
1289fn default_timeout() -> u64 {
1290    30
1291}
1292
1293fn default_max_retries() -> usize {
1294    3
1295}
1296
1297fn default_false() -> bool {
1298    false
1299}
1300
1301impl Default for RagConfig {
1302    fn default() -> Self {
1303        Self {
1304            enabled: false,
1305            provider: "openai".to_string(),
1306            api_endpoint: None,
1307            api_key: None,
1308            model: Some("gpt-3.5-turbo".to_string()),
1309            max_tokens: default_max_tokens(),
1310            temperature: default_temperature(),
1311            context_window: 4000,
1312            caching: default_true(),
1313            cache_ttl_secs: default_cache_ttl(),
1314            timeout_secs: default_timeout(),
1315            max_retries: default_max_retries(),
1316        }
1317    }
1318}
1319
1320/// Persona registry configuration
1321#[derive(Debug, Clone, Serialize, Deserialize)]
1322#[serde(default)]
1323pub struct PersonaRegistryConfig {
1324    /// Enable persistence (save personas to disk)
1325    #[serde(default = "default_false")]
1326    pub persistent: bool,
1327    /// Storage path for persistent personas
1328    #[serde(skip_serializing_if = "Option::is_none")]
1329    pub storage_path: Option<String>,
1330    /// Default traits for new personas
1331    #[serde(default)]
1332    pub default_traits: HashMap<String, String>,
1333}
1334
1335impl Default for PersonaRegistryConfig {
1336    fn default() -> Self {
1337        Self {
1338            persistent: false,
1339            storage_path: None,
1340            default_traits: HashMap::new(),
1341        }
1342    }
1343}
1344
1345/// MockAI (Behavioral Mock Intelligence) configuration
1346#[derive(Debug, Clone, Serialize, Deserialize)]
1347#[serde(default)]
1348pub struct MockAIConfig {
1349    /// Enable MockAI features
1350    pub enabled: bool,
1351    /// Intelligent behavior configuration
1352    pub intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig,
1353    /// Auto-learn from examples
1354    pub auto_learn: bool,
1355    /// Enable mutation detection
1356    pub mutation_detection: bool,
1357    /// Enable AI-driven validation errors
1358    pub ai_validation_errors: bool,
1359    /// Enable context-aware pagination
1360    pub intelligent_pagination: bool,
1361    /// Endpoints to enable MockAI for (empty = all endpoints)
1362    #[serde(default)]
1363    pub enabled_endpoints: Vec<String>,
1364}
1365
1366impl Default for MockAIConfig {
1367    fn default() -> Self {
1368        Self {
1369            enabled: false,
1370            intelligent_behavior: crate::intelligent_behavior::IntelligentBehaviorConfig::default(),
1371            auto_learn: true,
1372            mutation_detection: true,
1373            ai_validation_errors: true,
1374            intelligent_pagination: true,
1375            enabled_endpoints: Vec::new(),
1376        }
1377    }
1378}
1379
1380/// Observability configuration for metrics and distributed tracing
1381#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1382#[serde(default)]
1383pub struct ObservabilityConfig {
1384    /// Prometheus metrics configuration
1385    pub prometheus: PrometheusConfig,
1386    /// OpenTelemetry distributed tracing configuration
1387    pub opentelemetry: Option<OpenTelemetryConfig>,
1388    /// API Flight Recorder configuration
1389    pub recorder: Option<RecorderConfig>,
1390    /// Chaos engineering configuration
1391    pub chaos: Option<ChaosEngConfig>,
1392}
1393
1394/// Prometheus metrics configuration
1395#[derive(Debug, Clone, Serialize, Deserialize)]
1396#[serde(default)]
1397pub struct PrometheusConfig {
1398    /// Enable Prometheus metrics endpoint
1399    pub enabled: bool,
1400    /// Port for metrics endpoint
1401    pub port: u16,
1402    /// Host for metrics endpoint
1403    pub host: String,
1404    /// Path for metrics endpoint
1405    pub path: String,
1406}
1407
1408impl Default for PrometheusConfig {
1409    fn default() -> Self {
1410        Self {
1411            enabled: true,
1412            port: 9090,
1413            host: "0.0.0.0".to_string(),
1414            path: "/metrics".to_string(),
1415        }
1416    }
1417}
1418
1419/// OpenTelemetry distributed tracing configuration
1420#[derive(Debug, Clone, Serialize, Deserialize)]
1421#[serde(default)]
1422pub struct OpenTelemetryConfig {
1423    /// Enable OpenTelemetry tracing
1424    pub enabled: bool,
1425    /// Service name for traces
1426    pub service_name: String,
1427    /// Deployment environment (development, staging, production)
1428    pub environment: String,
1429    /// Jaeger endpoint for trace export
1430    pub jaeger_endpoint: String,
1431    /// OTLP endpoint (alternative to Jaeger)
1432    pub otlp_endpoint: Option<String>,
1433    /// Protocol: grpc or http
1434    pub protocol: String,
1435    /// Sampling rate (0.0 to 1.0)
1436    pub sampling_rate: f64,
1437}
1438
1439impl Default for OpenTelemetryConfig {
1440    fn default() -> Self {
1441        Self {
1442            enabled: false,
1443            service_name: "mockforge".to_string(),
1444            environment: "development".to_string(),
1445            jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
1446            otlp_endpoint: Some("http://localhost:4317".to_string()),
1447            protocol: "grpc".to_string(),
1448            sampling_rate: 1.0,
1449        }
1450    }
1451}
1452
1453/// API Flight Recorder configuration
1454#[derive(Debug, Clone, Serialize, Deserialize)]
1455#[serde(default)]
1456pub struct RecorderConfig {
1457    /// Enable recording
1458    pub enabled: bool,
1459    /// Database file path
1460    pub database_path: String,
1461    /// Enable management API
1462    pub api_enabled: bool,
1463    /// Management API port (if different from main port)
1464    pub api_port: Option<u16>,
1465    /// Maximum number of requests to store (0 for unlimited)
1466    pub max_requests: i64,
1467    /// Auto-delete requests older than N days (0 to disable)
1468    pub retention_days: i64,
1469    /// Record HTTP requests
1470    pub record_http: bool,
1471    /// Record gRPC requests
1472    pub record_grpc: bool,
1473    /// Record WebSocket messages
1474    pub record_websocket: bool,
1475    /// Record GraphQL requests
1476    pub record_graphql: bool,
1477    /// Record proxied requests (requests that are forwarded to real backends)
1478    /// When enabled, proxied requests/responses will be recorded with metadata indicating proxy source
1479    #[serde(default = "default_true")]
1480    pub record_proxy: bool,
1481}
1482
1483impl Default for RecorderConfig {
1484    fn default() -> Self {
1485        Self {
1486            enabled: false,
1487            database_path: "./mockforge-recordings.db".to_string(),
1488            api_enabled: true,
1489            api_port: None,
1490            max_requests: 10000,
1491            retention_days: 7,
1492            record_http: true,
1493            record_grpc: true,
1494            record_websocket: true,
1495            record_graphql: true,
1496            record_proxy: true,
1497        }
1498    }
1499}
1500
1501/// Chaos engineering configuration
1502#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1503#[serde(default)]
1504pub struct ChaosEngConfig {
1505    /// Enable chaos engineering
1506    pub enabled: bool,
1507    /// Latency injection configuration
1508    pub latency: Option<LatencyInjectionConfig>,
1509    /// Fault injection configuration
1510    pub fault_injection: Option<FaultConfig>,
1511    /// Rate limiting configuration
1512    pub rate_limit: Option<RateLimitingConfig>,
1513    /// Traffic shaping configuration
1514    pub traffic_shaping: Option<NetworkShapingConfig>,
1515    /// Predefined scenario to use
1516    pub scenario: Option<String>,
1517}
1518
1519/// Latency injection configuration for chaos engineering
1520#[derive(Debug, Clone, Serialize, Deserialize)]
1521pub struct LatencyInjectionConfig {
1522    /// Enable latency injection
1523    pub enabled: bool,
1524    /// Fixed delay to inject (in milliseconds)
1525    pub fixed_delay_ms: Option<u64>,
1526    /// Random delay range (min_ms, max_ms) in milliseconds
1527    pub random_delay_range_ms: Option<(u64, u64)>,
1528    /// Jitter percentage to add variance to delays (0.0 to 1.0)
1529    pub jitter_percent: f64,
1530    /// Probability of injecting latency (0.0 to 1.0)
1531    pub probability: f64,
1532}
1533
1534/// Fault injection configuration for chaos engineering
1535#[derive(Debug, Clone, Serialize, Deserialize)]
1536pub struct FaultConfig {
1537    /// Enable fault injection
1538    pub enabled: bool,
1539    /// HTTP status codes to randomly return (e.g., [500, 502, 503])
1540    pub http_errors: Vec<u16>,
1541    /// Probability of returning HTTP errors (0.0 to 1.0)
1542    pub http_error_probability: f64,
1543    /// Enable connection errors (connection refused, reset, etc.)
1544    pub connection_errors: bool,
1545    /// Probability of connection errors (0.0 to 1.0)
1546    pub connection_error_probability: f64,
1547    /// Enable timeout errors
1548    pub timeout_errors: bool,
1549    /// Timeout duration in milliseconds
1550    pub timeout_ms: u64,
1551    /// Probability of timeout errors (0.0 to 1.0)
1552    pub timeout_probability: f64,
1553}
1554
1555/// Rate limiting configuration for traffic control
1556#[derive(Debug, Clone, Serialize, Deserialize)]
1557pub struct RateLimitingConfig {
1558    /// Enable rate limiting
1559    pub enabled: bool,
1560    /// Maximum requests per second allowed
1561    pub requests_per_second: u32,
1562    /// Maximum burst size before rate limiting kicks in
1563    pub burst_size: u32,
1564    /// Apply rate limiting per IP address
1565    pub per_ip: bool,
1566    /// Apply rate limiting per endpoint/path
1567    pub per_endpoint: bool,
1568}
1569
1570/// Network shaping configuration for simulating network conditions
1571#[derive(Debug, Clone, Serialize, Deserialize)]
1572pub struct NetworkShapingConfig {
1573    /// Enable network shaping
1574    pub enabled: bool,
1575    /// Bandwidth limit in bits per second
1576    pub bandwidth_limit_bps: u64,
1577    /// Packet loss percentage (0.0 to 1.0)
1578    pub packet_loss_percent: f64,
1579    /// Maximum concurrent connections allowed
1580    pub max_connections: u32,
1581}
1582
1583/// Load configuration from file
1584pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1585    let content = fs::read_to_string(&path)
1586        .await
1587        .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1588
1589    // Parse config with improved error messages
1590    let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1591        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1592    {
1593        serde_yaml::from_str(&content).map_err(|e| {
1594            // Improve error message with field path context
1595            let error_msg = e.to_string();
1596            let mut full_msg = format!("Failed to parse YAML config: {}", error_msg);
1597
1598            // Add helpful context for common errors
1599            if error_msg.contains("missing field") {
1600                full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1601                full_msg.push_str(
1602                    "\n   Omit fields you don't need - MockForge will use sensible defaults.",
1603                );
1604                full_msg.push_str("\n   See config.template.yaml for all available options.");
1605            } else if error_msg.contains("unknown field") {
1606                full_msg.push_str("\n\n💡 Check for typos in field names.");
1607                full_msg.push_str("\n   See config.template.yaml for valid field names.");
1608            }
1609
1610            Error::generic(full_msg)
1611        })?
1612    } else {
1613        serde_json::from_str(&content).map_err(|e| {
1614            // Improve error message with field path context
1615            let error_msg = e.to_string();
1616            let mut full_msg = format!("Failed to parse JSON config: {}", error_msg);
1617
1618            // Add helpful context for common errors
1619            if error_msg.contains("missing field") {
1620                full_msg.push_str("\n\n💡 Most configuration fields are optional with defaults.");
1621                full_msg.push_str(
1622                    "\n   Omit fields you don't need - MockForge will use sensible defaults.",
1623                );
1624                full_msg.push_str("\n   See config.template.yaml for all available options.");
1625            } else if error_msg.contains("unknown field") {
1626                full_msg.push_str("\n\n💡 Check for typos in field names.");
1627                full_msg.push_str("\n   See config.template.yaml for valid field names.");
1628            }
1629
1630            Error::generic(full_msg)
1631        })?
1632    };
1633
1634    Ok(config)
1635}
1636
1637/// Save configuration to file
1638pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1639    let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1640        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1641    {
1642        serde_yaml::to_string(config)
1643            .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1644    } else {
1645        serde_json::to_string_pretty(config)
1646            .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1647    };
1648
1649    fs::write(path, content)
1650        .await
1651        .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1652
1653    Ok(())
1654}
1655
1656/// Load configuration with fallback to default
1657pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1658    match load_config(&path).await {
1659        Ok(config) => {
1660            tracing::info!("Loaded configuration from {:?}", path.as_ref());
1661            config
1662        }
1663        Err(e) => {
1664            tracing::warn!(
1665                "Failed to load config from {:?}: {}. Using defaults.",
1666                path.as_ref(),
1667                e
1668            );
1669            ServerConfig::default()
1670        }
1671    }
1672}
1673
1674/// Create default configuration file
1675pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1676    let config = ServerConfig::default();
1677    save_config(path, &config).await?;
1678    Ok(())
1679}
1680
1681/// Environment variable overrides for configuration
1682pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1683    // HTTP server overrides
1684    if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1685        if let Ok(port_num) = port.parse() {
1686            config.http.port = port_num;
1687        }
1688    }
1689
1690    if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1691        config.http.host = host;
1692    }
1693
1694    // WebSocket server overrides
1695    if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1696        if let Ok(port_num) = port.parse() {
1697            config.websocket.port = port_num;
1698        }
1699    }
1700
1701    // gRPC server overrides
1702    if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1703        if let Ok(port_num) = port.parse() {
1704            config.grpc.port = port_num;
1705        }
1706    }
1707
1708    // SMTP server overrides
1709    if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1710        if let Ok(port_num) = port.parse() {
1711            config.smtp.port = port_num;
1712        }
1713    }
1714
1715    if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1716        config.smtp.host = host;
1717    }
1718
1719    if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1720        config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1721    }
1722
1723    if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1724        config.smtp.hostname = hostname;
1725    }
1726
1727    // TCP server overrides
1728    if let Ok(port) = std::env::var("MOCKFORGE_TCP_PORT") {
1729        if let Ok(port_num) = port.parse() {
1730            config.tcp.port = port_num;
1731        }
1732    }
1733
1734    if let Ok(host) = std::env::var("MOCKFORGE_TCP_HOST") {
1735        config.tcp.host = host;
1736    }
1737
1738    if let Ok(enabled) = std::env::var("MOCKFORGE_TCP_ENABLED") {
1739        config.tcp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1740    }
1741
1742    // Admin UI overrides
1743    if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1744        if let Ok(port_num) = port.parse() {
1745            config.admin.port = port_num;
1746        }
1747    }
1748
1749    if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1750        config.admin.enabled = true;
1751    }
1752
1753    // Admin UI host override - critical for Docker deployments
1754    if let Ok(host) = std::env::var("MOCKFORGE_ADMIN_HOST") {
1755        config.admin.host = host;
1756    }
1757
1758    if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1759        if !mount_path.trim().is_empty() {
1760            config.admin.mount_path = Some(mount_path);
1761        }
1762    }
1763
1764    if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1765        let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1766        config.admin.api_enabled = on;
1767    }
1768
1769    if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1770        config.admin.prometheus_url = prometheus_url;
1771    }
1772
1773    // Core configuration overrides
1774    if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1775        let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1776        config.core.latency_enabled = enabled;
1777    }
1778
1779    if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1780        let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1781        config.core.failures_enabled = enabled;
1782    }
1783
1784    if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1785        let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1786        config.core.overrides_enabled = enabled;
1787    }
1788
1789    if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1790        let enabled =
1791            traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1792        config.core.traffic_shaping_enabled = enabled;
1793    }
1794
1795    // Traffic shaping overrides
1796    if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1797        let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1798        config.core.traffic_shaping.bandwidth.enabled = enabled;
1799    }
1800
1801    if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1802        if let Ok(bytes) = max_bytes_per_sec.parse() {
1803            config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1804            config.core.traffic_shaping.bandwidth.enabled = true;
1805        }
1806    }
1807
1808    if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1809        if let Ok(bytes) = burst_capacity.parse() {
1810            config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1811        }
1812    }
1813
1814    if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1815        let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1816        config.core.traffic_shaping.burst_loss.enabled = enabled;
1817    }
1818
1819    if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1820        if let Ok(prob) = burst_probability.parse::<f64>() {
1821            config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1822            config.core.traffic_shaping.burst_loss.enabled = true;
1823        }
1824    }
1825
1826    if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1827        if let Ok(ms) = burst_duration.parse() {
1828            config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1829        }
1830    }
1831
1832    if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1833        if let Ok(rate) = loss_rate.parse::<f64>() {
1834            config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1835        }
1836    }
1837
1838    if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1839        if let Ok(ms) = recovery_time.parse() {
1840            config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1841        }
1842    }
1843
1844    // Logging overrides
1845    if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1846        config.logging.level = level;
1847    }
1848
1849    config
1850}
1851
1852/// Validate configuration
1853pub fn validate_config(config: &ServerConfig) -> Result<()> {
1854    // Validate port ranges
1855    if config.http.port == 0 {
1856        return Err(Error::generic("HTTP port cannot be 0"));
1857    }
1858    if config.websocket.port == 0 {
1859        return Err(Error::generic("WebSocket port cannot be 0"));
1860    }
1861    if config.grpc.port == 0 {
1862        return Err(Error::generic("gRPC port cannot be 0"));
1863    }
1864    if config.admin.port == 0 {
1865        return Err(Error::generic("Admin port cannot be 0"));
1866    }
1867
1868    // Check for port conflicts
1869    let ports = [
1870        ("HTTP", config.http.port),
1871        ("WebSocket", config.websocket.port),
1872        ("gRPC", config.grpc.port),
1873        ("Admin", config.admin.port),
1874    ];
1875
1876    for i in 0..ports.len() {
1877        for j in (i + 1)..ports.len() {
1878            if ports[i].1 == ports[j].1 {
1879                return Err(Error::generic(format!(
1880                    "Port conflict: {} and {} both use port {}",
1881                    ports[i].0, ports[j].0, ports[i].1
1882                )));
1883            }
1884        }
1885    }
1886
1887    // Validate log level
1888    let valid_levels = ["trace", "debug", "info", "warn", "error"];
1889    if !valid_levels.contains(&config.logging.level.as_str()) {
1890        return Err(Error::generic(format!(
1891            "Invalid log level: {}. Valid levels: {}",
1892            config.logging.level,
1893            valid_levels.join(", ")
1894        )));
1895    }
1896
1897    Ok(())
1898}
1899
1900/// Apply a profile to a base configuration
1901pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1902    // Macro to merge optional fields
1903    macro_rules! merge_field {
1904        ($field:ident) => {
1905            if let Some(override_val) = profile.$field {
1906                base.$field = override_val;
1907            }
1908        };
1909    }
1910
1911    merge_field!(http);
1912    merge_field!(websocket);
1913    merge_field!(graphql);
1914    merge_field!(grpc);
1915    merge_field!(mqtt);
1916    merge_field!(smtp);
1917    merge_field!(ftp);
1918    merge_field!(kafka);
1919    merge_field!(amqp);
1920    merge_field!(tcp);
1921    merge_field!(admin);
1922    merge_field!(chaining);
1923    merge_field!(core);
1924    merge_field!(logging);
1925    merge_field!(data);
1926    merge_field!(mockai);
1927    merge_field!(observability);
1928    merge_field!(multi_tenant);
1929    merge_field!(routes);
1930    merge_field!(protocols);
1931
1932    base
1933}
1934
1935/// Load configuration with profile support
1936pub async fn load_config_with_profile<P: AsRef<Path>>(
1937    path: P,
1938    profile_name: Option<&str>,
1939) -> Result<ServerConfig> {
1940    // Use load_config_auto to support all formats
1941    let mut config = load_config_auto(&path).await?;
1942
1943    // Apply profile if specified
1944    if let Some(profile) = profile_name {
1945        if let Some(profile_config) = config.profiles.remove(profile) {
1946            tracing::info!("Applying profile: {}", profile);
1947            config = apply_profile(config, profile_config);
1948        } else {
1949            return Err(Error::generic(format!(
1950                "Profile '{}' not found in configuration. Available profiles: {}",
1951                profile,
1952                config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
1953            )));
1954        }
1955    }
1956
1957    // Clear profiles from final config to save memory
1958    config.profiles.clear();
1959
1960    Ok(config)
1961}
1962
1963/// Load configuration from TypeScript/JavaScript file
1964pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1965    use rquickjs::{Context, Runtime};
1966
1967    let content = fs::read_to_string(&path)
1968        .await
1969        .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
1970
1971    // Create a JavaScript runtime
1972    let runtime = Runtime::new()
1973        .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
1974    let context = Context::full(&runtime)
1975        .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
1976
1977    context.with(|ctx| {
1978        // For TypeScript files, we need to strip type annotations
1979        // This is a simple approach - for production, consider using a proper TS compiler
1980        let js_content = if path
1981            .as_ref()
1982            .extension()
1983            .and_then(|s| s.to_str())
1984            .map(|ext| ext == "ts")
1985            .unwrap_or(false)
1986        {
1987            strip_typescript_types(&content)?
1988        } else {
1989            content
1990        };
1991
1992        // Evaluate the config file
1993        let result: rquickjs::Value = ctx
1994            .eval(js_content.as_bytes())
1995            .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
1996
1997        // Convert to JSON string
1998        let json_str: String = ctx
1999            .json_stringify(result)
2000            .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
2001            .ok_or_else(|| Error::generic("JS config returned undefined"))?
2002            .get()
2003            .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
2004
2005        // Parse JSON into ServerConfig
2006        serde_json::from_str(&json_str).map_err(|e| {
2007            Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
2008        })
2009    })
2010}
2011
2012/// Simple TypeScript type stripper (removes type annotations)
2013/// Note: This is a basic implementation. For production use, consider using swc or esbuild
2014///
2015/// # Errors
2016/// Returns an error if regex compilation fails. This should never happen with static patterns,
2017/// but we handle it gracefully to prevent panics.
2018fn strip_typescript_types(content: &str) -> Result<String> {
2019    use regex::Regex;
2020
2021    let mut result = content.to_string();
2022
2023    // Compile regex patterns with error handling
2024    // Note: These patterns are statically known and should never fail,
2025    // but we handle errors to prevent panics in edge cases
2026
2027    // Remove interface declarations (handles multi-line)
2028    let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*")
2029        .map_err(|e| Error::generic(format!("Failed to compile interface regex: {}", e)))?;
2030    result = interface_re.replace_all(&result, "").to_string();
2031
2032    // Remove type aliases
2033    let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*")
2034        .map_err(|e| Error::generic(format!("Failed to compile type alias regex: {}", e)))?;
2035    result = type_alias_re.replace_all(&result, "").to_string();
2036
2037    // Remove type annotations (: Type)
2038    let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?")
2039        .map_err(|e| Error::generic(format!("Failed to compile type annotation regex: {}", e)))?;
2040    result = type_annotation_re.replace_all(&result, "").to_string();
2041
2042    // Remove type imports and exports
2043    let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$")
2044        .map_err(|e| Error::generic(format!("Failed to compile type import regex: {}", e)))?;
2045    result = type_import_re.replace_all(&result, "").to_string();
2046
2047    // Remove as Type
2048    let as_type_re = Regex::new(r"\s+as\s+\w+")
2049        .map_err(|e| Error::generic(format!("Failed to compile 'as type' regex: {}", e)))?;
2050    result = as_type_re.replace_all(&result, "").to_string();
2051
2052    Ok(result)
2053}
2054
2055/// Enhanced load_config that supports multiple formats including JS/TS
2056pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
2057    let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
2058
2059    match ext {
2060        "ts" | "js" => load_config_from_js(&path).await,
2061        "yaml" | "yml" | "json" => load_config(&path).await,
2062        _ => Err(Error::generic(format!(
2063            "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
2064            ext
2065        ))),
2066    }
2067}
2068
2069/// Discover configuration file with support for all formats
2070pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
2071    let current_dir = std::env::current_dir()
2072        .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
2073
2074    let config_names = vec![
2075        "mockforge.config.ts",
2076        "mockforge.config.js",
2077        "mockforge.yaml",
2078        "mockforge.yml",
2079        ".mockforge.yaml",
2080        ".mockforge.yml",
2081    ];
2082
2083    // Check current directory
2084    for name in &config_names {
2085        let path = current_dir.join(name);
2086        if tokio::fs::metadata(&path).await.is_ok() {
2087            return Ok(path);
2088        }
2089    }
2090
2091    // Check parent directories (up to 5 levels)
2092    let mut dir = current_dir.clone();
2093    for _ in 0..5 {
2094        if let Some(parent) = dir.parent() {
2095            for name in &config_names {
2096                let path = parent.join(name);
2097                if tokio::fs::metadata(&path).await.is_ok() {
2098                    return Ok(path);
2099                }
2100            }
2101            dir = parent.to_path_buf();
2102        } else {
2103            break;
2104        }
2105    }
2106
2107    Err(Error::generic(
2108        "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
2109    ))
2110}
2111
2112#[cfg(test)]
2113mod tests {
2114    use super::*;
2115
2116    #[test]
2117    fn test_default_config() {
2118        let config = ServerConfig::default();
2119        assert_eq!(config.http.port, 3000);
2120        assert_eq!(config.websocket.port, 3001);
2121        assert_eq!(config.grpc.port, 50051);
2122        assert_eq!(config.admin.port, 9080);
2123    }
2124
2125    #[test]
2126    fn test_config_validation() {
2127        let mut config = ServerConfig::default();
2128        assert!(validate_config(&config).is_ok());
2129
2130        // Test port conflict
2131        config.websocket.port = config.http.port;
2132        assert!(validate_config(&config).is_err());
2133
2134        // Test invalid log level
2135        config.websocket.port = 3001; // Fix port conflict
2136        config.logging.level = "invalid".to_string();
2137        assert!(validate_config(&config).is_err());
2138    }
2139
2140    #[test]
2141    fn test_apply_profile() {
2142        let mut base = ServerConfig::default();
2143        assert_eq!(base.http.port, 3000);
2144
2145        let mut profile = ProfileConfig::default();
2146        profile.http = Some(HttpConfig {
2147            port: 8080,
2148            ..Default::default()
2149        });
2150        profile.logging = Some(LoggingConfig {
2151            level: "debug".to_string(),
2152            ..Default::default()
2153        });
2154
2155        let merged = apply_profile(base, profile);
2156        assert_eq!(merged.http.port, 8080);
2157        assert_eq!(merged.logging.level, "debug");
2158        assert_eq!(merged.websocket.port, 3001); // Unchanged
2159    }
2160
2161    #[test]
2162    fn test_strip_typescript_types() {
2163        let ts_code = r#"
2164interface Config {
2165    port: number;
2166    host: string;
2167}
2168
2169const config: Config = {
2170    port: 3000,
2171    host: "localhost"
2172} as Config;
2173"#;
2174
2175        let stripped = strip_typescript_types(ts_code).expect("Should strip TypeScript types");
2176        assert!(!stripped.contains("interface"));
2177        assert!(!stripped.contains(": Config"));
2178        assert!(!stripped.contains("as Config"));
2179    }
2180}