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