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