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        Self {
726            enabled: false,
727            port: 9080,
728            host: "127.0.0.1".to_string(),
729            auth_required: false,
730            username: None,
731            password: None,
732            mount_path: None,
733            api_enabled: true,
734            prometheus_url: "http://localhost:9090".to_string(),
735        }
736    }
737}
738
739/// Logging configuration
740#[derive(Debug, Clone, Serialize, Deserialize)]
741#[serde(default)]
742pub struct LoggingConfig {
743    /// Log level
744    pub level: String,
745    /// Enable JSON logging
746    pub json_format: bool,
747    /// Log file path (optional)
748    pub file_path: Option<String>,
749    /// Maximum log file size in MB
750    pub max_file_size_mb: u64,
751    /// Maximum number of log files to keep
752    pub max_files: u32,
753}
754
755impl Default for LoggingConfig {
756    fn default() -> Self {
757        Self {
758            level: "info".to_string(),
759            json_format: false,
760            file_path: None,
761            max_file_size_mb: 10,
762            max_files: 5,
763        }
764    }
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize)]
768#[serde(default, rename_all = "camelCase")]
769pub struct ChainingConfig {
770    /// Enable request chaining
771    pub enabled: bool,
772    /// Maximum chain length to prevent infinite loops
773    pub max_chain_length: usize,
774    /// Global timeout for chain execution in seconds
775    pub global_timeout_secs: u64,
776    /// Enable parallel execution when dependencies allow
777    pub enable_parallel_execution: bool,
778}
779
780impl Default for ChainingConfig {
781    fn default() -> Self {
782        Self {
783            enabled: false,
784            max_chain_length: 20,
785            global_timeout_secs: 300,
786            enable_parallel_execution: false,
787        }
788    }
789}
790
791/// Data generation configuration
792#[derive(Debug, Clone, Serialize, Deserialize)]
793#[serde(default)]
794pub struct DataConfig {
795    /// Default number of rows to generate
796    pub default_rows: usize,
797    /// Default output format
798    pub default_format: String,
799    /// Faker locale
800    pub locale: String,
801    /// Custom faker templates
802    pub templates: HashMap<String, String>,
803    /// RAG configuration
804    pub rag: RagConfig,
805}
806
807impl Default for DataConfig {
808    fn default() -> Self {
809        Self {
810            default_rows: 100,
811            default_format: "json".to_string(),
812            locale: "en".to_string(),
813            templates: HashMap::new(),
814            rag: RagConfig::default(),
815        }
816    }
817}
818
819/// RAG configuration
820#[derive(Debug, Clone, Serialize, Deserialize)]
821#[serde(default)]
822pub struct RagConfig {
823    /// Enable RAG by default
824    pub enabled: bool,
825    /// LLM provider (openai, anthropic, ollama, openai_compatible)
826    #[serde(default)]
827    pub provider: String,
828    /// API endpoint for LLM
829    pub api_endpoint: Option<String>,
830    /// API key for LLM
831    pub api_key: Option<String>,
832    /// Model name
833    pub model: Option<String>,
834    /// Maximum tokens for generation
835    #[serde(default = "default_max_tokens")]
836    pub max_tokens: usize,
837    /// Temperature for generation (0.0 to 2.0)
838    #[serde(default = "default_temperature")]
839    pub temperature: f64,
840    /// Context window size
841    pub context_window: usize,
842    /// Enable caching
843    #[serde(default = "default_true")]
844    pub caching: bool,
845    /// Cache TTL in seconds
846    #[serde(default = "default_cache_ttl")]
847    pub cache_ttl_secs: u64,
848    /// Request timeout in seconds
849    #[serde(default = "default_timeout")]
850    pub timeout_secs: u64,
851    /// Maximum retries for failed requests
852    #[serde(default = "default_max_retries")]
853    pub max_retries: usize,
854}
855
856fn default_max_tokens() -> usize {
857    1024
858}
859
860fn default_temperature() -> f64 {
861    0.7
862}
863
864fn default_true() -> bool {
865    true
866}
867
868fn default_cache_ttl() -> u64 {
869    3600
870}
871
872fn default_timeout() -> u64 {
873    30
874}
875
876fn default_max_retries() -> usize {
877    3
878}
879
880impl Default for RagConfig {
881    fn default() -> Self {
882        Self {
883            enabled: false,
884            provider: "openai".to_string(),
885            api_endpoint: None,
886            api_key: None,
887            model: Some("gpt-3.5-turbo".to_string()),
888            max_tokens: default_max_tokens(),
889            temperature: default_temperature(),
890            context_window: 4000,
891            caching: default_true(),
892            cache_ttl_secs: default_cache_ttl(),
893            timeout_secs: default_timeout(),
894            max_retries: default_max_retries(),
895        }
896    }
897}
898
899/// Observability configuration for metrics and distributed tracing
900#[derive(Debug, Clone, Serialize, Deserialize, Default)]
901#[serde(default)]
902pub struct ObservabilityConfig {
903    /// Prometheus metrics configuration
904    pub prometheus: PrometheusConfig,
905    /// OpenTelemetry distributed tracing configuration
906    pub opentelemetry: Option<OpenTelemetryConfig>,
907    /// API Flight Recorder configuration
908    pub recorder: Option<RecorderConfig>,
909    /// Chaos engineering configuration
910    pub chaos: Option<ChaosEngConfig>,
911}
912
913/// Prometheus metrics configuration
914#[derive(Debug, Clone, Serialize, Deserialize)]
915#[serde(default)]
916pub struct PrometheusConfig {
917    /// Enable Prometheus metrics endpoint
918    pub enabled: bool,
919    /// Port for metrics endpoint
920    pub port: u16,
921    /// Host for metrics endpoint
922    pub host: String,
923    /// Path for metrics endpoint
924    pub path: String,
925}
926
927impl Default for PrometheusConfig {
928    fn default() -> Self {
929        Self {
930            enabled: true,
931            port: 9090,
932            host: "0.0.0.0".to_string(),
933            path: "/metrics".to_string(),
934        }
935    }
936}
937
938/// OpenTelemetry distributed tracing configuration
939#[derive(Debug, Clone, Serialize, Deserialize)]
940#[serde(default)]
941pub struct OpenTelemetryConfig {
942    /// Enable OpenTelemetry tracing
943    pub enabled: bool,
944    /// Service name for traces
945    pub service_name: String,
946    /// Deployment environment (development, staging, production)
947    pub environment: String,
948    /// Jaeger endpoint for trace export
949    pub jaeger_endpoint: String,
950    /// OTLP endpoint (alternative to Jaeger)
951    pub otlp_endpoint: Option<String>,
952    /// Protocol: grpc or http
953    pub protocol: String,
954    /// Sampling rate (0.0 to 1.0)
955    pub sampling_rate: f64,
956}
957
958impl Default for OpenTelemetryConfig {
959    fn default() -> Self {
960        Self {
961            enabled: false,
962            service_name: "mockforge".to_string(),
963            environment: "development".to_string(),
964            jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
965            otlp_endpoint: Some("http://localhost:4317".to_string()),
966            protocol: "grpc".to_string(),
967            sampling_rate: 1.0,
968        }
969    }
970}
971
972/// API Flight Recorder configuration
973#[derive(Debug, Clone, Serialize, Deserialize)]
974#[serde(default)]
975pub struct RecorderConfig {
976    /// Enable recording
977    pub enabled: bool,
978    /// Database file path
979    pub database_path: String,
980    /// Enable management API
981    pub api_enabled: bool,
982    /// Management API port (if different from main port)
983    pub api_port: Option<u16>,
984    /// Maximum number of requests to store (0 for unlimited)
985    pub max_requests: i64,
986    /// Auto-delete requests older than N days (0 to disable)
987    pub retention_days: i64,
988    /// Record HTTP requests
989    pub record_http: bool,
990    /// Record gRPC requests
991    pub record_grpc: bool,
992    /// Record WebSocket messages
993    pub record_websocket: bool,
994    /// Record GraphQL requests
995    pub record_graphql: bool,
996}
997
998impl Default for RecorderConfig {
999    fn default() -> Self {
1000        Self {
1001            enabled: false,
1002            database_path: "./mockforge-recordings.db".to_string(),
1003            api_enabled: true,
1004            api_port: None,
1005            max_requests: 10000,
1006            retention_days: 7,
1007            record_http: true,
1008            record_grpc: true,
1009            record_websocket: true,
1010            record_graphql: true,
1011        }
1012    }
1013}
1014
1015/// Chaos engineering configuration
1016#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1017#[serde(default)]
1018pub struct ChaosEngConfig {
1019    /// Enable chaos engineering
1020    pub enabled: bool,
1021    /// Latency injection configuration
1022    pub latency: Option<LatencyInjectionConfig>,
1023    /// Fault injection configuration
1024    pub fault_injection: Option<FaultConfig>,
1025    /// Rate limiting configuration
1026    pub rate_limit: Option<RateLimitingConfig>,
1027    /// Traffic shaping configuration
1028    pub traffic_shaping: Option<NetworkShapingConfig>,
1029    /// Predefined scenario to use
1030    pub scenario: Option<String>,
1031}
1032
1033/// Latency injection configuration
1034#[derive(Debug, Clone, Serialize, Deserialize)]
1035pub struct LatencyInjectionConfig {
1036    pub enabled: bool,
1037    pub fixed_delay_ms: Option<u64>,
1038    pub random_delay_range_ms: Option<(u64, u64)>,
1039    pub jitter_percent: f64,
1040    pub probability: f64,
1041}
1042
1043/// Fault injection configuration
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1045pub struct FaultConfig {
1046    pub enabled: bool,
1047    pub http_errors: Vec<u16>,
1048    pub http_error_probability: f64,
1049    pub connection_errors: bool,
1050    pub connection_error_probability: f64,
1051    pub timeout_errors: bool,
1052    pub timeout_ms: u64,
1053    pub timeout_probability: f64,
1054}
1055
1056/// Rate limiting configuration
1057#[derive(Debug, Clone, Serialize, Deserialize)]
1058pub struct RateLimitingConfig {
1059    pub enabled: bool,
1060    pub requests_per_second: u32,
1061    pub burst_size: u32,
1062    pub per_ip: bool,
1063    pub per_endpoint: bool,
1064}
1065
1066/// Network shaping configuration
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1068pub struct NetworkShapingConfig {
1069    pub enabled: bool,
1070    pub bandwidth_limit_bps: u64,
1071    pub packet_loss_percent: f64,
1072    pub max_connections: u32,
1073}
1074
1075/// Load configuration from file
1076pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1077    let content = fs::read_to_string(&path)
1078        .await
1079        .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
1080
1081    let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1082        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1083    {
1084        serde_yaml::from_str(&content)
1085            .map_err(|e| Error::generic(format!("Failed to parse YAML config: {}", e)))?
1086    } else {
1087        serde_json::from_str(&content)
1088            .map_err(|e| Error::generic(format!("Failed to parse JSON config: {}", e)))?
1089    };
1090
1091    Ok(config)
1092}
1093
1094/// Save configuration to file
1095pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
1096    let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
1097        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
1098    {
1099        serde_yaml::to_string(config)
1100            .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
1101    } else {
1102        serde_json::to_string_pretty(config)
1103            .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
1104    };
1105
1106    fs::write(path, content)
1107        .await
1108        .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
1109
1110    Ok(())
1111}
1112
1113/// Load configuration with fallback to default
1114pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
1115    match load_config(&path).await {
1116        Ok(config) => {
1117            tracing::info!("Loaded configuration from {:?}", path.as_ref());
1118            config
1119        }
1120        Err(e) => {
1121            tracing::warn!(
1122                "Failed to load config from {:?}: {}. Using defaults.",
1123                path.as_ref(),
1124                e
1125            );
1126            ServerConfig::default()
1127        }
1128    }
1129}
1130
1131/// Create default configuration file
1132pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1133    let config = ServerConfig::default();
1134    save_config(path, &config).await?;
1135    Ok(())
1136}
1137
1138/// Environment variable overrides for configuration
1139pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1140    // HTTP server overrides
1141    if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1142        if let Ok(port_num) = port.parse() {
1143            config.http.port = port_num;
1144        }
1145    }
1146
1147    if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1148        config.http.host = host;
1149    }
1150
1151    // WebSocket server overrides
1152    if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1153        if let Ok(port_num) = port.parse() {
1154            config.websocket.port = port_num;
1155        }
1156    }
1157
1158    // gRPC server overrides
1159    if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1160        if let Ok(port_num) = port.parse() {
1161            config.grpc.port = port_num;
1162        }
1163    }
1164
1165    // SMTP server overrides
1166    if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1167        if let Ok(port_num) = port.parse() {
1168            config.smtp.port = port_num;
1169        }
1170    }
1171
1172    if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1173        config.smtp.host = host;
1174    }
1175
1176    if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1177        config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1178    }
1179
1180    if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1181        config.smtp.hostname = hostname;
1182    }
1183
1184    // Admin UI overrides
1185    if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1186        if let Ok(port_num) = port.parse() {
1187            config.admin.port = port_num;
1188        }
1189    }
1190
1191    if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1192        config.admin.enabled = true;
1193    }
1194
1195    if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1196        if !mount_path.trim().is_empty() {
1197            config.admin.mount_path = Some(mount_path);
1198        }
1199    }
1200
1201    if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1202        let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1203        config.admin.api_enabled = on;
1204    }
1205
1206    if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1207        config.admin.prometheus_url = prometheus_url;
1208    }
1209
1210    // Core configuration overrides
1211    if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1212        let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1213        config.core.latency_enabled = enabled;
1214    }
1215
1216    if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1217        let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1218        config.core.failures_enabled = enabled;
1219    }
1220
1221    if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1222        let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1223        config.core.overrides_enabled = enabled;
1224    }
1225
1226    if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1227        let enabled =
1228            traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1229        config.core.traffic_shaping_enabled = enabled;
1230    }
1231
1232    // Traffic shaping overrides
1233    if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1234        let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1235        config.core.traffic_shaping.bandwidth.enabled = enabled;
1236    }
1237
1238    if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1239        if let Ok(bytes) = max_bytes_per_sec.parse() {
1240            config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1241            config.core.traffic_shaping.bandwidth.enabled = true;
1242        }
1243    }
1244
1245    if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1246        if let Ok(bytes) = burst_capacity.parse() {
1247            config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1248        }
1249    }
1250
1251    if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1252        let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1253        config.core.traffic_shaping.burst_loss.enabled = enabled;
1254    }
1255
1256    if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1257        if let Ok(prob) = burst_probability.parse::<f64>() {
1258            config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1259            config.core.traffic_shaping.burst_loss.enabled = true;
1260        }
1261    }
1262
1263    if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1264        if let Ok(ms) = burst_duration.parse() {
1265            config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1266        }
1267    }
1268
1269    if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1270        if let Ok(rate) = loss_rate.parse::<f64>() {
1271            config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1272        }
1273    }
1274
1275    if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1276        if let Ok(ms) = recovery_time.parse() {
1277            config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1278        }
1279    }
1280
1281    // Logging overrides
1282    if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1283        config.logging.level = level;
1284    }
1285
1286    config
1287}
1288
1289/// Validate configuration
1290pub fn validate_config(config: &ServerConfig) -> Result<()> {
1291    // Validate port ranges
1292    if config.http.port == 0 {
1293        return Err(Error::generic("HTTP port cannot be 0"));
1294    }
1295    if config.websocket.port == 0 {
1296        return Err(Error::generic("WebSocket port cannot be 0"));
1297    }
1298    if config.grpc.port == 0 {
1299        return Err(Error::generic("gRPC port cannot be 0"));
1300    }
1301    if config.admin.port == 0 {
1302        return Err(Error::generic("Admin port cannot be 0"));
1303    }
1304
1305    // Check for port conflicts
1306    let ports = [
1307        ("HTTP", config.http.port),
1308        ("WebSocket", config.websocket.port),
1309        ("gRPC", config.grpc.port),
1310        ("Admin", config.admin.port),
1311    ];
1312
1313    for i in 0..ports.len() {
1314        for j in (i + 1)..ports.len() {
1315            if ports[i].1 == ports[j].1 {
1316                return Err(Error::generic(format!(
1317                    "Port conflict: {} and {} both use port {}",
1318                    ports[i].0, ports[j].0, ports[i].1
1319                )));
1320            }
1321        }
1322    }
1323
1324    // Validate log level
1325    let valid_levels = ["trace", "debug", "info", "warn", "error"];
1326    if !valid_levels.contains(&config.logging.level.as_str()) {
1327        return Err(Error::generic(format!(
1328            "Invalid log level: {}. Valid levels: {}",
1329            config.logging.level,
1330            valid_levels.join(", ")
1331        )));
1332    }
1333
1334    Ok(())
1335}
1336
1337/// Apply a profile to a base configuration
1338pub fn apply_profile(mut base: ServerConfig, profile: ProfileConfig) -> ServerConfig {
1339    // Macro to merge optional fields
1340    macro_rules! merge_field {
1341        ($field:ident) => {
1342            if let Some(override_val) = profile.$field {
1343                base.$field = override_val;
1344            }
1345        };
1346    }
1347
1348    merge_field!(http);
1349    merge_field!(websocket);
1350    merge_field!(graphql);
1351    merge_field!(grpc);
1352    merge_field!(mqtt);
1353    merge_field!(smtp);
1354    merge_field!(ftp);
1355    merge_field!(kafka);
1356    merge_field!(amqp);
1357    merge_field!(admin);
1358    merge_field!(chaining);
1359    merge_field!(core);
1360    merge_field!(logging);
1361    merge_field!(data);
1362    merge_field!(observability);
1363    merge_field!(multi_tenant);
1364    merge_field!(routes);
1365    merge_field!(protocols);
1366
1367    base
1368}
1369
1370/// Load configuration with profile support
1371pub async fn load_config_with_profile<P: AsRef<Path>>(
1372    path: P,
1373    profile_name: Option<&str>,
1374) -> Result<ServerConfig> {
1375    // Use load_config_auto to support all formats
1376    let mut config = load_config_auto(&path).await?;
1377
1378    // Apply profile if specified
1379    if let Some(profile) = profile_name {
1380        if let Some(profile_config) = config.profiles.remove(profile) {
1381            tracing::info!("Applying profile: {}", profile);
1382            config = apply_profile(config, profile_config);
1383        } else {
1384            return Err(Error::generic(format!(
1385                "Profile '{}' not found in configuration. Available profiles: {}",
1386                profile,
1387                config.profiles.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
1388            )));
1389        }
1390    }
1391
1392    // Clear profiles from final config to save memory
1393    config.profiles.clear();
1394
1395    Ok(config)
1396}
1397
1398/// Load configuration from TypeScript/JavaScript file
1399pub async fn load_config_from_js<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1400    use rquickjs::{Context, Runtime};
1401
1402    let content = fs::read_to_string(&path)
1403        .await
1404        .map_err(|e| Error::generic(format!("Failed to read JS/TS config file: {}", e)))?;
1405
1406    // Create a JavaScript runtime
1407    let runtime = Runtime::new()
1408        .map_err(|e| Error::generic(format!("Failed to create JS runtime: {}", e)))?;
1409    let context = Context::full(&runtime)
1410        .map_err(|e| Error::generic(format!("Failed to create JS context: {}", e)))?;
1411
1412    context.with(|ctx| {
1413        // For TypeScript files, we need to strip type annotations
1414        // This is a simple approach - for production, consider using a proper TS compiler
1415        let js_content = if path
1416            .as_ref()
1417            .extension()
1418            .and_then(|s| s.to_str())
1419            .map(|ext| ext == "ts")
1420            .unwrap_or(false)
1421        {
1422            strip_typescript_types(&content)
1423        } else {
1424            content
1425        };
1426
1427        // Evaluate the config file
1428        let result: rquickjs::Value = ctx
1429            .eval(js_content.as_bytes())
1430            .map_err(|e| Error::generic(format!("Failed to evaluate JS config: {}", e)))?;
1431
1432        // Convert to JSON string
1433        let json_str: String = ctx
1434            .json_stringify(result)
1435            .map_err(|e| Error::generic(format!("Failed to stringify JS config: {}", e)))?
1436            .ok_or_else(|| Error::generic("JS config returned undefined"))?
1437            .get()
1438            .map_err(|e| Error::generic(format!("Failed to get JSON string: {}", e)))?;
1439
1440        // Parse JSON into ServerConfig
1441        serde_json::from_str(&json_str).map_err(|e| {
1442            Error::generic(format!("Failed to parse JS config as ServerConfig: {}", e))
1443        })
1444    })
1445}
1446
1447/// Simple TypeScript type stripper (removes type annotations)
1448/// Note: This is a basic implementation. For production use, consider using swc or esbuild
1449fn strip_typescript_types(content: &str) -> String {
1450    use regex::Regex;
1451
1452    let mut result = content.to_string();
1453
1454    // Remove interface declarations (handles multi-line)
1455    let interface_re = Regex::new(r"(?ms)interface\s+\w+\s*\{[^}]*\}\s*").unwrap();
1456    result = interface_re.replace_all(&result, "").to_string();
1457
1458    // Remove type aliases
1459    let type_alias_re = Regex::new(r"(?m)^type\s+\w+\s*=\s*[^;]+;\s*").unwrap();
1460    result = type_alias_re.replace_all(&result, "").to_string();
1461
1462    // Remove type annotations (: Type)
1463    let type_annotation_re = Regex::new(r":\s*[A-Z]\w*(<[^>]+>)?(\[\])?").unwrap();
1464    result = type_annotation_re.replace_all(&result, "").to_string();
1465
1466    // Remove type imports and exports
1467    let type_import_re = Regex::new(r"(?m)^(import|export)\s+type\s+.*$").unwrap();
1468    result = type_import_re.replace_all(&result, "").to_string();
1469
1470    // Remove as Type
1471    let as_type_re = Regex::new(r"\s+as\s+\w+").unwrap();
1472    result = as_type_re.replace_all(&result, "").to_string();
1473
1474    result
1475}
1476
1477/// Enhanced load_config that supports multiple formats including JS/TS
1478pub async fn load_config_auto<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
1479    let ext = path.as_ref().extension().and_then(|s| s.to_str()).unwrap_or("");
1480
1481    match ext {
1482        "ts" | "js" => load_config_from_js(&path).await,
1483        "yaml" | "yml" | "json" => load_config(&path).await,
1484        _ => Err(Error::generic(format!(
1485            "Unsupported config file format: {}. Supported: .ts, .js, .yaml, .yml, .json",
1486            ext
1487        ))),
1488    }
1489}
1490
1491/// Discover configuration file with support for all formats
1492pub async fn discover_config_file_all_formats() -> Result<std::path::PathBuf> {
1493    let current_dir = std::env::current_dir()
1494        .map_err(|e| Error::generic(format!("Failed to get current directory: {}", e)))?;
1495
1496    let config_names = vec![
1497        "mockforge.config.ts",
1498        "mockforge.config.js",
1499        "mockforge.yaml",
1500        "mockforge.yml",
1501        ".mockforge.yaml",
1502        ".mockforge.yml",
1503    ];
1504
1505    // Check current directory
1506    for name in &config_names {
1507        let path = current_dir.join(name);
1508        if tokio::fs::metadata(&path).await.is_ok() {
1509            return Ok(path);
1510        }
1511    }
1512
1513    // Check parent directories (up to 5 levels)
1514    let mut dir = current_dir.clone();
1515    for _ in 0..5 {
1516        if let Some(parent) = dir.parent() {
1517            for name in &config_names {
1518                let path = parent.join(name);
1519                if tokio::fs::metadata(&path).await.is_ok() {
1520                    return Ok(path);
1521                }
1522            }
1523            dir = parent.to_path_buf();
1524        } else {
1525            break;
1526        }
1527    }
1528
1529    Err(Error::generic(
1530        "No configuration file found. Expected one of: mockforge.config.ts, mockforge.config.js, mockforge.yaml, mockforge.yml",
1531    ))
1532}
1533
1534#[cfg(test)]
1535mod tests {
1536    use super::*;
1537
1538    #[test]
1539    fn test_default_config() {
1540        let config = ServerConfig::default();
1541        assert_eq!(config.http.port, 3000);
1542        assert_eq!(config.websocket.port, 3001);
1543        assert_eq!(config.grpc.port, 50051);
1544        assert_eq!(config.admin.port, 9080);
1545    }
1546
1547    #[test]
1548    fn test_config_validation() {
1549        let mut config = ServerConfig::default();
1550        assert!(validate_config(&config).is_ok());
1551
1552        // Test port conflict
1553        config.websocket.port = config.http.port;
1554        assert!(validate_config(&config).is_err());
1555
1556        // Test invalid log level
1557        config.websocket.port = 3001; // Fix port conflict
1558        config.logging.level = "invalid".to_string();
1559        assert!(validate_config(&config).is_err());
1560    }
1561
1562    #[test]
1563    fn test_apply_profile() {
1564        let mut base = ServerConfig::default();
1565        assert_eq!(base.http.port, 3000);
1566
1567        let mut profile = ProfileConfig::default();
1568        profile.http = Some(HttpConfig {
1569            port: 8080,
1570            ..Default::default()
1571        });
1572        profile.logging = Some(LoggingConfig {
1573            level: "debug".to_string(),
1574            ..Default::default()
1575        });
1576
1577        let merged = apply_profile(base, profile);
1578        assert_eq!(merged.http.port, 8080);
1579        assert_eq!(merged.logging.level, "debug");
1580        assert_eq!(merged.websocket.port, 3001); // Unchanged
1581    }
1582
1583    #[test]
1584    fn test_strip_typescript_types() {
1585        let ts_code = r#"
1586interface Config {
1587    port: number;
1588    host: string;
1589}
1590
1591const config: Config = {
1592    port: 3000,
1593    host: "localhost"
1594} as Config;
1595"#;
1596
1597        let stripped = strip_typescript_types(ts_code);
1598        assert!(!stripped.contains("interface"));
1599        assert!(!stripped.contains(": Config"));
1600        assert!(!stripped.contains("as Config"));
1601    }
1602}