mockforge_sdk/
builder.rs

1//! Builder for configuring mock servers
2
3use crate::server::MockServer;
4use crate::{Error, Result};
5use mockforge_core::{Config, FailureConfig, LatencyProfile, ProxyConfig, ServerConfig};
6use std::net::TcpListener;
7use std::path::PathBuf;
8
9/// Builder for creating and configuring mock servers
10pub struct MockServerBuilder {
11    port: Option<u16>,
12    host: Option<String>,
13    config_file: Option<PathBuf>,
14    openapi_spec: Option<PathBuf>,
15    latency_profile: Option<LatencyProfile>,
16    failure_config: Option<FailureConfig>,
17    proxy_config: Option<ProxyConfig>,
18    enable_admin: bool,
19    admin_port: Option<u16>,
20    auto_port: bool,
21    port_range: Option<(u16, u16)>,
22}
23
24impl Default for MockServerBuilder {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl MockServerBuilder {
31    /// Create a new mock server builder
32    pub fn new() -> Self {
33        Self {
34            port: None,
35            host: None,
36            config_file: None,
37            openapi_spec: None,
38            latency_profile: None,
39            failure_config: None,
40            proxy_config: None,
41            enable_admin: false,
42            admin_port: None,
43            auto_port: false,
44            port_range: None,
45        }
46    }
47
48    /// Set the HTTP port
49    pub fn port(mut self, port: u16) -> Self {
50        self.port = Some(port);
51        self.auto_port = false;
52        self
53    }
54
55    /// Automatically discover an available port
56    pub fn auto_port(mut self) -> Self {
57        self.auto_port = true;
58        self.port = None;
59        self
60    }
61
62    /// Set the port range for automatic port discovery
63    /// Default range is 30000-30100
64    pub fn port_range(mut self, start: u16, end: u16) -> Self {
65        self.port_range = Some((start, end));
66        self
67    }
68
69    /// Set the host address
70    pub fn host(mut self, host: impl Into<String>) -> Self {
71        self.host = Some(host.into());
72        self
73    }
74
75    /// Load configuration from a YAML file
76    pub fn config_file(mut self, path: impl Into<PathBuf>) -> Self {
77        self.config_file = Some(path.into());
78        self
79    }
80
81    /// Load routes from an OpenAPI specification
82    pub fn openapi_spec(mut self, path: impl Into<PathBuf>) -> Self {
83        self.openapi_spec = Some(path.into());
84        self
85    }
86
87    /// Set the latency profile for simulating network delays
88    pub fn latency(mut self, profile: LatencyProfile) -> Self {
89        self.latency_profile = Some(profile);
90        self
91    }
92
93    /// Enable failure injection with configuration
94    pub fn failures(mut self, config: FailureConfig) -> Self {
95        self.failure_config = Some(config);
96        self
97    }
98
99    /// Enable proxy mode with configuration
100    pub fn proxy(mut self, config: ProxyConfig) -> Self {
101        self.proxy_config = Some(config);
102        self
103    }
104
105    /// Enable admin API
106    pub fn admin(mut self, enabled: bool) -> Self {
107        self.enable_admin = enabled;
108        self
109    }
110
111    /// Set admin API port
112    pub fn admin_port(mut self, port: u16) -> Self {
113        self.admin_port = Some(port);
114        self
115    }
116
117    /// Start the mock server
118    pub async fn start(self) -> Result<MockServer> {
119        // Build the configuration
120        let mut config = if let Some(config_file) = self.config_file {
121            mockforge_core::load_config(&config_file)
122                .await
123                .map_err(|e| Error::InvalidConfig(e.to_string()))?
124        } else {
125            ServerConfig::default()
126        };
127
128        // Apply port settings
129        if self.auto_port {
130            // Discover an available port
131            let (start, end) = self.port_range.unwrap_or((30000, 30100));
132            let port = find_available_port(start, end)?;
133            config.http.port = port;
134        } else if let Some(port) = self.port {
135            config.http.port = port;
136        }
137
138        // Apply other builder settings
139        if let Some(host) = self.host {
140            config.http.host = host;
141        }
142        if let Some(spec_path) = self.openapi_spec {
143            config.http.openapi_spec = Some(spec_path.to_string_lossy().to_string());
144        }
145
146        // Create core config
147        let mut core_config = Config::default();
148
149        if let Some(latency) = self.latency_profile {
150            core_config.latency_enabled = true;
151            core_config.default_latency = latency;
152        }
153
154        if let Some(failures) = self.failure_config {
155            core_config.failures_enabled = true;
156            core_config.failure_config = Some(failures);
157        }
158
159        if let Some(proxy) = self.proxy_config {
160            core_config.proxy = Some(proxy);
161        }
162
163        // Create and start the server
164        MockServer::from_config(config, core_config).await
165    }
166}
167
168/// Check if a port is available by attempting to bind to it
169///
170/// Note: There is a small race condition (TOCTOU - Time Of Check, Time Of Use)
171/// between checking availability and the actual server binding. In practice,
172/// this is rarely an issue for test environments. For guaranteed port assignment,
173/// consider using `port(0)` to let the OS assign any available port.
174fn is_port_available(port: u16) -> bool {
175    TcpListener::bind(("127.0.0.1", port)).is_ok()
176}
177
178/// Find an available port in the specified range
179///
180/// Scans the port range and returns the first available port.
181/// Returns an error if no ports are available in the range.
182///
183/// # Arguments
184/// * `start` - Starting port number (inclusive)
185/// * `end` - Ending port number (inclusive)
186///
187/// # Errors
188/// Returns `Error::InvalidConfig` if start >= end
189/// Returns `Error::PortDiscoveryFailed` if no ports are available
190fn find_available_port(start: u16, end: u16) -> Result<u16> {
191    // Validate port range
192    if start >= end {
193        return Err(Error::InvalidConfig(format!(
194            "Invalid port range: start ({}) must be less than end ({})",
195            start, end
196        )));
197    }
198
199    // Try to find an available port
200    for port in start..=end {
201        if is_port_available(port) {
202            return Ok(port);
203        }
204    }
205
206    Err(Error::PortDiscoveryFailed(format!(
207        "No available ports found in range {}-{}",
208        start, end
209    )))
210}