mockforge_core/
config.rs

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