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)]
11pub struct AuthConfig {
12    /// JWT configuration
13    pub jwt: Option<JwtConfig>,
14    /// OAuth2 configuration
15    pub oauth2: Option<OAuth2Config>,
16    /// Basic auth configuration
17    pub basic_auth: Option<BasicAuthConfig>,
18    /// API key configuration
19    pub api_key: Option<ApiKeyConfig>,
20    /// Whether to require authentication for all requests
21    pub require_auth: bool,
22}
23
24/// JWT authentication configuration
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct JwtConfig {
27    /// JWT secret key for HMAC algorithms
28    pub secret: Option<String>,
29    /// RSA public key PEM for RSA algorithms
30    pub rsa_public_key: Option<String>,
31    /// ECDSA public key PEM for ECDSA algorithms
32    pub ecdsa_public_key: Option<String>,
33    /// Expected issuer
34    pub issuer: Option<String>,
35    /// Expected audience
36    pub audience: Option<String>,
37    /// Supported algorithms (defaults to HS256, RS256, ES256)
38    pub algorithms: Vec<String>,
39}
40
41/// OAuth2 configuration
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct OAuth2Config {
44    /// OAuth2 client ID
45    pub client_id: String,
46    /// OAuth2 client secret
47    pub client_secret: String,
48    /// Token introspection URL
49    pub introspection_url: String,
50    /// Authorization server URL
51    pub auth_url: Option<String>,
52    /// Token URL
53    pub token_url: Option<String>,
54    /// Expected token type
55    pub token_type_hint: Option<String>,
56}
57
58/// Basic authentication configuration
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct BasicAuthConfig {
61    /// Username/password pairs
62    pub credentials: HashMap<String, String>,
63}
64
65/// API key configuration
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ApiKeyConfig {
68    /// Expected header name (default: X-API-Key)
69    pub header_name: String,
70    /// Expected query parameter name
71    pub query_name: Option<String>,
72    /// Valid API keys
73    pub keys: Vec<String>,
74}
75
76impl Default for AuthConfig {
77    fn default() -> Self {
78        Self {
79            jwt: None,
80            oauth2: None,
81            basic_auth: None,
82            api_key: Some(ApiKeyConfig {
83                header_name: "X-API-Key".to_string(),
84                query_name: None,
85                keys: vec![],
86            }),
87            require_auth: false,
88        }
89    }
90}
91
92/// Route configuration for custom HTTP routes
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct RouteConfig {
95    /// Route path
96    pub path: String,
97    /// HTTP method
98    pub method: String,
99    /// Request configuration
100    pub request: Option<RouteRequestConfig>,
101    /// Response configuration
102    pub response: RouteResponseConfig,
103}
104
105/// Request configuration for routes
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct RouteRequestConfig {
108    /// Request validation configuration
109    pub validation: Option<RouteValidationConfig>,
110}
111
112/// Response configuration for routes
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RouteResponseConfig {
115    /// HTTP status code
116    pub status: u16,
117    /// Response headers
118    #[serde(default)]
119    pub headers: HashMap<String, String>,
120    /// Response body
121    pub body: Option<serde_json::Value>,
122}
123
124/// Validation configuration for routes
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RouteValidationConfig {
127    /// JSON schema for request validation
128    pub schema: serde_json::Value,
129}
130
131/// Protocol enable/disable configuration
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ProtocolConfig {
134    /// Enable this protocol
135    pub enabled: bool,
136}
137
138/// Protocols configuration
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ProtocolsConfig {
141    /// HTTP protocol configuration
142    pub http: ProtocolConfig,
143    /// GraphQL protocol configuration
144    pub graphql: ProtocolConfig,
145    /// gRPC protocol configuration
146    pub grpc: ProtocolConfig,
147    /// WebSocket protocol configuration
148    pub websocket: ProtocolConfig,
149    /// SMTP protocol configuration
150    pub smtp: ProtocolConfig,
151    /// MQTT protocol configuration
152    pub mqtt: ProtocolConfig,
153    /// FTP protocol configuration
154    pub ftp: ProtocolConfig,
155    /// Kafka protocol configuration
156    pub kafka: ProtocolConfig,
157    /// RabbitMQ protocol configuration
158    pub rabbitmq: ProtocolConfig,
159    /// AMQP protocol configuration
160    pub amqp: ProtocolConfig,
161}
162
163impl Default for ProtocolsConfig {
164    fn default() -> Self {
165        Self {
166            http: ProtocolConfig { enabled: true },
167            graphql: ProtocolConfig { enabled: true },
168            grpc: ProtocolConfig { enabled: true },
169            websocket: ProtocolConfig { enabled: true },
170            smtp: ProtocolConfig { enabled: false },
171            mqtt: ProtocolConfig { enabled: true },
172            ftp: ProtocolConfig { enabled: false },
173            kafka: ProtocolConfig { enabled: false },
174            rabbitmq: ProtocolConfig { enabled: false },
175            amqp: ProtocolConfig { enabled: false },
176        }
177    }
178}
179
180/// Server configuration
181#[derive(Debug, Clone, Serialize, Deserialize, Default)]
182#[serde(default)]
183pub struct ServerConfig {
184    /// HTTP server configuration
185    pub http: HttpConfig,
186    /// WebSocket server configuration
187    pub websocket: WebSocketConfig,
188    /// gRPC server configuration
189    pub grpc: GrpcConfig,
190    /// MQTT server configuration
191    pub mqtt: MqttConfig,
192    /// SMTP server configuration
193    pub smtp: SmtpConfig,
194    /// FTP server configuration
195    pub ftp: FtpConfig,
196    /// Kafka server configuration
197    pub kafka: KafkaConfig,
198    /// AMQP server configuration
199    pub amqp: AmqpConfig,
200    /// Admin UI configuration
201    pub admin: AdminConfig,
202    /// Request chaining configuration
203    pub chaining: ChainingConfig,
204    /// Core MockForge configuration
205    pub core: CoreConfig,
206    /// Logging configuration
207    pub logging: LoggingConfig,
208    /// Data generation configuration
209    pub data: DataConfig,
210    /// Observability configuration (metrics, tracing)
211    pub observability: ObservabilityConfig,
212    /// Multi-tenant workspace configuration
213    pub multi_tenant: crate::multi_tenant::MultiTenantConfig,
214    /// Custom routes configuration
215    #[serde(default)]
216    pub routes: Vec<RouteConfig>,
217    /// Protocol enable/disable configuration
218    #[serde(default)]
219    pub protocols: ProtocolsConfig,
220}
221
222// Default is derived for ServerConfig
223
224/// HTTP validation configuration
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct HttpValidationConfig {
227    /// Request validation mode: off, warn, enforce
228    pub mode: String,
229}
230
231/// HTTP CORS configuration
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct HttpCorsConfig {
234    /// Enable CORS
235    pub enabled: bool,
236    /// Allowed origins
237    #[serde(default)]
238    pub allowed_origins: Vec<String>,
239    /// Allowed methods
240    #[serde(default)]
241    pub allowed_methods: Vec<String>,
242    /// Allowed headers
243    #[serde(default)]
244    pub allowed_headers: Vec<String>,
245}
246
247/// HTTP server configuration
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct HttpConfig {
250    /// Enable HTTP server
251    pub enabled: bool,
252    /// Server port
253    pub port: u16,
254    /// Host address
255    pub host: String,
256    /// Path to OpenAPI spec file for HTTP server
257    pub openapi_spec: Option<String>,
258    /// CORS configuration
259    pub cors: Option<HttpCorsConfig>,
260    /// Request timeout in seconds
261    pub request_timeout_secs: u64,
262    /// Request validation configuration
263    pub validation: Option<HttpValidationConfig>,
264    /// Aggregate validation errors into JSON array
265    pub aggregate_validation_errors: bool,
266    /// Validate responses (warn-only logging)
267    pub validate_responses: bool,
268    /// Expand templating tokens in responses/examples
269    pub response_template_expand: bool,
270    /// Validation error HTTP status (e.g., 400 or 422)
271    pub validation_status: Option<u16>,
272    /// Per-route overrides: key "METHOD path" => mode (off/warn/enforce)
273    pub validation_overrides: std::collections::HashMap<String, String>,
274    /// When embedding Admin UI under HTTP, skip validation for the mounted prefix
275    pub skip_admin_validation: bool,
276    /// Authentication configuration
277    pub auth: Option<AuthConfig>,
278}
279
280impl Default for HttpConfig {
281    fn default() -> Self {
282        Self {
283            enabled: true,
284            port: 3000,
285            host: "0.0.0.0".to_string(),
286            openapi_spec: None,
287            cors: Some(HttpCorsConfig {
288                enabled: true,
289                allowed_origins: vec!["*".to_string()],
290                allowed_methods: vec![
291                    "GET".to_string(),
292                    "POST".to_string(),
293                    "PUT".to_string(),
294                    "DELETE".to_string(),
295                    "PATCH".to_string(),
296                    "OPTIONS".to_string(),
297                ],
298                allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
299            }),
300            request_timeout_secs: 30,
301            validation: Some(HttpValidationConfig {
302                mode: "enforce".to_string(),
303            }),
304            aggregate_validation_errors: true,
305            validate_responses: false,
306            response_template_expand: false,
307            validation_status: None,
308            validation_overrides: std::collections::HashMap::new(),
309            skip_admin_validation: true,
310            auth: None,
311        }
312    }
313}
314
315/// WebSocket server configuration
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct WebSocketConfig {
318    /// Enable WebSocket server
319    pub enabled: bool,
320    /// Server port
321    pub port: u16,
322    /// Host address
323    pub host: String,
324    /// Replay file path
325    pub replay_file: Option<String>,
326    /// Connection timeout in seconds
327    pub connection_timeout_secs: u64,
328}
329
330impl Default for WebSocketConfig {
331    fn default() -> Self {
332        Self {
333            enabled: true,
334            port: 3001,
335            host: "0.0.0.0".to_string(),
336            replay_file: None,
337            connection_timeout_secs: 300,
338        }
339    }
340}
341
342/// gRPC server configuration
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct GrpcConfig {
345    /// Enable gRPC server
346    pub enabled: bool,
347    /// Server port
348    pub port: u16,
349    /// Host address
350    pub host: String,
351    /// Proto files directory
352    pub proto_dir: Option<String>,
353    /// TLS configuration
354    pub tls: Option<TlsConfig>,
355}
356
357impl Default for GrpcConfig {
358    fn default() -> Self {
359        Self {
360            enabled: true,
361            port: 50051,
362            host: "0.0.0.0".to_string(),
363            proto_dir: None,
364            tls: None,
365        }
366    }
367}
368
369/// TLS configuration for gRPC
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct TlsConfig {
372    /// Certificate file path
373    pub cert_path: String,
374    /// Private key file path
375    pub key_path: String,
376}
377
378/// MQTT server configuration
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct MqttConfig {
381    /// Enable MQTT server
382    pub enabled: bool,
383    /// Server port
384    pub port: u16,
385    /// Host address
386    pub host: String,
387    /// Maximum connections
388    pub max_connections: usize,
389    /// Maximum packet size
390    pub max_packet_size: usize,
391    /// Keep-alive timeout in seconds
392    pub keep_alive_secs: u16,
393    /// Directory containing fixture files
394    pub fixtures_dir: Option<std::path::PathBuf>,
395    /// Enable retained messages
396    pub enable_retained_messages: bool,
397    /// Maximum retained messages
398    pub max_retained_messages: usize,
399}
400
401impl Default for MqttConfig {
402    fn default() -> Self {
403        Self {
404            enabled: false,
405            port: 1883,
406            host: "0.0.0.0".to_string(),
407            max_connections: 1000,
408            max_packet_size: 268435456, // 256 MB
409            keep_alive_secs: 60,
410            fixtures_dir: None,
411            enable_retained_messages: true,
412            max_retained_messages: 10000,
413        }
414    }
415}
416
417/// SMTP server configuration
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct SmtpConfig {
420    /// Enable SMTP server
421    pub enabled: bool,
422    /// Server port
423    pub port: u16,
424    /// Host address
425    pub host: String,
426    /// Server hostname for SMTP greeting
427    pub hostname: String,
428    /// Directory containing fixture files
429    pub fixtures_dir: Option<std::path::PathBuf>,
430    /// Connection timeout in seconds
431    pub timeout_secs: u64,
432    /// Maximum connections
433    pub max_connections: usize,
434    /// Enable mailbox storage
435    pub enable_mailbox: bool,
436    /// Maximum mailbox size
437    pub max_mailbox_messages: usize,
438    /// Enable STARTTLS support
439    pub enable_starttls: bool,
440    /// Path to TLS certificate file
441    pub tls_cert_path: Option<std::path::PathBuf>,
442    /// Path to TLS private key file
443    pub tls_key_path: Option<std::path::PathBuf>,
444}
445
446impl Default for SmtpConfig {
447    fn default() -> Self {
448        Self {
449            enabled: false,
450            port: 1025,
451            host: "0.0.0.0".to_string(),
452            hostname: "mockforge-smtp".to_string(),
453            fixtures_dir: Some(std::path::PathBuf::from("./fixtures/smtp")),
454            timeout_secs: 300,
455            max_connections: 10,
456            enable_mailbox: true,
457            max_mailbox_messages: 1000,
458            enable_starttls: false,
459            tls_cert_path: None,
460            tls_key_path: None,
461        }
462    }
463}
464
465/// FTP server configuration
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct FtpConfig {
468    /// Enable FTP server
469    pub enabled: bool,
470    /// Server port
471    pub port: u16,
472    /// Host address
473    pub host: String,
474    /// Passive mode port range
475    pub passive_ports: (u16, u16),
476    /// Maximum connections
477    pub max_connections: usize,
478    /// Connection timeout in seconds
479    pub timeout_secs: u64,
480    /// Allow anonymous access
481    pub allow_anonymous: bool,
482    /// Fixtures directory
483    pub fixtures_dir: Option<std::path::PathBuf>,
484    /// Virtual root directory
485    pub virtual_root: std::path::PathBuf,
486}
487
488impl Default for FtpConfig {
489    fn default() -> Self {
490        Self {
491            enabled: false,
492            port: 2121,
493            host: "0.0.0.0".to_string(),
494            passive_ports: (50000, 51000),
495            max_connections: 100,
496            timeout_secs: 300,
497            allow_anonymous: true,
498            fixtures_dir: None,
499            virtual_root: std::path::PathBuf::from("/mockforge"),
500        }
501    }
502}
503
504/// Kafka server configuration
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct KafkaConfig {
507    /// Enable Kafka server
508    pub enabled: bool,
509    /// Server port
510    pub port: u16,
511    /// Host address
512    pub host: String,
513    /// Broker ID
514    pub broker_id: i32,
515    /// Maximum connections
516    pub max_connections: usize,
517    /// Log retention time in milliseconds
518    pub log_retention_ms: i64,
519    /// Log segment size in bytes
520    pub log_segment_bytes: i64,
521    /// Fixtures directory
522    pub fixtures_dir: Option<std::path::PathBuf>,
523    /// Auto-create topics
524    pub auto_create_topics: bool,
525    /// Default number of partitions for new topics
526    pub default_partitions: i32,
527    /// Default replication factor for new topics
528    pub default_replication_factor: i16,
529}
530
531impl Default for KafkaConfig {
532    fn default() -> Self {
533        Self {
534            enabled: false,
535            port: 9092, // Standard Kafka port
536            host: "0.0.0.0".to_string(),
537            broker_id: 1,
538            max_connections: 1000,
539            log_retention_ms: 604800000,   // 7 days
540            log_segment_bytes: 1073741824, // 1 GB
541            fixtures_dir: None,
542            auto_create_topics: true,
543            default_partitions: 3,
544            default_replication_factor: 1,
545        }
546    }
547}
548
549/// AMQP server configuration
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct AmqpConfig {
552    /// Enable AMQP server
553    pub enabled: bool,
554    /// Server port
555    pub port: u16,
556    /// Host address
557    pub host: String,
558    /// Maximum connections
559    pub max_connections: usize,
560    /// Maximum channels per connection
561    pub max_channels_per_connection: u16,
562    /// Frame max size
563    pub frame_max: u32,
564    /// Heartbeat interval in seconds
565    pub heartbeat_interval: u16,
566    /// Fixtures directory
567    pub fixtures_dir: Option<std::path::PathBuf>,
568    /// Virtual hosts
569    pub virtual_hosts: Vec<String>,
570}
571
572impl Default for AmqpConfig {
573    fn default() -> Self {
574        Self {
575            enabled: false,
576            port: 5672, // Standard AMQP port
577            host: "0.0.0.0".to_string(),
578            max_connections: 1000,
579            max_channels_per_connection: 100,
580            frame_max: 131072, // 128 KB
581            heartbeat_interval: 60,
582            fixtures_dir: None,
583            virtual_hosts: vec!["/".to_string()],
584        }
585    }
586}
587
588/// Admin UI configuration
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct AdminConfig {
591    /// Enable admin UI
592    pub enabled: bool,
593    /// Admin UI port
594    pub port: u16,
595    /// Host address
596    pub host: String,
597    /// Authentication required
598    pub auth_required: bool,
599    /// Admin username (if auth required)
600    pub username: Option<String>,
601    /// Admin password (if auth required)
602    pub password: Option<String>,
603    /// Optional mount path to embed Admin UI under HTTP server (e.g., "/admin")
604    pub mount_path: Option<String>,
605    /// Enable Admin API endpoints (under `__mockforge`)
606    pub api_enabled: bool,
607    /// Prometheus server URL for analytics queries
608    pub prometheus_url: String,
609}
610
611impl Default for AdminConfig {
612    fn default() -> Self {
613        Self {
614            enabled: false,
615            port: 9080,
616            host: "127.0.0.1".to_string(),
617            auth_required: false,
618            username: None,
619            password: None,
620            mount_path: None,
621            api_enabled: true,
622            prometheus_url: "http://localhost:9090".to_string(),
623        }
624    }
625}
626
627/// Logging configuration
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct LoggingConfig {
630    /// Log level
631    pub level: String,
632    /// Enable JSON logging
633    pub json_format: bool,
634    /// Log file path (optional)
635    pub file_path: Option<String>,
636    /// Maximum log file size in MB
637    pub max_file_size_mb: u64,
638    /// Maximum number of log files to keep
639    pub max_files: u32,
640}
641
642impl Default for LoggingConfig {
643    fn default() -> Self {
644        Self {
645            level: "info".to_string(),
646            json_format: false,
647            file_path: None,
648            max_file_size_mb: 10,
649            max_files: 5,
650        }
651    }
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize)]
655#[serde(rename_all = "camelCase")]
656pub struct ChainingConfig {
657    /// Enable request chaining
658    pub enabled: bool,
659    /// Maximum chain length to prevent infinite loops
660    pub max_chain_length: usize,
661    /// Global timeout for chain execution in seconds
662    pub global_timeout_secs: u64,
663    /// Enable parallel execution when dependencies allow
664    pub enable_parallel_execution: bool,
665}
666
667impl Default for ChainingConfig {
668    fn default() -> Self {
669        Self {
670            enabled: false,
671            max_chain_length: 20,
672            global_timeout_secs: 300,
673            enable_parallel_execution: false,
674        }
675    }
676}
677
678/// Data generation configuration
679#[derive(Debug, Clone, Serialize, Deserialize)]
680pub struct DataConfig {
681    /// Default number of rows to generate
682    pub default_rows: usize,
683    /// Default output format
684    pub default_format: String,
685    /// Faker locale
686    pub locale: String,
687    /// Custom faker templates
688    pub templates: HashMap<String, String>,
689    /// RAG configuration
690    pub rag: RagConfig,
691}
692
693impl Default for DataConfig {
694    fn default() -> Self {
695        Self {
696            default_rows: 100,
697            default_format: "json".to_string(),
698            locale: "en".to_string(),
699            templates: HashMap::new(),
700            rag: RagConfig::default(),
701        }
702    }
703}
704
705/// RAG configuration
706#[derive(Debug, Clone, Serialize, Deserialize)]
707#[serde(default)]
708pub struct RagConfig {
709    /// Enable RAG by default
710    pub enabled: bool,
711    /// LLM provider (openai, anthropic, ollama, openai_compatible)
712    #[serde(default)]
713    pub provider: String,
714    /// API endpoint for LLM
715    pub api_endpoint: Option<String>,
716    /// API key for LLM
717    pub api_key: Option<String>,
718    /// Model name
719    pub model: Option<String>,
720    /// Maximum tokens for generation
721    #[serde(default = "default_max_tokens")]
722    pub max_tokens: usize,
723    /// Temperature for generation (0.0 to 2.0)
724    #[serde(default = "default_temperature")]
725    pub temperature: f64,
726    /// Context window size
727    pub context_window: usize,
728    /// Enable caching
729    #[serde(default = "default_true")]
730    pub caching: bool,
731    /// Cache TTL in seconds
732    #[serde(default = "default_cache_ttl")]
733    pub cache_ttl_secs: u64,
734    /// Request timeout in seconds
735    #[serde(default = "default_timeout")]
736    pub timeout_secs: u64,
737    /// Maximum retries for failed requests
738    #[serde(default = "default_max_retries")]
739    pub max_retries: usize,
740}
741
742fn default_max_tokens() -> usize {
743    1024
744}
745
746fn default_temperature() -> f64 {
747    0.7
748}
749
750fn default_true() -> bool {
751    true
752}
753
754fn default_cache_ttl() -> u64 {
755    3600
756}
757
758fn default_timeout() -> u64 {
759    30
760}
761
762fn default_max_retries() -> usize {
763    3
764}
765
766impl Default for RagConfig {
767    fn default() -> Self {
768        Self {
769            enabled: false,
770            provider: "openai".to_string(),
771            api_endpoint: None,
772            api_key: None,
773            model: Some("gpt-3.5-turbo".to_string()),
774            max_tokens: default_max_tokens(),
775            temperature: default_temperature(),
776            context_window: 4000,
777            caching: default_true(),
778            cache_ttl_secs: default_cache_ttl(),
779            timeout_secs: default_timeout(),
780            max_retries: default_max_retries(),
781        }
782    }
783}
784
785/// Observability configuration for metrics and distributed tracing
786#[derive(Debug, Clone, Serialize, Deserialize, Default)]
787pub struct ObservabilityConfig {
788    /// Prometheus metrics configuration
789    pub prometheus: PrometheusConfig,
790    /// OpenTelemetry distributed tracing configuration
791    pub opentelemetry: Option<OpenTelemetryConfig>,
792    /// API Flight Recorder configuration
793    pub recorder: Option<RecorderConfig>,
794    /// Chaos engineering configuration
795    pub chaos: Option<ChaosEngConfig>,
796}
797
798/// Prometheus metrics configuration
799#[derive(Debug, Clone, Serialize, Deserialize)]
800pub struct PrometheusConfig {
801    /// Enable Prometheus metrics endpoint
802    pub enabled: bool,
803    /// Port for metrics endpoint
804    pub port: u16,
805    /// Host for metrics endpoint
806    pub host: String,
807    /// Path for metrics endpoint
808    pub path: String,
809}
810
811impl Default for PrometheusConfig {
812    fn default() -> Self {
813        Self {
814            enabled: true,
815            port: 9090,
816            host: "0.0.0.0".to_string(),
817            path: "/metrics".to_string(),
818        }
819    }
820}
821
822/// OpenTelemetry distributed tracing configuration
823#[derive(Debug, Clone, Serialize, Deserialize)]
824pub struct OpenTelemetryConfig {
825    /// Enable OpenTelemetry tracing
826    pub enabled: bool,
827    /// Service name for traces
828    pub service_name: String,
829    /// Deployment environment (development, staging, production)
830    pub environment: String,
831    /// Jaeger endpoint for trace export
832    pub jaeger_endpoint: String,
833    /// OTLP endpoint (alternative to Jaeger)
834    pub otlp_endpoint: Option<String>,
835    /// Protocol: grpc or http
836    pub protocol: String,
837    /// Sampling rate (0.0 to 1.0)
838    pub sampling_rate: f64,
839}
840
841impl Default for OpenTelemetryConfig {
842    fn default() -> Self {
843        Self {
844            enabled: false,
845            service_name: "mockforge".to_string(),
846            environment: "development".to_string(),
847            jaeger_endpoint: "http://localhost:14268/api/traces".to_string(),
848            otlp_endpoint: Some("http://localhost:4317".to_string()),
849            protocol: "grpc".to_string(),
850            sampling_rate: 1.0,
851        }
852    }
853}
854
855/// API Flight Recorder configuration
856#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct RecorderConfig {
858    /// Enable recording
859    pub enabled: bool,
860    /// Database file path
861    pub database_path: String,
862    /// Enable management API
863    pub api_enabled: bool,
864    /// Management API port (if different from main port)
865    pub api_port: Option<u16>,
866    /// Maximum number of requests to store (0 for unlimited)
867    pub max_requests: i64,
868    /// Auto-delete requests older than N days (0 to disable)
869    pub retention_days: i64,
870    /// Record HTTP requests
871    pub record_http: bool,
872    /// Record gRPC requests
873    pub record_grpc: bool,
874    /// Record WebSocket messages
875    pub record_websocket: bool,
876    /// Record GraphQL requests
877    pub record_graphql: bool,
878}
879
880impl Default for RecorderConfig {
881    fn default() -> Self {
882        Self {
883            enabled: false,
884            database_path: "./mockforge-recordings.db".to_string(),
885            api_enabled: true,
886            api_port: None,
887            max_requests: 10000,
888            retention_days: 7,
889            record_http: true,
890            record_grpc: true,
891            record_websocket: true,
892            record_graphql: true,
893        }
894    }
895}
896
897/// Chaos engineering configuration
898#[derive(Debug, Clone, Serialize, Deserialize, Default)]
899pub struct ChaosEngConfig {
900    /// Enable chaos engineering
901    pub enabled: bool,
902    /// Latency injection configuration
903    pub latency: Option<LatencyInjectionConfig>,
904    /// Fault injection configuration
905    pub fault_injection: Option<FaultConfig>,
906    /// Rate limiting configuration
907    pub rate_limit: Option<RateLimitingConfig>,
908    /// Traffic shaping configuration
909    pub traffic_shaping: Option<NetworkShapingConfig>,
910    /// Predefined scenario to use
911    pub scenario: Option<String>,
912}
913
914/// Latency injection configuration
915#[derive(Debug, Clone, Serialize, Deserialize)]
916pub struct LatencyInjectionConfig {
917    pub enabled: bool,
918    pub fixed_delay_ms: Option<u64>,
919    pub random_delay_range_ms: Option<(u64, u64)>,
920    pub jitter_percent: f64,
921    pub probability: f64,
922}
923
924/// Fault injection configuration
925#[derive(Debug, Clone, Serialize, Deserialize)]
926pub struct FaultConfig {
927    pub enabled: bool,
928    pub http_errors: Vec<u16>,
929    pub http_error_probability: f64,
930    pub connection_errors: bool,
931    pub connection_error_probability: f64,
932    pub timeout_errors: bool,
933    pub timeout_ms: u64,
934    pub timeout_probability: f64,
935}
936
937/// Rate limiting configuration
938#[derive(Debug, Clone, Serialize, Deserialize)]
939pub struct RateLimitingConfig {
940    pub enabled: bool,
941    pub requests_per_second: u32,
942    pub burst_size: u32,
943    pub per_ip: bool,
944    pub per_endpoint: bool,
945}
946
947/// Network shaping configuration
948#[derive(Debug, Clone, Serialize, Deserialize)]
949pub struct NetworkShapingConfig {
950    pub enabled: bool,
951    pub bandwidth_limit_bps: u64,
952    pub packet_loss_percent: f64,
953    pub max_connections: u32,
954}
955
956/// Load configuration from file
957pub async fn load_config<P: AsRef<Path>>(path: P) -> Result<ServerConfig> {
958    let content = fs::read_to_string(&path)
959        .await
960        .map_err(|e| Error::generic(format!("Failed to read config file: {}", e)))?;
961
962    let config: ServerConfig = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
963        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
964    {
965        serde_yaml::from_str(&content)
966            .map_err(|e| Error::generic(format!("Failed to parse YAML config: {}", e)))?
967    } else {
968        serde_json::from_str(&content)
969            .map_err(|e| Error::generic(format!("Failed to parse JSON config: {}", e)))?
970    };
971
972    Ok(config)
973}
974
975/// Save configuration to file
976pub async fn save_config<P: AsRef<Path>>(path: P, config: &ServerConfig) -> Result<()> {
977    let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("yaml")
978        || path.as_ref().extension().and_then(|s| s.to_str()) == Some("yml")
979    {
980        serde_yaml::to_string(config)
981            .map_err(|e| Error::generic(format!("Failed to serialize config to YAML: {}", e)))?
982    } else {
983        serde_json::to_string_pretty(config)
984            .map_err(|e| Error::generic(format!("Failed to serialize config to JSON: {}", e)))?
985    };
986
987    fs::write(path, content)
988        .await
989        .map_err(|e| Error::generic(format!("Failed to write config file: {}", e)))?;
990
991    Ok(())
992}
993
994/// Load configuration with fallback to default
995pub async fn load_config_with_fallback<P: AsRef<Path>>(path: P) -> ServerConfig {
996    match load_config(&path).await {
997        Ok(config) => {
998            tracing::info!("Loaded configuration from {:?}", path.as_ref());
999            config
1000        }
1001        Err(e) => {
1002            tracing::warn!(
1003                "Failed to load config from {:?}: {}. Using defaults.",
1004                path.as_ref(),
1005                e
1006            );
1007            ServerConfig::default()
1008        }
1009    }
1010}
1011
1012/// Create default configuration file
1013pub async fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
1014    let config = ServerConfig::default();
1015    save_config(path, &config).await?;
1016    Ok(())
1017}
1018
1019/// Environment variable overrides for configuration
1020pub fn apply_env_overrides(mut config: ServerConfig) -> ServerConfig {
1021    // HTTP server overrides
1022    if let Ok(port) = std::env::var("MOCKFORGE_HTTP_PORT") {
1023        if let Ok(port_num) = port.parse() {
1024            config.http.port = port_num;
1025        }
1026    }
1027
1028    if let Ok(host) = std::env::var("MOCKFORGE_HTTP_HOST") {
1029        config.http.host = host;
1030    }
1031
1032    // WebSocket server overrides
1033    if let Ok(port) = std::env::var("MOCKFORGE_WS_PORT") {
1034        if let Ok(port_num) = port.parse() {
1035            config.websocket.port = port_num;
1036        }
1037    }
1038
1039    // gRPC server overrides
1040    if let Ok(port) = std::env::var("MOCKFORGE_GRPC_PORT") {
1041        if let Ok(port_num) = port.parse() {
1042            config.grpc.port = port_num;
1043        }
1044    }
1045
1046    // SMTP server overrides
1047    if let Ok(port) = std::env::var("MOCKFORGE_SMTP_PORT") {
1048        if let Ok(port_num) = port.parse() {
1049            config.smtp.port = port_num;
1050        }
1051    }
1052
1053    if let Ok(host) = std::env::var("MOCKFORGE_SMTP_HOST") {
1054        config.smtp.host = host;
1055    }
1056
1057    if let Ok(enabled) = std::env::var("MOCKFORGE_SMTP_ENABLED") {
1058        config.smtp.enabled = enabled == "1" || enabled.eq_ignore_ascii_case("true");
1059    }
1060
1061    if let Ok(hostname) = std::env::var("MOCKFORGE_SMTP_HOSTNAME") {
1062        config.smtp.hostname = hostname;
1063    }
1064
1065    // Admin UI overrides
1066    if let Ok(port) = std::env::var("MOCKFORGE_ADMIN_PORT") {
1067        if let Ok(port_num) = port.parse() {
1068            config.admin.port = port_num;
1069        }
1070    }
1071
1072    if std::env::var("MOCKFORGE_ADMIN_ENABLED").unwrap_or_default() == "true" {
1073        config.admin.enabled = true;
1074    }
1075
1076    if let Ok(mount_path) = std::env::var("MOCKFORGE_ADMIN_MOUNT_PATH") {
1077        if !mount_path.trim().is_empty() {
1078            config.admin.mount_path = Some(mount_path);
1079        }
1080    }
1081
1082    if let Ok(api_enabled) = std::env::var("MOCKFORGE_ADMIN_API_ENABLED") {
1083        let on = api_enabled == "1" || api_enabled.eq_ignore_ascii_case("true");
1084        config.admin.api_enabled = on;
1085    }
1086
1087    if let Ok(prometheus_url) = std::env::var("PROMETHEUS_URL") {
1088        config.admin.prometheus_url = prometheus_url;
1089    }
1090
1091    // Core configuration overrides
1092    if let Ok(latency_enabled) = std::env::var("MOCKFORGE_LATENCY_ENABLED") {
1093        let enabled = latency_enabled == "1" || latency_enabled.eq_ignore_ascii_case("true");
1094        config.core.latency_enabled = enabled;
1095    }
1096
1097    if let Ok(failures_enabled) = std::env::var("MOCKFORGE_FAILURES_ENABLED") {
1098        let enabled = failures_enabled == "1" || failures_enabled.eq_ignore_ascii_case("true");
1099        config.core.failures_enabled = enabled;
1100    }
1101
1102    if let Ok(overrides_enabled) = std::env::var("MOCKFORGE_OVERRIDES_ENABLED") {
1103        let enabled = overrides_enabled == "1" || overrides_enabled.eq_ignore_ascii_case("true");
1104        config.core.overrides_enabled = enabled;
1105    }
1106
1107    if let Ok(traffic_shaping_enabled) = std::env::var("MOCKFORGE_TRAFFIC_SHAPING_ENABLED") {
1108        let enabled =
1109            traffic_shaping_enabled == "1" || traffic_shaping_enabled.eq_ignore_ascii_case("true");
1110        config.core.traffic_shaping_enabled = enabled;
1111    }
1112
1113    // Traffic shaping overrides
1114    if let Ok(bandwidth_enabled) = std::env::var("MOCKFORGE_BANDWIDTH_ENABLED") {
1115        let enabled = bandwidth_enabled == "1" || bandwidth_enabled.eq_ignore_ascii_case("true");
1116        config.core.traffic_shaping.bandwidth.enabled = enabled;
1117    }
1118
1119    if let Ok(max_bytes_per_sec) = std::env::var("MOCKFORGE_BANDWIDTH_MAX_BYTES_PER_SEC") {
1120        if let Ok(bytes) = max_bytes_per_sec.parse() {
1121            config.core.traffic_shaping.bandwidth.max_bytes_per_sec = bytes;
1122            config.core.traffic_shaping.bandwidth.enabled = true;
1123        }
1124    }
1125
1126    if let Ok(burst_capacity) = std::env::var("MOCKFORGE_BANDWIDTH_BURST_CAPACITY_BYTES") {
1127        if let Ok(bytes) = burst_capacity.parse() {
1128            config.core.traffic_shaping.bandwidth.burst_capacity_bytes = bytes;
1129        }
1130    }
1131
1132    if let Ok(burst_loss_enabled) = std::env::var("MOCKFORGE_BURST_LOSS_ENABLED") {
1133        let enabled = burst_loss_enabled == "1" || burst_loss_enabled.eq_ignore_ascii_case("true");
1134        config.core.traffic_shaping.burst_loss.enabled = enabled;
1135    }
1136
1137    if let Ok(burst_probability) = std::env::var("MOCKFORGE_BURST_LOSS_PROBABILITY") {
1138        if let Ok(prob) = burst_probability.parse::<f64>() {
1139            config.core.traffic_shaping.burst_loss.burst_probability = prob.clamp(0.0, 1.0);
1140            config.core.traffic_shaping.burst_loss.enabled = true;
1141        }
1142    }
1143
1144    if let Ok(burst_duration) = std::env::var("MOCKFORGE_BURST_LOSS_DURATION_MS") {
1145        if let Ok(ms) = burst_duration.parse() {
1146            config.core.traffic_shaping.burst_loss.burst_duration_ms = ms;
1147        }
1148    }
1149
1150    if let Ok(loss_rate) = std::env::var("MOCKFORGE_BURST_LOSS_RATE") {
1151        if let Ok(rate) = loss_rate.parse::<f64>() {
1152            config.core.traffic_shaping.burst_loss.loss_rate_during_burst = rate.clamp(0.0, 1.0);
1153        }
1154    }
1155
1156    if let Ok(recovery_time) = std::env::var("MOCKFORGE_BURST_LOSS_RECOVERY_MS") {
1157        if let Ok(ms) = recovery_time.parse() {
1158            config.core.traffic_shaping.burst_loss.recovery_time_ms = ms;
1159        }
1160    }
1161
1162    // Logging overrides
1163    if let Ok(level) = std::env::var("MOCKFORGE_LOG_LEVEL") {
1164        config.logging.level = level;
1165    }
1166
1167    config
1168}
1169
1170/// Validate configuration
1171pub fn validate_config(config: &ServerConfig) -> Result<()> {
1172    // Validate port ranges
1173    if config.http.port == 0 {
1174        return Err(Error::generic("HTTP port cannot be 0"));
1175    }
1176    if config.websocket.port == 0 {
1177        return Err(Error::generic("WebSocket port cannot be 0"));
1178    }
1179    if config.grpc.port == 0 {
1180        return Err(Error::generic("gRPC port cannot be 0"));
1181    }
1182    if config.admin.port == 0 {
1183        return Err(Error::generic("Admin port cannot be 0"));
1184    }
1185
1186    // Check for port conflicts
1187    let ports = [
1188        ("HTTP", config.http.port),
1189        ("WebSocket", config.websocket.port),
1190        ("gRPC", config.grpc.port),
1191        ("Admin", config.admin.port),
1192    ];
1193
1194    for i in 0..ports.len() {
1195        for j in (i + 1)..ports.len() {
1196            if ports[i].1 == ports[j].1 {
1197                return Err(Error::generic(format!(
1198                    "Port conflict: {} and {} both use port {}",
1199                    ports[i].0, ports[j].0, ports[i].1
1200                )));
1201            }
1202        }
1203    }
1204
1205    // Validate log level
1206    let valid_levels = ["trace", "debug", "info", "warn", "error"];
1207    if !valid_levels.contains(&config.logging.level.as_str()) {
1208        return Err(Error::generic(format!(
1209            "Invalid log level: {}. Valid levels: {}",
1210            config.logging.level,
1211            valid_levels.join(", ")
1212        )));
1213    }
1214
1215    Ok(())
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220    use super::*;
1221
1222    #[test]
1223    fn test_default_config() {
1224        let config = ServerConfig::default();
1225        assert_eq!(config.http.port, 3000);
1226        assert_eq!(config.websocket.port, 3001);
1227        assert_eq!(config.grpc.port, 50051);
1228        assert_eq!(config.admin.port, 9080);
1229    }
1230
1231    #[test]
1232    fn test_config_validation() {
1233        let mut config = ServerConfig::default();
1234        assert!(validate_config(&config).is_ok());
1235
1236        // Test port conflict
1237        config.websocket.port = config.http.port;
1238        assert!(validate_config(&config).is_err());
1239
1240        // Test invalid log level
1241        config.websocket.port = 3001; // Fix port conflict
1242        config.logging.level = "invalid".to_string();
1243        assert!(validate_config(&config).is_err());
1244    }
1245}