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