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